色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

源碼級深度理解Java SPI

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

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

一、SPI 簡介

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

Java SPI 有四個要素:

  • SPI 接口為服務(wù)提供者實現(xiàn)類約定的的接口或抽象類。

  • SPI 實現(xiàn)類:實際提供服務(wù)的實現(xiàn)類。

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

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

二、SPI 示例

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

2.1 SPI 接口

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

package io.github.dunwu.javacore.spi;


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

2.2 SPI 實現(xiàn)類

假設(shè),我們需要在程序中使用兩種不同的數(shù)據(jù)存儲——MySQL 和 Redis。因此,我們需要兩個不同的實現(xiàn)類去分別完成相應(yīng)工作。

MySQL查詢 MOCK 類

package io.github.dunwu.javacore.spi;


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

Redis 查詢 MOCK 類

package io.github.dunwu.javacore.spi;


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

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

2.3 SPI 配置

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

io.github.dunwu.javacore.spi.DataStorage

文件中的內(nèi)容如下:

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

2.4 ServiceLoader

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

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,結(jié)果:No
【Redis】搜索Yes Or No,結(jié)果:Yes

三、SPI 原理

上文中,我們已經(jīng)了解 Java SPI 的要素以及使用 Java SPI 的方法。你有沒有想過,Java SPI 和普通 Java 接口有何不同,Java SPI 是如何工作的。實際上,Java SPI 機制依賴于 ServiceLoader 類去解析、加載服務(wù)。因此,掌握了 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 服務(wù)
    private final Class service;


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


    // ServiceLoader 創(chuàng)建時的訪問控制上下文
    private final AccessControlContext acc;


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


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


    // ...
}

3.2 ServiceLoader 的工作流程

(1)ServiceLoader.load靜態(tài)方法

應(yīng)用程序加載 Java SPI 服務(wù),都是先調(diào)用 ServiceLoader.load 靜態(tài)方法。

ServiceLoader.load 靜態(tài)方法的作用是:

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

② 然后,重新加載 SPI 服務(wù)

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

  • 根據(jù)ClassLoader和 SPI 類型,創(chuàng)建懶加載迭代器

這里,摘錄 ServiceLoader.load 相關(guān)源碼,如下:

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


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


// 私有構(gòu)造方法
// 重新加載 SPI 服務(wù)
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 服務(wù)
  reload();
}

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

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

當(dāng)應(yīng)用程序調(diào)用 ServiceLoader 的 iterator 方法時,ServiceLoader 會先判斷緩存 providers 中是否有數(shù)據(jù):如果有,則直接返回緩存 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關(guān)鍵代碼

hasNextService 方法:

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

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

  • 解析資源文件中的內(nèi)容,獲取 SPI 接口的實現(xiàn)類的全限定名nextName

nextService 方法:

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

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

private boolean hasNextService() {
  if (nextName != null) {
    return true;
  }
  if (configs == null) {
    try {
            // 1.拼接 META-INF/services/ + SPI 接口全限定名
            // 2.通過類加載器,嘗試加載資源文件
            // 3.解析資源文件中的內(nèi)容
      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 和類加載器

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

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

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

雙親委派的好處:使得 Java 類伴隨著它的類加載器,天然具備一種帶有優(yōu)先級的層次關(guān)系,從而使得類加載得到統(tǒng)一,不會出現(xiàn)重復(fù)加載的問題:

  • 系統(tǒng)類防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼

  • 保證 Java 程序安全穩(wěn)定運行

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

雙親委派的限制:子類加載器可以使用父類加載器已經(jīng)加載的類,而父類加載器無法使用子類加載器已經(jīng)加載的。——這就導(dǎo)致了雙親委派模型并不能解決所有的類加載器問題。Java SPI 就面臨著這樣的問題:

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

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

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

通常可以通過

Thread.currentThread().getClassLoader()

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

3.4 Java SPI 的不足

Java SPI 存在一些不足:

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

  • 獲取某個實現(xiàn)類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據(jù)某個參數(shù)來獲取對應(yīng)的實現(xiàn)類。

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

四、SPI 應(yīng)用場景

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

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

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

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

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

  • PersistenceProvider:提供 JPA API 的實現(xiàn)。

  • 等等

除此以外,SPI 還有很多應(yīng)用,下面列舉幾個經(jīng)典案例。

4.1 SPI 應(yīng)用案例之 JDBC DriverManager

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

4.1.1創(chuàng)建數(shù)據(jù)庫連接

我們先回顧一下,JDBC 如何創(chuàng)建數(shù)據(jù)庫連接的呢?

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

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

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

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

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

(2)JDBC 接口實現(xiàn):各個數(shù)據(jù)庫的驅(qū)動自行實現(xiàn) java.sql.Driver 接口,用于管理數(shù)據(jù)庫連接。

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

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

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

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

(3)創(chuàng)建數(shù)據(jù)庫連接

以 MySQL 為例,創(chuàng)建數(shù)據(jù)庫連接代碼如下:

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

從前文,我們已經(jīng)知道 DriverManager 是創(chuàng)建數(shù)據(jù)庫連接的關(guān)鍵。它究竟是如何工作的呢?

可以看到是加載實例化驅(qū)動的,接著看 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 獲取所有實現(xiàn) java.sql.Driver 的驅(qū)動類
  AccessController.doPrivileged(new PrivilegedAction() {
    public Void run() {
            // 利用 SPI,記載所有 Driver 服務(wù)
      ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
            // 獲取迭代器
      Iterator driversIterator = loadedDrivers.iterator();
      try{
                // 遍歷迭代器
        while(driversIterator.hasNext()) {
          driversIterator.next();
        }
      } catch(Throwable t) {
      // Do nothing
      }
      return null;
    }
  });


    // 打印數(shù)據(jù)庫驅(qū)動信息
  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);
            // 嘗試實例化驅(qū)動
      Class.forName(aDriver, true,
          ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
      println("DriverManager.Initialize: load failed: " + ex);
    }
  }
}

上面的代碼主要步驟是:

  1. 從系統(tǒng)變量中獲取驅(qū)動的實現(xiàn)類。

  2. 利用 SPI 來獲取所有驅(qū)動的實現(xiàn)類。

  3. 遍歷所有驅(qū)動,嘗試實例化各個實現(xiàn)類。

  4. 根據(jù)第 1 步獲取到的驅(qū)動列表來實例化具體的實現(xiàn)類。

需要關(guān)注的是下面這行代碼:

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

這里實際獲取的是

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

4.2SPI 應(yīng)用案例之 Common-Loggin

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

common-logging 的核心類是入口是 LogFactory,LogFatory 是一個抽象類,它負(fù)責(zé)加載具體的日志實現(xiàn)。

其入口方法是 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 采用了工廠設(shè)計模式,是先調(diào)用 getFactory 方法獲取具體日志庫的工廠類,然后根據(jù)類名稱或類型創(chuàng)建日志實例。

LogFatory.getFactory 方法負(fù)責(zé)選出匹配的日志工廠,其源碼如下:

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


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


  // 省略...


    // 決定創(chuàng)建哪個 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 屬性,嘗試實例化具體實現(xiàn)類
    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 實現(xiàn)類
  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)以上情況都不滿足,實例化默認(rèn)實現(xiàn)類 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,如果指定了具體類,嘗試創(chuàng)建實例。

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

    org.apache.commons.logging.LogFactory 的實現(xiàn)類。

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

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

  • 以上情況如果都不滿足,則實例化默認(rèn)實現(xiàn)類,即

    org.apache.commons.logging.impl.LogFactoryImpl

4.3 SPI 應(yīng)用案例之 Spring Boot

Spring Boot 是基于 Spring 構(gòu)建的框架,其設(shè)計目的在于簡化 Spring 應(yīng)用的配置、運行。在 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 應(yīng)用的呢。我們不妨帶著疑問,從源碼入手,一步步探究其原理。

4.3.1 @SpringBootApplication 注解

首先,Spring Boot 應(yīng)用的啟動類上都會標(biāo)記一個

@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 注解的定義中還標(biāo)記了@SpringBootConfiguration、

@EnableAutoConfiguration、@ComponentScan 三個注解。

4.3.2 @SpringBootConfiguration 注解

從@SpringBootConfiguration 注解的定義來看,@SpringBootConfiguration 注解本質(zhì)上就是一個@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 默認(rèn)會自動掃描根路徑下的所有 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 用于獲取候選配置。該方法調(diào)用了

SpringFactoriesLoader.loadFactoryNames 方法,這個方法即為 Spring Boot SPI 的關(guān)鍵,它負(fù)責(zé)加載所有 META-INF/spring.factories 文件,加載的過程由 SpringFactoriesLoader 負(fù)責(zé)。

Spring Boot 的 META-INF/spring.factories 文件本質(zhì)上就是一個 properties 文件,數(shù)據(jù)內(nèi)容就是一個個鍵值對。

SpringFactoriesLoader.loadFactoryNames 方法的關(guān)鍵源碼:

// 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) {
  // 嘗試獲取緩存,如果緩存中有數(shù)據(jù),直接返回
  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);
      // 解析文件,得到對應(yīng)的一組 Properties
      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
      // 遍歷解析出的 properties,組裝數(shù)據(jù)
      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 負(fù)責(zé)。

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

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

4.3.6 Spring Boot 的 AutoConfiguration 類

Spring Boot 有各種 starter 包,可以根據(jù)實際項目需要,按需取材。在項目開發(fā)中,只要將 starter 包引入,我們就可以用很少的配置,甚至什么都不配置,即可獲取相關(guān)的能力。通過前面的 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 包的結(jié)構(gòu)來看,它有一個 META-INF/spring.factories ,顯然利用了 Spring Boot SPI,來自動裝配其中的配置類。

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

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

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

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

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

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

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

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

五、SPI 應(yīng)用案例之 Dubbo

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

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

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

5.1 ExtensionLoader 入口

Dubbo SPI 的相關(guān)邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,可以加載指定的實現(xiàn)類。

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

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 獲取默認(rèn)的拓展實現(xiàn)類
        return getDefaultExtension();
    }
    // Holder,顧名思義,用于持有目標(biāo)對象
    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) {
                // 創(chuàng)建拓展實例
                instance = createExtension(name);
                // 設(shè)置實例到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}
			

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

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

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

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

  2. 通過反射創(chuàng)建拓展對象

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

  4. 將拓展對象包裹在相應(yīng)的Wrapper對象中

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

5.2獲取所有的拓展類

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

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 變量是在調(diào)用 getExtensionLoader 方法時傳入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // 對 SPI 注解內(nèi)容進行切分
            String[] names = NAME_SEPARATOR.split(value);
            // 檢測 SPI 注解內(nèi)容是否合法,不合法則拋出異常
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension...");
            }


            // 設(shè)置默認(rèn)名稱,參考 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 注解進行解析,二是調(diào)用 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();
        // 根據(jù)文件名加載所有的同名文件
        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 方法加載資源。我們繼續(xù)跟下去,看一下 loadResource 方法的實現(xiàn)。

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;
            // 按行讀取配置內(nèi)容
            while ((line = reader.readLine()) != null) {
                // 定位 # 字符
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    // 截取 # 之前的字符串,# 之后的內(nèi)容為注釋,需要忽略
                    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 方法用于讀取和解析配置文件,并通過反射加載類,最后調(diào)用 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("...");
    }


    // 檢測目標(biāo)類上是否有 Adaptive 注解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            // 設(shè)置 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 是否有默認(rèn)的構(gòu)造方法,如果沒有,則拋出異常
        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 數(shù)組的第一個元素作為鍵,
                // 存儲 name 到 Activate 注解對象的映射關(guān)系
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    // 存儲 Class 到名稱的映射關(guān)系
                    cachedNames.put(clazz, n);
                }
                Class c = extensionClasses.get(n);
                if (c == null) {
                    // 存儲名稱到 Class 的映射關(guān)系
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    throw new IllegalStateException("...");
                }
            }
        }
    }
}

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

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

參考資料

  • Java SPI 思想梳理

  • Dubbo SPI

  • springboot 中 SPI 機制

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

審核編輯 :李倩


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • JAVA
    +關(guān)注

    關(guān)注

    19

    文章

    2970

    瀏覽量

    104810
  • SPI
    SPI
    +關(guān)注

    關(guān)注

    17

    文章

    1707

    瀏覽量

    91670

原文標(biāo)題:源碼級深度理解Java SPI

文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    請問DAC5682z內(nèi)部FIFO深度為多少,8SAMPLE具體怎么理解

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

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

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

    SSM框架的源碼解析與理解

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

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

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

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

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

    java反編譯能拿到源碼

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

    華納云:java web和java有什么區(qū)別java web和java有什么區(qū)別

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

    如何用java語言開發(fā)一套數(shù)字化產(chǎn)科系統(tǒng)? 數(shù)字化產(chǎn)科管理平臺源碼

    如何用java語言開發(fā)一套數(shù)字化產(chǎn)科系統(tǒng) 數(shù)字化產(chǎn)科管理平臺源碼
    的頭像 發(fā)表于 07-06 09:38 ?1023次閱讀
    如何用<b class='flag-5'>java</b>語言開發(fā)一套數(shù)字化產(chǎn)科系統(tǒng)? 數(shù)字化產(chǎn)科管理平臺<b class='flag-5'>源碼</b>

    Java語言、idea開發(fā)工具、MYSQL數(shù)據(jù)庫開發(fā)的UWB定位技術(shù)系統(tǒng)源碼

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

    Java 智慧工地監(jiān)管平臺源碼 依托智慧工地平臺,滿足省、市級住建數(shù)據(jù)監(jiān)管要求

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

    基于java+單體服務(wù) +?硬件(UWB定位基站、卡牌)技術(shù)架構(gòu)開發(fā)的UWB室內(nèi)定位系統(tǒng)源碼

    基于java+單體服務(wù) + 硬件(UWB定位基站、卡牌)技術(shù)架構(gòu)開發(fā)的UWB室內(nèi)定位系統(tǒng)源碼 UWB定位技術(shù) 超寬帶定位 高精度定位系統(tǒng)源碼
    的頭像 發(fā)表于 06-13 09:35 ?464次閱讀
    基于<b class='flag-5'>java</b>+單體服務(wù) +?硬件(UWB定位基站、卡牌)技術(shù)架構(gòu)開發(fā)的UWB室內(nèi)定位系統(tǒng)<b class='flag-5'>源碼</b>

    佰維存儲推出自研工規(guī)寬溫SPI NOR Flash產(chǎn)品—TGN298系列

    近日,佰維存儲(股票代碼:688525)推出了自研工規(guī)寬溫SPI NOR Flash產(chǎn)品——TGN298系列。
    的頭像 發(fā)表于 05-16 10:09 ?474次閱讀
    佰維存儲推出自研工規(guī)<b class='flag-5'>級</b>寬溫<b class='flag-5'>SPI</b> NOR Flash產(chǎn)品—TGN298系列

    【外設(shè)移植】Ai-M61-32s 開發(fā)板+3.5寸SPI彩屏

    是: lcd_init.c LCD初始化函數(shù)相關(guān)源碼文件 lcd_init.h LCD初始化函數(shù)頭文件 lcd.c LCD顯示相關(guān)源碼文件 lcd.h LCD顯示相關(guān)頭文件 lcdfont.h 字體文件 pic.h 顯示圖片相關(guān) USER_
    的頭像 發(fā)表于 03-07 10:02 ?501次閱讀
    【外設(shè)移植】Ai-M61-32s 開發(fā)板+3.5寸<b class='flag-5'>SPI</b>彩屏

    Apache Doris聚合函數(shù)源碼解析

    筆者最近由于工作需要開始調(diào)研 Apache Doris,通過閱讀聚合函數(shù)代碼切入 Apache Doris 內(nèi)核,同時也秉承著開源的精神,開發(fā)了 array_agg 函數(shù)并貢獻(xiàn)給社區(qū)。筆者通過這篇文章記錄下對源碼的一些理解,同時也方便后面的新人更快速地上手
    的頭像 發(fā)表于 01-16 09:52 ?1054次閱讀
    Apache Doris聚合函數(shù)<b class='flag-5'>源碼</b>解析

    OneFlow Softmax算子源碼解讀之WarpSoftmax

    寫在前面:近來筆者偶然間接觸了一個深度學(xué)習(xí)框架 OneFlow,所以這段時間主要在閱讀 OneFlow 框架的 cuda 源碼。官方源碼基于不同場景分三種方式實現(xiàn) Softmax,本文主要介紹其中一種的實現(xiàn)過程,即 Warp 級
    的頭像 發(fā)表于 01-08 09:24 ?874次閱讀
    OneFlow Softmax算子<b class='flag-5'>源碼</b>解讀之WarpSoftmax
    主站蜘蛛池模板: 国产盗摄一区二区三区| 午夜熟女插插XX免费视频| 色哟哟tv| 亚洲午夜精品一区二区公牛电影院 | 亚洲精品在线免费| 99亚偷拍自图区亚洲| 国内精品久久久久影院网站| 欧美性喷潮xxxx| 在线日韩欧美一区二区三区| 国产高清美女一级a毛片久久w| 毛片免费在线播放| 亚洲国产精品免费线观看视频| www黄色com| 久久这里只有精品1| 喜马拉雅听书免费版| GOGOGO高清免费播放| 久久免费国产| 亚洲AV无码专区国产精品99| younv 学生国产在线视频| 精品一区二区三区四区五区六区 | 伊人大香人妻在线播放| 国产99久久九九免费精品无码| 蜜桃狠狠色伊人亚洲综合网站| 亚洲黄色高清视频| 国产成人国产在线观看入口| 欧美黑人巨大videos免费| 一区二区中文字幕在线观看 | 色欲AV精品人妻一二三区| 91九色网址| 久久aa毛片免费播放嗯啊| 先锋影音av无码第1页| 成人免费在线视频| 内射老妇BBX| 中文视频在线| 久艾草在线精品视频在线观看| 校园女教师之禁区| 动漫美女禁区图| 青青草 久久久| 99在线观看精品| 美女强奷到抽搐在线播放| 用快播看av的网站|