女人自慰AV免费观看内涵网,日韩国产剧情在线观看网址,神马电影网特片网,最新一级电影欧美,在线观看亚洲欧美日韩,黄色视频在线播放免费观看,ABO涨奶期羡澄,第一导航fulione,美女主播操b

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

源碼級深度理解Java SPI

OSC開源社區 ? 來源:OSC開源社區 ? 作者:Zhang Peng ? 2022-11-15 11:38 ? 次閱讀

SPI 是一種用于動態加載服務的機制。它的核心思想就是解耦,屬于典型的微內核架構模式。SPI 在 Java 世界應用非常廣泛,如:Dubbo、Spring Boot 等框架。本文從源碼入手分析,深入探討 Java SPI 的特性、原理,以及在一些比較經典領域的應用。

一、SPI 簡介

SPI 全稱 Service Provider Interface,是 Java 提供的,旨在由第三方實現或擴展的 API,它是一種用于動態加載服務的機制。Java 中 SPI 機制主要思想是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要,其核心思想就是解耦。

Java SPI 有四個要素:

  • SPI 接口為服務提供者實現類約定的的接口或抽象類。

  • SPI 實現類:實際提供服務的實現類。

  • SPI 配置:Java SPI 機制約定的配置文件,提供查找服務實現類的邏輯。配置文件必須置于 META-INF/services 目錄中,并且,文件名應與服務提供者接口的完全限定名保持一致。文件中的每一行都有一個實現服務類的詳細信息,同樣是服務提供者類的完全限定名稱。

  • ServiceLoader:Java SPI 的核心類,用于加載 SPI 實現類。ServiceLoader 中有各種實用方法來獲取特定實現、迭代它們或重新加載服務。

二、SPI 示例

正所謂,實踐出真知,我們不妨通過一個具體的示例來看一下,如何使用 Java SPI。

2.1 SPI 接口

首先,需要定義一個 SPI 接口,和普通接口并沒有什么差別。

package io.github.dunwu.javacore.spi;


public interface DataStorage {
    String search(String key);
}

2.2 SPI 實現類

假設,我們需要在程序中使用兩種不同的數據存儲——MySQL 和 Redis。因此,我們需要兩個不同的實現類去分別完成相應工作。

MySQL查詢 MOCK 類

package io.github.dunwu.javacore.spi;


public class MysqlStorage implements DataStorage {
    @Override
    public String search(String key) {
        return "【Mysql】搜索" + key + ",結果:No";
    }
}

Redis 查詢 MOCK 類

package io.github.dunwu.javacore.spi;


public class RedisStorage implements DataStorage {
    @Override
    public String search(String key) {
        return "【Redis】搜索" + key + ",結果:Yes";
    }
}

service 傳入的是期望加載的 SPI 接口類型 到目前為止,定義接口,并實現接口和普通的 Java 接口實現沒有任何不同。

2.3 SPI 配置

如果想通過 Java SPI 機制來發現服務,就需要在 SPI 配置中約定好發現服務的邏輯。配置文件必須置于 META-INF/services 目錄中,并且,文件名應與服務提供者接口的完全限定名保持一致。文件中的每一行都有一個實現服務類的詳細信息,同樣是服務提供者類的完全限定名稱。以本示例代碼為例,其文件名應該為

io.github.dunwu.javacore.spi.DataStorage

文件中的內容如下:

io.github.dunwu.javacore.spi.MysqlStorage
io.github.dunwu.javacore.spi.RedisStorage

2.4 ServiceLoader

完成了上面的步驟,就可以通過 ServiceLoader 來加載服務。示例如下:

import java.util.ServiceLoader;


public class SpiDemo {


    public static void main(String[] args) {
        ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);
        System.out.println("============ Java SPI 測試============");
        serviceLoader.forEach(loader -> System.out.println(loader.search("Yes Or No")));
    }


}

輸出:

============ Java SPI 測試============
【Mysql】搜索Yes Or No,結果:No
【Redis】搜索Yes Or No,結果:Yes

三、SPI 原理

上文中,我們已經了解 Java SPI 的要素以及使用 Java SPI 的方法。你有沒有想過,Java SPI 和普通 Java 接口有何不同,Java SPI 是如何工作的。實際上,Java SPI 機制依賴于 ServiceLoader 類去解析、加載服務。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。ServiceLoader 的代碼本身很精練,接下來,讓我們通過走讀源碼的方式,逐一理解 ServiceLoader 的工作流程。

3.1 ServiceLoader 的成員變量

先看一下 ServiceLoader 類的成員變量,大致有個印象,后面的源碼中都會使用到。

public final class ServiceLoader<S> implements Iterable<S> {


    // SPI 配置文件目錄
    private static final String PREFIX = "META-INF/services/";


    // 將要被加載的 SPI 服務
    private final Class service;


    // 用于加載 SPI 服務的類加載器
    private final ClassLoader loader;


    // ServiceLoader 創建時的訪問控制上下文
    private final AccessControlContext acc;


    // SPI 服務緩存,按實例化的順序排列
    private LinkedHashMap providers = new LinkedHashMap<>();


    // 懶查詢迭代器
    private LazyIterator lookupIterator;


    // ...
}

3.2 ServiceLoader 的工作流程

(1)ServiceLoader.load靜態方法

應用程序加載 Java SPI 服務,都是先調用 ServiceLoader.load 靜態方法。

ServiceLoader.load 靜態方法的作用是:

① 指定類加載 ClassLoader 和訪問控制上下文;

② 然后,重新加載 SPI 服務

  • 清空緩存中所有已實例化的 SPI 服務

  • 根據ClassLoader和 SPI 類型,創建懶加載迭代器

這里,摘錄 ServiceLoader.load 相關源碼,如下:

// service 傳入的是期望加載的 SPI 接口類型
// loader 是用于加載 SPI 服務的類加載器
public static  ServiceLoader load(Class service, ClassLoader loader) {
  return new ServiceLoader<>(service, loader);
}


public void reload() {
    // 清空緩存中所有已實例化的 SPI 服務
  providers.clear();
    // 根據 ClassLoader 和 SPI 類型,創建懶加載迭代器
  lookupIterator = new LazyIterator(service, loader);
}


// 私有構造方法
// 重新加載 SPI 服務
private ServiceLoader(Class svc, ClassLoader cl) {
  service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 指定類加載 ClassLoader 和訪問控制上下文
  loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 然后,重新加載 SPI 服務
  reload();
}

(2)應用程序通過ServiceLoader的iterator方法遍歷 SPI 實例

ServiceLoader 的類定義,明確了 ServiceLoader 類實現了 Iterable接口,所以,它是可以迭代遍歷的。實際上,ServiceLoader 類維護了一個緩存 providers( LinkedHashMap 對象),緩存 providers 中保存了已經被成功加載的 SPI 實例,這個 Map 的 key 是 SPI 接口實現類的全限定名,value 是該實現類的一個實例對象。

當應用程序調用 ServiceLoader 的 iterator 方法時,ServiceLoader 會先判斷緩存 providers 中是否有數據:如果有,則直接返回緩存 providers 的迭代器;如果沒有,則返回懶加載迭代器的迭代器。

public Iterator iterator() {
  return new Iterator() {


        // 緩存 SPI providers
    Iterator> knownProviders
      = providers.entrySet().iterator();


        // lookupIterator 是 LazyIterator 實例,用于懶加載 SPI 實例
    public boolean hasNext() {
      if (knownProviders.hasNext())
        return true;
      return lookupIterator.hasNext();
    }


    public S next() {
      if (knownProviders.hasNext())
        return knownProviders.next().getValue();
      return lookupIterator.next();
    }


    public void remove() {
      throw new UnsupportedOperationException();
    }


  };
}

(3)懶加載迭代器的工作流程

上面的源碼中提到了,lookupIterator 是 LazyIterator 實例,而 LazyIterator 用于懶加載 SPI 實例。那么, LazyIterator 是如何工作的呢?

這里,摘取LazyIterator關鍵代碼

hasNextService 方法:

  • 拼接META-INF/services/+ SPI 接口全限定名

  • 通過類加載器,嘗試加載資源文件

  • 解析資源文件中的內容,獲取 SPI 接口的實現類的全限定名nextName

nextService 方法:

  • hasNextService()方法解析出了 SPI 實現類的的全限定名 nextName,通過反射,獲取 SPI 實現類的類定義 Class。

  • 然后,嘗試通過 Class 的 newInstance 方法實例化一個 SPI 服務對象。如果成功,則將這個對象加入到緩存 providers 中并返回該對象。

private boolean hasNextService() {
  if (nextName != null) {
    return true;
  }
  if (configs == null) {
    try {
            // 1.拼接 META-INF/services/ + SPI 接口全限定名
            // 2.通過類加載器,嘗試加載資源文件
            // 3.解析資源文件中的內容
      String fullName = PREFIX + service.getName();
      if (loader == null)
        configs = ClassLoader.getSystemResources(fullName);
      else
        configs = loader.getResources(fullName);
    } catch (IOException x) {
      fail(service, "Error locating configuration files", x);
    }
  }
  while ((pending == null) || !pending.hasNext()) {
    if (!configs.hasMoreElements()) {
      return false;
    }
    pending = parse(service, configs.nextElement());
  }
  nextName = pending.next();
  return true;
}


private S nextService() {
  if (!hasNextService())
    throw new NoSuchElementException();
  String cn = nextName;
  nextName = null;
  Class c = null;
  try {
    c = Class.forName(cn, false, loader);
  } catch (ClassNotFoundException x) {
    fail(service,
       "Provider " + cn + " not found");
  }
  if (!service.isAssignableFrom(c)) {
    fail(service,
       "Provider " + cn  + " not a s");
  }
  try {
    S p = service.cast(c.newInstance());
    providers.put(cn, p);
    return p;
  } catch (Throwable x) {
    fail(service,
       "Provider " + cn + " could not be instantiated",
       x);
  }
  throw new Error();          // This cannot happen
}

3.3 SPI 和類加載器

通過上面兩個章節中,走讀 ServiceLoader 代碼,我們已經大致了解 Java SPI 的工作原理,即通過 ClassLoader 加載 SPI 配置文件,解析 SPI 服務,然后通過反射,實例化 SPI 服務實例。我們不妨思考一下,為什么加載 SPI 服務時,需要指定類加載器 ClassLoader 呢?

學習過 JVM 的讀者,想必都了解過類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的 BootstrapClassLoader 外,其余的類加載器都應有自己的父類加載器。這里類加載器之間的父子關系一般通過組合(Composition)關系來實現,而不是通過繼承(Inheritance)的關系實現。

雙親委派機制約定了:一個類加載器首先將類加載請求傳送到父類加載器,只有當父類加載器無法完成類加載請求時才嘗試加載。

雙親委派的好處:使得 Java 類伴隨著它的類加載器,天然具備一種帶有優先級的層次關系,從而使得類加載得到統一,不會出現重復加載的問題:

  • 系統類防止內存中出現多份同樣的字節碼

  • 保證 Java 程序安全穩定運行

例如:java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 的類并放到 classpath 中,程序可以編譯通過。因為雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 優先級更高,因為 rt.jar 中的 Object 使用的是啟動類加載器,而 classpath 中的 Object 使用的是應用程序類加載器。正因為 rt.jar 中的 Object 優先級更高,因為程序中所有的 Object 都是這個 Object。

雙親委派的限制:子類加載器可以使用父類加載器已經加載的類,而父類加載器無法使用子類加載器已經加載的?!@就導致了雙親委派模型并不能解決所有的類加載器問題。Java SPI 就面臨著這樣的問題:

  • SPI 的接口是 Java 核心庫的一部分,是由 BootstrapClassLoader 加載的;

  • 而 SPI 實現的 Java 類一般是由 AppClassLoader 來加載的。BootstrapClassLoader 是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給 AppClassLoader,因為它是最頂層的類加載器。這也解釋了本節開始的問題——為什么加載 SPI 服務時,需要指定類加載器 ClassLoader 呢?因為如果不指定 ClassLoader,則無法獲取 SPI 服務。

如果不做任何的設置,Java 應用的線程的上下文類加載器默認就是 AppClassLoader。在核心類庫使用 SPI 接口時,傳遞的類加載器使用線程上下文類加載器,就可以成功的加載到 SPI 實現的類。線程上下文類加載器在很多 SPI 的實現中都會用到。

通??梢酝ㄟ^

Thread.currentThread().getClassLoader()

Thread.currentThread().getContextClassLoader()獲取線程上下文類加載器。

3.4 Java SPI 的不足

Java SPI 存在一些不足:

  • 不能按需加載,需要遍歷所有的實現,并實例化,然后在循環中才能找到我們需要的實現。如果不想用某些實現類,或者某些類實例化很耗時,它也被載入并實例化了,這就造成了浪費。

  • 獲取某個實現類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據某個參數來獲取對應的實現類。

  • 多個并發多線程使用 ServiceLoader 類的實例是不安全的。

四、SPI 應用場景

SPI 在 Java 開發中應用十分廣泛。首先,在 Java 的 java.util.spi package 中就約定了很多 SPI 接口。下面,列舉一些 SPI 接口:

  • TimeZoneNameProvider:為 TimeZone 類提供本地化的時區名稱。

  • DateFormatProvider:為指定的語言環境提供日期和時間格式。

  • NumberFormatProvider:為 NumberFormat 類提供貨幣、整數和百分比值。

  • Driver:從 4.0 版開始,JDBC API 支持 SPI 模式。舊版本使用 Class.forName() 方法加載驅動程序。

  • PersistenceProvider:提供 JPA API 的實現。

  • 等等

除此以外,SPI 還有很多應用,下面列舉幾個經典案例。

4.1 SPI 應用案例之 JDBC DriverManager

作為 Java 工程師,尤其是 CRUD 工程師,相必都非常熟悉 JDBC。眾所周知,關系型數據庫有很多種,如:MySQL、Oracle、PostgreSQL 等等。JDBC 如何識別各種數據庫的驅動呢?

4.1.1創建數據庫連接

我們先回顧一下,JDBC 如何創建數據庫連接的呢?

JDBC4.0 之前,連接數據庫的時候,通常會用 Class.forName(XXX)方法來加載數據庫相應的驅動,然后再獲取數據庫連接,繼而進行 CRUD 等操作。

Class.forName("com.mysql.jdbc.Driver")

而 JDBC4.0 之后,不再需要用

Class.forName(XXX)方法來加載數據庫驅動,直接獲取連接就可以了。顯然,這種方式很方便,但是如何做到的呢?

(1)JDBC 接口:首先,Java 中內置了接口 java.sql.Driver。

(2)JDBC 接口實現:各個數據庫的驅動自行實現 java.sql.Driver 接口,用于管理數據庫連接。

① MySQL:在 MySQL的 Java 驅動包 mysql-connector-java-XXX.jar 中,可以找到 META-INF/services 目錄,該目錄下會有一個名字為java.sql.Driver 的文件,文件內容是com.mysql.cj.jdbc.Driver。

com.mysql.cj.jdbc.Driver 正是 MySQL 版的 java.sql.Driver 實現。如下圖所示:

d684f7e6-6495-11ed-8abf-dac502259ad0.png

②PostgreSQL 實現:在 PostgreSQL 的 Java 驅動包 postgresql-42.0.0.jar 中,也可以找到同樣的配置文件,文件內容是 org.postgresql.Driver,org.postgresql.Driver 正是 PostgreSQL 版的 java.sql.Driver 實現。

(3)創建數據庫連接

以 MySQL 為例,創建數據庫連接代碼如下:

final String DB_URL = String.format("jdbc//%s:%s/%s", DB_HOST, DB_PORT, DB_SCHEMA);
connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);

4.1.2 DriverManager

從前文,我們已經知道 DriverManager 是創建數據庫連接的關鍵。它究竟是如何工作的呢?

可以看到是加載實例化驅動的,接著看 loadInitialDrivers 方法:

private static void loadInitialDrivers() {
  String drivers;
  try {
    drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
      public String run() {
        return System.getProperty("jdbc.drivers");
      }
    });
  } catch (Exception ex) {
    drivers = null;
  }
  // 通過 classloader 獲取所有實現 java.sql.Driver 的驅動類
  AccessController.doPrivileged(new PrivilegedAction() {
    public Void run() {
            // 利用 SPI,記載所有 Driver 服務
      ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
            // 獲取迭代器
      Iterator driversIterator = loadedDrivers.iterator();
      try{
                // 遍歷迭代器
        while(driversIterator.hasNext()) {
          driversIterator.next();
        }
      } catch(Throwable t) {
      // Do nothing
      }
      return null;
    }
  });


    // 打印數據庫驅動信息
  println("DriverManager.initialize: jdbc.drivers = " + drivers);


  if (drivers == null || drivers.equals("")) {
    return;
  }
  String[] driversList = drivers.split(":");
  println("number of Drivers:" + driversList.length);
  for (String aDriver : driversList) {
    try {
      println("DriverManager.Initialize: loading " + aDriver);
            // 嘗試實例化驅動
      Class.forName(aDriver, true,
          ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
      println("DriverManager.Initialize: load failed: " + ex);
    }
  }
}

上面的代碼主要步驟是:

  1. 從系統變量中獲取驅動的實現類。

  2. 利用 SPI 來獲取所有驅動的實現類。

  3. 遍歷所有驅動,嘗試實例化各個實現類。

  4. 根據第 1 步獲取到的驅動列表來實例化具體的實現類。

需要關注的是下面這行代碼:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

這里實際獲取的是

java.util.ServiceLoader.LazyIterator 迭代器。調用其 hasNext 方法時,會搜索 classpath 下以及 jar 包中的 META-INF/services 目錄,查找 java.sql.Driver 文件,并找到文件中的驅動實現類的全限定名。調用其 next 方法時,會根據驅動類的全限定名去嘗試實例化一個驅動類的對象。

4.2SPI 應用案例之 Common-Loggin

common-logging(也稱 Jakarta Commons Logging,縮寫 JCL)是常用的日志門面工具包。

common-logging 的核心類是入口是 LogFactory,LogFatory 是一個抽象類,它負責加載具體的日志實現。

其入口方法是 LogFactory.getLog 方法,源碼如下:

public static Log getLog(Class clazz) throws LogConfigurationException {
  return getFactory().getInstance(clazz);
}


public static Log getLog(String name) throws LogConfigurationException {
  return getFactory().getInstance(name);
}

從以上源碼可知,getLog 采用了工廠設計模式,是先調用 getFactory 方法獲取具體日志庫的工廠類,然后根據類名稱或類型創建日志實例。

LogFatory.getFactory 方法負責選出匹配的日志工廠,其源碼如下:

public static LogFactory getFactory() throws LogConfigurationException {
  // 省略...


  // 加載 commons-logging.properties 配置文件
  Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);


  // 省略...


    // 決定創建哪個 LogFactory 實例
  // (1)嘗試讀取全局屬性 org.apache.commons.logging.LogFactory
  if (isDiagnosticsEnabled()) {
    logDiagnostic("[LOOKUP] Looking for system property [" + FACTORY_PROPERTY +
            "] to define the LogFactory subclass to use...");
  }


  try {
        // 如果指定了 org.apache.commons.logging.LogFactory 屬性,嘗試實例化具體實現類
    String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
    if (factoryClass != null) {
      if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] Creating an instance of LogFactory class '" + factoryClass +
                "' as specified by system property " + FACTORY_PROPERTY);
      }
      factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
    } else {
      if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] No system property [" + FACTORY_PROPERTY + "] defined.");
      }
    }
  } catch (SecurityException e) {
      // 異常處理
  } catch (RuntimeException e) {
      // 異常處理
  }


    // (2)利用 Java SPI 機制,嘗試在 classpatch 的 META-INF/services 目錄下尋找 org.apache.commons.logging.LogFactory 實現類
  if (factory == null) {
    if (isDiagnosticsEnabled()) {
      logDiagnostic("[LOOKUP] Looking for a resource file of name [" + SERVICE_ID +
              "] to define the LogFactory subclass to use...");
    }
    try {
      final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);


      if( is != null ) {
        // This code is needed by EBCDIC and other strange systems.
        // It's a fix for bugs reported in xerces
        BufferedReader rd;
        try {
          rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        } catch (java.io.UnsupportedEncodingException e) {
          rd = new BufferedReader(new InputStreamReader(is));
        }


        String factoryClassName = rd.readLine();
        rd.close();


        if (factoryClassName != null && ! "".equals(factoryClassName)) {
          if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP]  Creating an instance of LogFactory class " +
                    factoryClassName +
                    " as specified by file '" + SERVICE_ID +
                    "' which was present in the path of the context classloader.");
          }
          factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
        }
      } else {
        // is == null
        if (isDiagnosticsEnabled()) {
          logDiagnostic("[LOOKUP] No resource file with name '" + SERVICE_ID + "' found.");
        }
      }
    } catch (Exception ex) {
      // note: if the specified LogFactory class wasn't compatible with LogFactory
      // for some reason, a ClassCastException will be caught here, and attempts will
      // continue to find a compatible class.
      if (isDiagnosticsEnabled()) {
        logDiagnostic(
          "[LOOKUP] A security exception occurred while trying to create an" +
          " instance of the custom factory class" +
          ": [" + trim(ex.getMessage()) +
          "]. Trying alternative implementations...");
      }
      // ignore
    }
  }


  // (3)嘗試從 classpath 目錄下的 commons-logging.properties 文件中查找 org.apache.commons.logging.LogFactory 屬性


  if (factory == null) {
    if (props != null) {
      if (isDiagnosticsEnabled()) {
        logDiagnostic(
          "[LOOKUP] Looking in properties file for entry with key '" + FACTORY_PROPERTY +
          "' to define the LogFactory subclass to use...");
      }
      String factoryClass = props.getProperty(FACTORY_PROPERTY);
      if (factoryClass != null) {
        if (isDiagnosticsEnabled()) {
          logDiagnostic(
            "[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'");
        }
        factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);


        // TODO: think about whether we need to handle exceptions from newFactory
      } else {
        if (isDiagnosticsEnabled()) {
          logDiagnostic("[LOOKUP] Properties file has no entry specifying LogFactory subclass.");
        }
      }
    } else {
      if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] No properties file available to determine" + " LogFactory subclass from..");
      }
    }
  }


  // (4)以上情況都不滿足,實例化默認實現類 org.apache.commons.logging.impl.LogFactoryImpl


  if (factory == null) {
    if (isDiagnosticsEnabled()) {
      logDiagnostic(
        "[LOOKUP] Loading the default LogFactory implementation '" + FACTORY_DEFAULT +
        "' via the same classloader that loaded this LogFactory" +
        " class (ie not looking in the context classloader).");
    }


    factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
  }


  if (factory != null) {
    /**
     * Always cache using context class loader.
     */
    cacheFactory(contextClassLoader, factory);


    if (props != null) {
      Enumeration names = props.propertyNames();
      while (names.hasMoreElements()) {
        String name = (String) names.nextElement();
        String value = props.getProperty(name);
        factory.setAttribute(name, value);
      }
    }
  }


  return factory;
}

從 getFactory 方法的源碼可以看出,其核心邏輯分為 4 步:

  • 首先,嘗試查找全局屬性

    org.apache.commons.logging.LogFactory,如果指定了具體類,嘗試創建實例。

  • 利用 Java SPI 機制,嘗試在 classpatch 的 META-INF/services 目錄下尋找

    org.apache.commons.logging.LogFactory 的實現類。

  • 嘗試從 classpath 目錄下的 commons-logging.properties 文件中查找

    org.apache.commons.logging.LogFactory 屬性,如果指定了具體類,嘗試創建實例。

  • 以上情況如果都不滿足,則實例化默認實現類,即

    org.apache.commons.logging.impl.LogFactoryImpl。

4.3 SPI 應用案例之 Spring Boot

Spring Boot 是基于 Spring 構建的框架,其設計目的在于簡化 Spring 應用的配置、運行。在 Spring Boot 中,大量運用了自動裝配來盡可能減少配置。

下面是一個 Spring Boot 入口示例,可以看到,代碼非常簡潔。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@SpringBootApplication
@RestController
public class DemoApplication {


    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }


    @GetMapping("/hello")
    public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
        return String.format("Hello %s!", name);
    }
}

那么,Spring Boot 是如何做到寥寥幾行代碼,就可以運行一個 Spring Boot 應用的呢。我們不妨帶著疑問,從源碼入手,一步步探究其原理。

4.3.1 @SpringBootApplication 注解

首先,Spring Boot 應用的啟動類上都會標記一個

@SpringBootApplication 注解。

@SpringBootApplication 注解定義如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // 略
}

除了@Target、@Retention、@Documented、@Inherited 這幾個元注解,

@SpringBootApplication 注解的定義中還標記了@SpringBootConfiguration、

@EnableAutoConfiguration、@ComponentScan 三個注解。

4.3.2 @SpringBootConfiguration 注解

從@SpringBootConfiguration 注解的定義來看,@SpringBootConfiguration 注解本質上就是一個@Configuration 注解,這意味著被@SpringBootConfiguration 注解修飾的類會被 Spring Boot 識別為一個配置類。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

4.3.3 @EnableAutoConfiguration 注解

@EnableAutoConfiguration 注解定義如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";


    Class[] exclude() default {};


    String[] excludeName() default {};
}

@EnableAutoConfiguration 注解包含了@AutoConfigurationPackage

與@Import({AutoConfigurationImportSelector.class})兩個注解。

4.3.4 @AutoConfigurationPackage 注解

@AutoConfigurationPackage 會將被修飾的類作為主配置類,該類所在的 package 會被視為根路徑,Spring Boot 默認會自動掃描根路徑下的所有 Spring Bean(被@Component 以及繼承@Component 的各個注解所修飾的類)?!@就是為什么 Spring Boot 的啟動類一般要置于根路徑的原因。這個功能等同于在 Spring xml 配置中通過 context:component-scan 來指定掃描路徑。@Import 注解的作用是向 Spring 容器中直接注入指定組件。@AutoConfigurationPackage 注解中注明了@Import({Registrar.class})。Registrar 類用于保存 Spring Boot 的入口類、根路徑等信息。

4.3.5 SpringFactoriesLoader.loadFactoryNames 方法

@Import(AutoConfigurationImportSelector.class)表示直接注入

AutoConfigurationImportSelector。

AutoConfigurationImportSelector 有一個核心方法

getCandidateConfigurations 用于獲取候選配置。該方法調用了

SpringFactoriesLoader.loadFactoryNames 方法,這個方法即為 Spring Boot SPI 的關鍵,它負責加載所有 META-INF/spring.factories 文件,加載的過程由 SpringFactoriesLoader 負責。

Spring Boot 的 META-INF/spring.factories 文件本質上就是一個 properties 文件,數據內容就是一個個鍵值對。

SpringFactoriesLoader.loadFactoryNames 方法的關鍵源碼:

// spring.factories 文件的格式為:key=value1,value2,value3
// 遍歷所有 META-INF/spring.factories 文件
// 解析文件,獲得 key=factoryClass 的類名稱
public static List<String> loadFactoryNames(Class factoryType, @Nullable ClassLoader classLoader) {
  String factoryTypeName = factoryType.getName();
  return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}


private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
  // 嘗試獲取緩存,如果緩存中有數據,直接返回
  MultiValueMap<String, String> result = cache.get(classLoader);
  if (result != null) {
    return result;
  }


  try {
    // 獲取資源文件路徑
    Enumeration urls = (classLoader != null ?
        classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
        ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    result = new LinkedMultiValueMap<>();
    // 遍歷所有路徑
    while (urls.hasMoreElements()) {
      URL url = urls.nextElement();
      UrlResource resource = new UrlResource(url);
      // 解析文件,得到對應的一組 Properties
      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
      // 遍歷解析出的 properties,組裝數據
      for (Map.Entry entry : properties.entrySet()) {
        String factoryTypeName = ((String) entry.getKey()).trim();
        for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
          result.add(factoryTypeName, factoryImplementationName.trim());
        }
      }
    }
    cache.put(classLoader, result);
    return result;
  }
  catch (IOException ex) {
    throw new IllegalArgumentException("Unable to load factories from location [" +
        FACTORIES_RESOURCE_LOCATION + "]", ex);
  }
}

歸納上面的方法,主要作了這些事:

加載所有 META-INF/spring.factories 文件,加載過程有 SpringFactoriesLoader 負責。

  • 在 CLASSPATH 中搜尋所有 META-INF/spring.factories 配置文件。

  • 然后,解析 spring.factories 文件,獲取指定自動裝配類的全限定名。

4.3.6 Spring Boot 的 AutoConfiguration 類

Spring Boot 有各種 starter 包,可以根據實際項目需要,按需取材。在項目開發中,只要將 starter 包引入,我們就可以用很少的配置,甚至什么都不配置,即可獲取相關的能力。通過前面的 Spring Boot SPI 流程,只完成了自動裝配工作的一半,剩下的工作如何處理呢 ?

以 spring-boot-starter-web 的 jar 包為例,查看其 maven pom,可以看到,它依賴于 spring-boot-starter,所有 Spring Boot 官方 starter 包都會依賴于這個 jar 包。而 spring-boot-starter 又依賴于 spring-boot-autoconfigure,Spring Boot 的自動裝配秘密,就在于這個 jar 包。

從 spring-boot-autoconfigure 包的結構來看,它有一個 META-INF/spring.factories ,顯然利用了 Spring Boot SPI,來自動裝配其中的配置類。

d6a2b600-6495-11ed-8abf-dac502259ad0.png

下圖是 spring-boot-autoconfigure 的 META-INF/spring.factories 文件的部分內容,可以看到其中注冊了一長串會被自動加載的 AutoConfiguration 類。

d6c7c4a4-6495-11ed-8abf-dac502259ad0.png

以 RedisAutoConfiguration 為例,這個配置類中,會根據@ConditionalXXX 中的條件去決定是否實例化對應的 Bean,實例化 Bean 所依賴的重要參數則通過 RedisProperties 傳入。

d76b383c-6495-11ed-8abf-dac502259ad0.png

RedisProperties 中維護了 Redis 連接所需要的關鍵屬性,只要在 yml 或 properties 配置文件中,指定 spring.redis 開頭的屬性,都會被自動裝載到 RedisProperties 實例中。

d7873b90-6495-11ed-8abf-dac502259ad0.png

通過以上分析,已經一步步解讀出 Spring Boot 自動裝載的原理。

五、SPI 應用案例之 Dubbo

Dubbo 并未使用 Java SPI,而是自己封裝了一套新的 SPI 機制。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內容形式如下:

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

與 Java SPI 實現類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣可以按需加載指定的實現類。Dubbo SPI 除了支持按需加載接口實現類,還增加了 IOC 和 AOP 等特性。

5.1 ExtensionLoader 入口

Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,可以加載指定的實現類。

ExtensionLoader 的 getExtension 方法是其入口方法,其源碼如下:

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 獲取默認的拓展實現類
        return getDefaultExtension();
    }
    // Holder,顧名思義,用于持有目標對象
    Holder holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // 雙重檢查
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 創建拓展實例
                instance = createExtension(name);
                // 設置實例到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}
			

可以看出,這個方法的作用就是:首先檢查緩存,緩存未命中則調用 createExtension 方法創建拓展對象。那么,createExtension 是如何創建拓展對象的呢,其源碼如下:

private T createExtension(String name) {
    // 從配置文件中加載所有的拓展類,可得到“配置項名稱”到“配置類”的映射關系表
    Class clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // 通過反射創建實例
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向實例中注入依賴
        injectExtension(instance);
        Set> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            // 循環創建 Wrapper 實例
            for (Class wrapperClass : wrapperClasses) {
                // 將當前 instance 作為參數傳給 Wrapper 的構造方法,并通過反射創建 Wrapper 實例。
                // 然后向 Wrapper 實例中注入依賴,最后將 Wrapper 實例再次賦值給 instance 變量
                instance = injectExtension(
                    (T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}

createExtension 方法的的工作步驟可以歸納為:

  1. 通過getExtensionClasses獲取所有的拓展類

  2. 通過反射創建拓展對象

  3. 向拓展對象中注入依賴

  4. 將拓展對象包裹在相應的Wrapper對象中

以上步驟中,第一個步驟是加載拓展類的關鍵,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實現。

5.2獲取所有的拓展類

Dubbo 在通過名稱獲取拓展類之前,首先需要根據配置文件解析出拓展項名稱到拓展類的映射關系表(Map<名稱, 拓展類>),之后再根據拓展項名稱從映射關系表中取出相應的拓展類即可。相關過程的代碼分析如下:

private Map> getExtensionClasses() {
    // 從緩存中獲取已加載的拓展類
    Map> classes = cachedClasses.get();
    // 雙重檢查
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加載拓展類
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

這里也是先檢查緩存,若緩存未命中,則通過 synchronized 加鎖。加鎖后再次檢查緩存,并判空。此時如果 classes 仍為 null,則通過 loadExtensionClasses 加載拓展類。下面分析 loadExtensionClasses 方法的邏輯。

private Map<String, Class> loadExtensionClasses() {
    // 獲取 SPI 注解,這里的 type 變量是在調用 getExtensionLoader 方法時傳入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // 對 SPI 注解內容進行切分
            String[] names = NAME_SEPARATOR.split(value);
            // 檢測 SPI 注解內容是否合法,不合法則拋出異常
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension...");
            }


            // 設置默認名稱,參考 getDefaultExtension 方法
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }


    Map<String, Class> extensionClasses = new HashMap<String, Class>();
    // 加載指定文件夾下的配置文件
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

loadExtensionClasses 方法總共做了兩件事情,一是對 SPI 注解進行解析,二是調用 loadDirectory 方法加載指定文件夾配置文件。SPI 注解解析過程比較簡單,無需多說。下面我們來看一下 loadDirectory 做了哪些事情。

private void loadDirectory(Map<String, Class> extensionClasses, String dir) {
    // fileName = 文件夾路徑 + type 全限定名
    String fileName = dir + type.getName();
    try {
        Enumeration urls;
        ClassLoader classLoader = findClassLoader();
        // 根據文件名加載所有的同名文件
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        if (urls != null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                // 加載資源
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("...");
    }
}

loadDirectory 方法先通過 classLoader 獲取所有資源鏈接,然后再通過 loadResource 方法加載資源。我們繼續跟下去,看一下 loadResource 方法的實現。

private void loadResource(Map> extensionClasses,
  ClassLoader classLoader, java.net.URL resourceURL) {
    try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(resourceURL.openStream(), "utf-8"));
        try {
            String line;
            // 按行讀取配置內容
            while ((line = reader.readLine()) != null) {
                // 定位 # 字符
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    // 截取 # 之前的字符串,# 之后的內容為注釋,需要忽略
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        if (i > 0) {
                            // 以等于號 = 為界,截取鍵與值
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // 加載類,并通過 loadClass 方法對類進行緩存
                            loadClass(extensionClasses, resourceURL,
                                      Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class...");
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class...");
    }
}

loadResource 方法用于讀取和解析配置文件,并通過反射加載類,最后調用 loadClass 方法進行其他操作。loadClass 方法用于主要用于操作緩存,該方法的邏輯如下:

private void loadClass(Map<String, Class> extensionClasses, java.net.URL resourceURL,
    Class clazz, String name) throws NoSuchMethodException {


    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("...");
    }


    // 檢測目標類上是否有 Adaptive 注解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            // 設置 cachedAdaptiveClass緩存
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("...");
        }


    // 檢測 clazz 是否是 Wrapper 類型
    } else if (isWrapperClass(clazz)) {
        Set> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet>();
            wrappers = cachedWrapperClasses;
        }
        // 存儲 clazz 到 cachedWrapperClasses 緩存中
        wrappers.add(clazz);


    // 程序進入此分支,表明 clazz 是一個普通的拓展類
    } else {
        // 檢測 clazz 是否有默認的構造方法,如果沒有,則拋出異常
        clazz.getConstructor();
        if (name == null || name.length() == 0) {
            // 如果 name 為空,則嘗試從 Extension 注解中獲取 name,或使用小寫的類名作為 name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("...");
            }
        }
        // 切分 name
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                // 如果類上有 Activate 注解,則使用 names 數組的第一個元素作為鍵,
                // 存儲 name 到 Activate 注解對象的映射關系
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    // 存儲 Class 到名稱的映射關系
                    cachedNames.put(clazz, n);
                }
                Class c = extensionClasses.get(n);
                if (c == null) {
                    // 存儲名稱到 Class 的映射關系
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    throw new IllegalStateException("...");
                }
            }
        }
    }
}

如上,loadClass 方法操作了不同的緩存,比如 cachedAdaptiveClass、

cachedWrapperClasses 和 cachedNames 等等。除此之外,該方法沒有其他什么邏輯了。

參考資料

  • Java SPI 思想梳理

  • Dubbo SPI

  • springboot 中 SPI 機制

  • SpringBoot 的自動裝配原理、自定義 starter 與 spi 機制,一網打盡

審核編輯 :李倩


聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • JAVA
    +關注

    關注

    20

    文章

    2984

    瀏覽量

    106849
  • SPI
    SPI
    +關注

    關注

    17

    文章

    1779

    瀏覽量

    94679

原文標題:源碼級深度理解Java SPI

文章出處:【微信號:OSC開源社區,微信公眾號:OSC開源社區】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦
    熱點推薦

    JavaSPI機制詳解

    作者:京東物流 楊葦葦 1.SPI簡介 SPI(Service Provicer Interface)是Java語言提供的一種接口發現機制,用來實現接口和接口實現的解耦。簡單來說,就是系統只需要定義
    的頭像 發表于 03-05 11:35 ?670次閱讀
    <b class='flag-5'>Java</b>的<b class='flag-5'>SPI</b>機制詳解

    華為云 Flexus X 實例下的場景體驗——小企業必備——JAVA 環境搭建——保姆教學

    前言 上次我們使用的是 Ubuntu 來操作的,這里跑的服務器多的還是 Huawei Cloud EulerOS,所以我們還原到基礎鏡像上做環境架設,此次我們來架設 java 的基礎運行環境,是能
    的頭像 發表于 01-07 17:05 ?424次閱讀
    華為云 Flexus X 實例下的場景體驗——小企業必備——<b class='flag-5'>JAVA</b> 環境搭建——保姆<b class='flag-5'>級</b>教學

    請問DAC5682z內部FIFO深度為多少,8SAMPLE具體怎么理解?

    你好,請問DAC5682z內部FIFO深度為多少,8SAMPLE具體怎么理解。 另外,DAC5682zEVM是否可以直接通過TI的ADC-HSMC板卡與ALTERA的FPGA開發相連(FPGA板HSMC接口與電壓都匹配條件下)。 謝謝
    發表于 01-03 07:27

    校園點餐訂餐外賣跑腿Java源碼

    創建一個校園點餐訂餐外賣跑腿系統是一個復雜的項目,涉及到前端、后端、數據庫設計等多個方面。在這里,我可以提供一個簡化的Java后端示例,使用Spring Boot框架來搭建一個基本的API服務。這個
    的頭像 發表于 12-24 14:55 ?515次閱讀
    校園點餐訂餐外賣跑腿<b class='flag-5'>Java</b><b class='flag-5'>源碼</b>

    SSM框架的源碼解析與理解

    SSM框架(Spring + Spring MVC + MyBatis)是一種在Java開發中常用的輕量級企業應用框架。它通過整合Spring、Spring MVC和MyBatis三個框架,實現了
    的頭像 發表于 12-17 09:20 ?844次閱讀

    Java 23功能介紹

    Java 23 包含全新和更新的 Java 語言功能、核心 API 以及 JVM,同時適合新的 Java 開發者和高級開發者。從?IntelliJ IDEA 2024.2?開始已支持 Java
    的頭像 發表于 12-04 10:02 ?838次閱讀
    <b class='flag-5'>Java</b> 23功能介紹

    FPGA做深度學習能走多遠?

    ,共同進步。 歡迎加入FPGA技術微信交流群14群! 交流問題(一) Q:FPGA做深度學習能走多遠?現在用FPGA做深度學習加速成為一個熱門,深鑒科技,商湯,曠視科技等都有基于FPGA做深度學習的項目
    發表于 09-27 20:53

    航天100krad隔離式串行外設接口(SPI)LVDS電路

    電子發燒友網站提供《航天100krad隔離式串行外設接口(SPI)LVDS電路.pdf》資料免費下載
    發表于 09-20 10:54 ?3次下載
    航天<b class='flag-5'>級</b>100krad隔離式串行外設接口(<b class='flag-5'>SPI</b>)LVDS電路

    航天100krad隔離式串行外設接口(SPI)RS-422電路

    電子發燒友網站提供《航天100krad隔離式串行外設接口(SPI)RS-422電路.pdf》資料免費下載
    發表于 09-19 13:15 ?2次下載
    航天<b class='flag-5'>級</b>100krad隔離式串行外設接口(<b class='flag-5'>SPI</b>)RS-422電路

    java反編譯能拿到源碼

    Java反編譯是一種將編譯后的Java字節碼(.class文件)轉換回Java源代碼的過程。雖然反編譯可以幫助理解代碼的邏輯和結構,但它并不總是能完美地還原原始源代碼。反編譯工具通常會
    的頭像 發表于 09-02 11:03 ?1657次閱讀

    華納云:java web和java有什么區別java web和java有什么區別

    的平臺,Java可以用于開發桌面應用程序、移動應用程序、企業應用程序等。 – Java Web是Java語言在Web開發領域的應用,它使用Java
    的頭像 發表于 07-16 13:35 ?1350次閱讀
    華納云:<b class='flag-5'>java</b> web和<b class='flag-5'>java</b>有什么區別<b class='flag-5'>java</b> web和<b class='flag-5'>java</b>有什么區別

    如何用java語言開發一套數字化產科系統? 數字化產科管理平臺源碼

    如何用java語言開發一套數字化產科系統 數字化產科管理平臺源碼
    的頭像 發表于 07-06 09:38 ?1229次閱讀
    如何用<b class='flag-5'>java</b>語言開發一套數字化產科系統? 數字化產科管理平臺<b class='flag-5'>源碼</b>

    Java語言、idea開發工具、MYSQL數據庫開發的UWB定位技術系統源碼

    Java語言+?idea開發工具+?MYSQL?數據庫開發的 UWB定位技術系統源碼 實現人員/設備/車輛實時軌跡定位 UWB高精度人員定位系統提供實時定位、電子圍欄、軌跡回放等基礎功能以及各種拓展
    的頭像 發表于 06-24 09:33 ?680次閱讀
    <b class='flag-5'>Java</b>語言、idea開發工具、MYSQL數據庫開發的UWB定位技術系統<b class='flag-5'>源碼</b>

    Java 智慧工地監管平臺源碼 依托智慧工地平臺,滿足省、市級住建數據監管要求

    本文主要介紹了基于智慧工地平臺的Java智慧工地監管平臺源碼,通過結合物聯網、大數據、互聯網、云計算等技術,視頻監控管理、危大工程管理、綠色施工管理等多個功能。
    的頭像 發表于 06-18 15:35 ?853次閱讀
    <b class='flag-5'>Java</b> 智慧工地監管平臺<b class='flag-5'>源碼</b> 依托智慧工地平臺,滿足省、市級住建數據監管要求

    基于java+單體服務 +?硬件(UWB定位基站、卡牌)技術架構開發的UWB室內定位系統源碼

    基于java+單體服務 + 硬件(UWB定位基站、卡牌)技術架構開發的UWB室內定位系統源碼 UWB定位技術 超寬帶定位 高精度定位系統源碼
    的頭像 發表于 06-13 09:35 ?699次閱讀
    基于<b class='flag-5'>java</b>+單體服務 +?硬件(UWB定位基站、卡牌)技術架構開發的UWB室內定位系統<b class='flag-5'>源碼</b>