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

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

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

源碼級(jí)深度理解Java SPI

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

SPI 是一種用于動(dòng)態(tài)加載服務(wù)的機(jī)制。它的核心思想就是解耦,屬于典型的微內(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 提供的,旨在由第三方實(shí)現(xiàn)或擴(kuò)展的 API,它是一種用于動(dòng)態(tài)加載服務(wù)的機(jī)制。Java 中 SPI 機(jī)制主要思想是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計(jì)中這個(gè)機(jī)制尤其重要,其核心思想就是解耦

Java SPI 有四個(gè)要素:

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

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

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

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

二、SPI 示例

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

2.1 SPI 接口

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

package io.github.dunwu.javacore.spi;


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

2.2 SPI 實(shí)現(xiàn)類

假設(shè),我們需要在程序中使用兩種不同的數(shù)據(jù)存儲(chǔ)——MySQL 和 Redis。因此,我們需要兩個(gè)不同的實(shí)現(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 接口類型 到目前為止,定義接口,并實(shí)現(xiàn)接口和普通的 Java 接口實(shí)現(xiàn)沒有任何不同。

2.3 SPI 配置

如果想通過 Java SPI 機(jī)制來發(fā)現(xiàn)服務(wù),就需要在 SPI 配置中約定好發(fā)現(xiàn)服務(wù)的邏輯。配置文件必須置于 META-INF/services 目錄中,并且,文件名應(yīng)與服務(wù)提供者接口的完全限定名保持一致。文件中的每一行都有一個(gè)實(shí)現(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 是如何工作的。實(shí)際上,Java SPI 機(jī)制依賴于 ServiceLoader 類去解析、加載服務(wù)。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。ServiceLoader 的代碼本身很精練,接下來,讓我們通過走讀源碼的方式,逐一理解 ServiceLoader 的工作流程。

3.1 ServiceLoader 的成員變量

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

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)建時(shí)的訪問控制上下文
    private final AccessControlContext acc;


    // SPI 服務(wù)緩存,按實(shí)例化的順序排列
    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ù)

  • 清空緩存中所有已實(shí)例化的 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() {
    // 清空緩存中所有已實(shí)例化的 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 實(shí)例

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

當(dāng)應(yīng)用程序調(diào)用 ServiceLoader 的 iterator 方法時(shí),ServiceLoader 會(huì)先判斷緩存 providers 中是否有數(shù)據(jù):如果有,則直接返回緩存 providers 的迭代器;如果沒有,則返回懶加載迭代器的迭代器。

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


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


        // lookupIterator 是 LazyIterator 實(shí)例,用于懶加載 SPI 實(shí)例
    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 實(shí)例,而 LazyIterator 用于懶加載 SPI 實(shí)例。那么, LazyIterator 是如何工作的呢?

這里,摘取LazyIterator關(guān)鍵代碼

hasNextService 方法:

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

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

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

nextService 方法:

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

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

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 和類加載器

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

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

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

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

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

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

例如:java.lang.Object 存放在 rt.jar 中,如果編寫另外一個(gè) java.lang.Object 的類并放到 classpath 中,程序可以編譯通過。因?yàn)殡p親委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 優(yōu)先級(jí)更高,因?yàn)?rt.jar 中的 Object 使用的是啟動(dòng)類加載器,而 classpath 中的 Object 使用的是應(yīng)用程序類加載器。正因?yàn)?rt.jar 中的 Object 優(yōu)先級(jí)更高,因?yàn)槌绦蛑兴械?Object 都是這個(gè) Object。

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

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

  • 而 SPI 實(shí)現(xiàn)的 Java 類一般是由 AppClassLoader 來加載的。BootstrapClassLoader 是無法找到 SPI 的實(shí)現(xiàn)類的,因?yàn)樗患虞d Java 的核心庫。它也不能代理給 AppClassLoader,因?yàn)樗亲铐攲拥念惣虞d器。這也解釋了本節(jié)開始的問題——為什么加載 SPI 服務(wù)時(shí),需要指定類加載器 ClassLoader 呢?因?yàn)槿绻恢付?ClassLoader,則無法獲取 SPI 服務(wù)。

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

通??梢酝ㄟ^

Thread.currentThread().getClassLoader()

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

3.4 Java SPI 的不足

Java SPI 存在一些不足:

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

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

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

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

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

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

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

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

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

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

  • 等等

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

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

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

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

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

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

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

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

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

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

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

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

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

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

②PostgreSQL 實(shí)現(xiàn):在 PostgreSQL 的 Java 驅(qū)動(dòng)包 postgresql-42.0.0.jar 中,也可以找到同樣的配置文件,文件內(nèi)容是 org.postgresql.Driver,org.postgresql.Driver 正是 PostgreSQL 版的 java.sql.Driver 實(shí)現(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)鍵。它究竟是如何工作的呢?

可以看到是加載實(shí)例化驅(qū)動(dòng)的,接著看 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 獲取所有實(shí)現(xiàn) java.sql.Driver 的驅(qū)動(dòng)類
  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ū)動(dòng)信息
  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);
            // 嘗試實(shí)例化驅(qū)動(dòng)
      Class.forName(aDriver, true,
          ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
      println("DriverManager.Initialize: load failed: " + ex);
    }
  }
}

上面的代碼主要步驟是:

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

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

  3. 遍歷所有驅(qū)動(dòng),嘗試實(shí)例化各個(gè)實(shí)現(xiàn)類。

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

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

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

這里實(shí)際獲取的是

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

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

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

common-logging 的核心類是入口是 LogFactory,LogFatory 是一個(gè)抽象類,它負(fù)責(zé)加載具體的日志實(shí)現(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è)計(jì)模式,是先調(diào)用 getFactory 方法獲取具體日志庫的工廠類,然后根據(jù)類名稱或類型創(chuàng)建日志實(shí)例。

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

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


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


  // 省略...


    // 決定創(chuàng)建哪個(gè) LogFactory 實(shí)例
  // (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 屬性,嘗試實(shí)例化具體實(shí)現(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 機(jī)制,嘗試在 classpatch 的 META-INF/services 目錄下尋找 org.apache.commons.logging.LogFactory 實(shí)現(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)以上情況都不滿足,實(shí)例化默認(rèn)實(shí)現(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)建實(shí)例。

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

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

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

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

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

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

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

Spring Boot 是基于 Spring 構(gòu)建的框架,其設(shè)計(jì)目的在于簡化 Spring 應(yīng)用的配置、運(yùn)行。在 Spring Boot 中,大量運(yùn)用了自動(dòng)裝配來盡可能減少配置。

下面是一個(gè) 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 是如何做到寥寥幾行代碼,就可以運(yùn)行一個(gè) Spring Boot 應(yīng)用的呢。我們不妨帶著疑問,從源碼入手,一步步探究其原理。

4.3.1 @SpringBootApplication 注解

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

@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 這幾個(gè)元注解,

@SpringBootApplication 注解的定義中還標(biāo)記了@SpringBootConfiguration、

@EnableAutoConfiguration、@ComponentScan 三個(gè)注解。

4.3.2 @SpringBootConfiguration 注解

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

@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})兩個(gè)注解。

4.3.4 @AutoConfigurationPackage 注解

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

4.3.5 SpringFactoriesLoader.loadFactoryNames 方法

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

AutoConfigurationImportSelector。

AutoConfigurationImportSelector 有一個(gè)核心方法

getCandidateConfigurations 用于獲取候選配置。該方法調(diào)用了

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

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

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);
      // 解析文件,得到對(duì)應(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 文件,獲取指定自動(dòng)裝配類的全限定名。

4.3.6 Spring Boot 的 AutoConfiguration 類

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

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

從 spring-boot-autoconfigure 包的結(jié)構(gòu)來看,它有一個(gè) META-INF/spring.factories ,顯然利用了 Spring Boot SPI,來自動(dòng)裝配其中的配置類。

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

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

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

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

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

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

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

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

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

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

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

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

5.1 ExtensionLoader 入口

Dubbo SPI 的相關(guān)邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,可以加載指定的實(shí)現(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)的拓展實(shí)現(xiàn)類
        return getDefaultExtension();
    }
    // Holder,顧名思義,用于持有目標(biāo)對(duì)象
    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)建拓展實(shí)例
                instance = createExtension(name);
                // 設(shè)置實(shí)例到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}
			

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

private T createExtension(String name) {
    // 從配置文件中加載所有的拓展類,可得到“配置項(xiàng)名稱”到“配置類”的映射關(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)建實(shí)例
            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 實(shí)例
            for (Class wrapperClass : wrapperClasses) {
                // 將當(dāng)前 instance 作為參數(shù)傳給 Wrapper 的構(gòu)造方法,并通過反射創(chuàng)建 Wrapper 實(shí)例。
                // 然后向 Wrapper 實(shí)例中注入依賴,最后將 Wrapper 實(shí)例再次賦值給 instance 變量
                instance = injectExtension(
                    (T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}

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

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

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

  3. 向拓展對(duì)象中注入依賴

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

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

5.2獲取所有的拓展類

Dubbo 在通過名稱獲取拓展類之前,首先需要根據(jù)配置文件解析出拓展項(xiàng)名稱到拓展類的映射關(guān)系表(Map<名稱, 拓展類>),之后再根據(jù)拓展項(xiàng)名稱從映射關(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 加鎖。加鎖后再次檢查緩存,并判空。此時(shí)如果 classes 仍為 null,則通過 loadExtensionClasses 加載拓展類。下面分析 loadExtensionClasses 方法的邏輯。

private Map<String, Class> loadExtensionClasses() {
    // 獲取 SPI 注解,這里的 type 變量是在調(diào)用 getExtensionLoader 方法時(shí)傳入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // 對(duì) SPI 注解內(nèi)容進(jìn)行切分
            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 方法總共做了兩件事情,一是對(duì) SPI 注解進(jìn)行解析,二是調(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 方法的實(shí)現(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) {
                            // 以等于號(hào) = 為界,截取鍵與值
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // 加載類,并通過 loadClass 方法對(duì)類進(jìn)行緩存
                            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 方法進(jìn)行其他操作。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;
        }
        // 存儲(chǔ) clazz 到 cachedWrapperClasses 緩存中
        wrappers.add(clazz);


    // 程序進(jìn)入此分支,表明 clazz 是一個(gè)普通的拓展類
    } 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ù)組的第一個(gè)元素作為鍵,
                // 存儲(chǔ) name 到 Activate 注解對(duì)象的映射關(guān)系
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    // 存儲(chǔ) Class 到名稱的映射關(guān)系
                    cachedNames.put(clazz, n);
                }
                Class c = extensionClasses.get(n);
                if (c == null) {
                    // 存儲(chǔ)名稱到 Class 的映射關(guān)系
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    throw new IllegalStateException("...");
                }
            }
        }
    }
}

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

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

參考資料

  • Java SPI 思想梳理

  • Dubbo SPI

  • springboot 中 SPI 機(jī)制

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

審核編輯 :李倩


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

    關(guān)注

    19

    文章

    2948

    瀏覽量

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

    關(guān)注

    17

    文章

    1685

    瀏覽量

    91089

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

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

收藏 人收藏

    評(píng)論

    相關(guān)推薦

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

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

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

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

    java反編譯能拿到源碼

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

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

    的平臺(tái),Java可以用于開發(fā)桌面應(yīng)用程序、移動(dòng)應(yīng)用程序、企業(yè)級(jí)應(yīng)用程序等。 – Java Web是Java語言在Web開發(fā)領(lǐng)域的應(yīng)用,它使用Java
    的頭像 發(fā)表于 07-16 13:35 ?531次閱讀
    華納云:<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)科管理平臺(tái)源碼

    如何用java語言開發(fā)一套數(shù)字化產(chǎn)科系統(tǒng) 數(shù)字化產(chǎn)科管理平臺(tái)源碼
    的頭像 發(fā)表于 07-06 09:38 ?928次閱讀
    如何用<b class='flag-5'>java</b>語言開發(fā)一套數(shù)字化產(chǎn)科系統(tǒng)? 數(shù)字化產(chǎn)科管理平臺(tái)<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)源碼 實(shí)現(xiàn)人員/設(shè)備/車輛實(shí)時(shí)軌跡定位 UWB高精度人員定位系統(tǒng)提供實(shí)時(shí)定位、電子圍欄、軌跡回放等基礎(chǔ)功能以及各種拓展
    的頭像 發(fā)表于 06-24 09:33 ?344次閱讀
    <b class='flag-5'>Java</b>語言、idea開發(fā)工具、MYSQL數(shù)據(jù)庫開發(fā)的UWB定位技術(shù)系統(tǒng)<b class='flag-5'>源碼</b>

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

    本文主要介紹了基于智慧工地平臺(tái)的Java智慧工地監(jiān)管平臺(tái)源碼,通過結(jié)合物聯(lián)網(wǎng)、大數(shù)據(jù)、互聯(lián)網(wǎng)、云計(jì)算等技術(shù),視頻監(jiān)控管理、危大工程管理、綠色施工管理等多個(gè)功能。
    的頭像 發(fā)表于 06-18 15:35 ?428次閱讀
    <b class='flag-5'>Java</b> 智慧工地監(jiān)管平臺(tái)<b class='flag-5'>源碼</b> 依托智慧工地平臺(tái),滿足省、市級(jí)住建數(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 ?324次閱讀
    基于<b class='flag-5'>java</b>+單體服務(wù) +?硬件(UWB定位基站、卡牌)技術(shù)架構(gòu)開發(fā)的UWB室內(nèi)定位系統(tǒng)<b class='flag-5'>源碼</b>

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

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

    OneFlow Softmax算子源碼解讀之WarpSoftmax

    寫在前面:近來筆者偶然間接觸了一個(gè)深度學(xué)習(xí)框架 OneFlow,所以這段時(shí)間主要在閱讀 OneFlow 框架的 cuda 源碼。官方源碼基于不同場景分三種方式實(shí)現(xiàn) Softmax,本文主要介紹其中一種的實(shí)現(xiàn)過程,即 Warp 級(jí)
    的頭像 發(fā)表于 01-08 09:24 ?666次閱讀
    OneFlow Softmax算子<b class='flag-5'>源碼</b>解讀之WarpSoftmax

    Java怎么排查oom異常

    據(jù)量的應(yīng)用中。要排查OOM異常,需要經(jīng)過以下幾個(gè)步驟: 理解OOM異常的原因:OOM異常通常有以下幾個(gè)原因:內(nèi)存泄露、內(nèi)存溢出、內(nèi)存不足以容納所需的數(shù)據(jù)等。理解OOM異常的原因?qū)ε挪閱栴}非常重要。 配置Java堆內(nèi)存:要解決OO
    的頭像 發(fā)表于 12-05 13:47 ?1153次閱讀

    java文檔注釋的作用

    Java文檔注釋(JavaDoc)是一種特殊的注釋格式,用于對(duì)Java源代碼中的類、方法和字段進(jìn)行解釋和說明。它有助于開發(fā)人員理解代碼的功能、使用和注意事項(xiàng),并且還可以用于生成軟件文檔
    的頭像 發(fā)表于 11-28 17:02 ?824次閱讀

    簡單了解Java的新特性

    Java 8 到 Java 20,Java 已經(jīng)走過了漫長的道路,自 Java 8 以來,Java 生態(tài)系統(tǒng)發(fā)生了很多變化。最顯著的變化是
    的頭像 發(fā)表于 11-23 16:38 ?1002次閱讀
    簡單了解<b class='flag-5'>Java</b>的新特性

    如何理解java中的抽象類

    Java中的抽象類是一種特殊的類,無法被實(shí)例化,只能被繼承。它是一個(gè)中間層的類,位于具體類和接口之間,用于定義通用的屬性和方法,并提供一些默認(rèn)的實(shí)現(xiàn)。抽象類經(jīng)常被用來作為其他類的基類,以提供一些通用
    的頭像 發(fā)表于 11-21 10:16 ?514次閱讀

    如何理解示波器的采樣率和存儲(chǔ)深度?

    如何理解示波器的采樣率和存儲(chǔ)深度? 示波器是一種用于電子信號(hào)測量和分析的儀器。在理解示波器的采樣率和存儲(chǔ)深度之前,我們先來了解一下示波器的基本原理和工作方式。 示波器的基本原理 示波器
    的頭像 發(fā)表于 11-06 10:26 ?1463次閱讀