原始碼級深度理解 Java SPI

vivo網際網路技術發表於2022-11-07

作者:vivo 網際網路伺服器團隊- Zhang Peng

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

一、SPI 簡介

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

Java SPI 有四個要素:

  • SPI 介面:為服務提供者實現類約定的的介面或抽象類。

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

  • SPI 配置:Java SPI 機制約定的配置檔案,提供查zhao 服務實現類的邏輯。配置檔案必須置於 META-INF/services 目錄中,並且,檔名應與服務提供者介面的完全限定名保持一致。檔案中的每一行都有一個實現服務類的詳細資訊,同樣是服務提供者類的完全限定名稱。

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

二、SPI 示例

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

2.1 SPI 介面

首先,需要定義一個 SPI 介面,和普通介面並沒有什麼差別。

2.2 SPI 實現類

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

MySQL查詢 MOCK 類

Redis 查詢 MOCK 類

service 傳入的是期望載入的 SPI 介面型別 到目前為止,定義介面,並實現介面和普通的 Java 介面實現沒有任何不同。

2.3 SPI 配置

如果想透過 Java SPI 機制來發現服務,就需要在 SPI 配置中約定好發現服務的邏輯。配置檔案必須置於 META-INF/services 目錄中,並且,檔名應與服務提供者介面的完全限定名保持一致。檔案中的每一行都有一個實現服務類的詳細資訊,同樣是服務提供者類的完全限定名稱。以本示例程式碼為例,其檔名應該為io.github.dunwu.javacore.spi.DataStorage,

檔案中的內容如下:


2.4 ServiceLoader

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

輸出:

三、SPI 原理

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

3.1 ServiceLoader 的成員變數

先看一下 ServiceLoader 類的成員變數,大致有個印象,後面的原始碼中都會使用到。

3.2 ServiceLoader 的工作流程

(1)ServiceLoader.load 靜態方法

應用程式載入 Java SPI 服務,都是先呼叫 ServiceLoader.load 靜態方法。

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

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

② 然後,重新載入 SPI 服務

  • 清空快取中所有已例項化的 SPI 服務

  • 根據 ClassLoader 和 SPI 型別,建立懶載入迭代器

這裡,摘錄 ServiceLoader.load 相關原始碼,如下:


(2)應用程式透過 ServiceLoader 的 iterator 方法遍歷 SPI 例項

ServiceLoader 的類定義,明確了 ServiceLoader 類實現了  Iterable<T> 介面,所以,它是可以迭代遍歷的。實際上,ServiceLoader 類維護了一個快取 providers(  LinkedHashMap 物件),快取 providers 中儲存了已經被成功載入的 SPI 例項,這個 Map 的 key 是 SPI 介面實現類的全限定名,value 是該實現類的一個例項物件。

當應用程式呼叫 ServiceLoader 的 iterator 方法時,ServiceLoader 會先判斷快取 providers 中是否有資料:如果有,則直接返回快取 providers 的迭代器;如果沒有,則返回懶載入迭代器的迭代器。


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

上面的原始碼中提到了,lookupIterator 是 LazyIterator 例項,而 LazyIterator 用於懶載入 SPI 例項。那麼, LazyIterator 是如何工作的呢?

這裡,摘取 LazyIterator 關鍵程式碼

hasNextService 方法:

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

  • 透過類載入器,嘗試載入資原始檔

  • 解析資原始檔中的內容,獲取 SPI 介面的實現類的全限定名 nextName

nextService 方法:

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

  • 然後,嘗試透過 Class 的 newInstance 方法例項化一個 SPI 服務物件。如果成功,則將這個物件加入到快取 providers 中並返回該物件。

3.3 SPI 和類載入器

透過上面兩個章節中,走讀 ServiceLoader 程式碼,我們已經大致瞭解 Java SPI 的工作原理,即透過 ClassLoader 載入 SPI 配置檔案,解析 SPI 服務,然後透過反射,例項化 SPI 服務例項。我們不妨思考一下,為什麼載入 SPI 服務時,需要指定類載入器 ClassLoader 呢?

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

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

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

  1. 系統類防止記憶體中出現多份同樣的位元組碼

  2. 保證 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 介面:

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

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

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

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

  • : 提供 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 等操作。

而 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 實現。如下圖所示:

圖片

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

(3)建立資料庫連線

以 MySQL 為例,建立資料庫連線程式碼如下:

4.1.2 DriverManager

從前文,我們已經知道 DriverManager 是建立資料庫連線的關鍵。它究竟是如何工作的呢?

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


上面的程式碼主要步驟是:

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

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

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

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

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


這裡實際獲取的是java.util.ServiceLoader.LazyIterator 迭代器。呼叫其 hasNext 方法時,會搜尋 classpath 下以及 jar 包中的 META-INF/services 目錄,查詢 java.sql.Driver 檔案,並找到檔案中的驅動實現類的全限定名。呼叫其 next 方法時,會根據驅動類的全限定名去嘗試例項化一個驅動類的物件。

4.2 SPI 應用案例之 Common-Loggin

common-logging(也稱 Jakarta Commons Logging,縮寫 JCL)是常用的日誌門面工具包。common-logging 的核心類是入口是 LogFactory,LogFatory 是一個抽象類,它負責載入具體的日誌實現。

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

從以上原始碼可知,getLog 採用了工廠設計模式,是先呼叫 getFactory 方法獲取具體日誌庫的工廠類,然後根據類名稱或型別建立日誌例項。

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


從 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<URL> 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,來自動裝配其中的配置類。

圖片

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

圖片

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

圖片

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

圖片

透過以上分析,已經一步步解讀出 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<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        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<Class<?>> 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<String, Class<?>> getExtensionClasses() {
    // 從快取中獲取已載入的擴充類
    Map<String, Class<?>> 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<java.net.URL> 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<String, Class<?>> 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<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            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 等等。除此之外,該方法沒有其他什麼邏輯了。


參考資料


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2921716/,如需轉載,請註明出處,否則將追究法律責任。

相關文章