SOUL 中 SPI 的使用

SOUL 中 SPI 的使用

做者: "朱明"java

在以前分析 divide 插件的負載均衡策略時, 有看到過一行代碼:mysql

DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
複製代碼

當時很簡單的略過了它的實現, 它的做用很容易分析, 調用一個看似工具類的方法, 傳入多個節點組成的集羣, 返回一個節點. 這是一個負載均衡器.git

可是細節卻很是多, 最重要的一點是使用 SPI 來選擇具體的實現類. 看看這個方法的代碼:github

public class LoadBalanceUtils {

  public static DivideUpstream selector(final List<DivideUpstream> upstreamList, final String algorithm, final String ip) {
    // 調用自定義的 SPI 獲得一個子類
    LoadBalance loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getJoin(algorithm);
    return loadBalance.select(upstreamList, ip);
  }
}
複製代碼

後面的是調用具體子類的 select() 方法, 根據子類的不一樣實現, 最終會表現出各類形式. 目前的子類實現有:sql

  • HashLoadBalance
  • RandomLoadBalance
  • RoundRobinLoadBalance

關鍵就在於 ExtensionLoader.getExtensionLoader(LoadBalance.class).getJoin(algorithm); 這行.數據庫

在研究它以前, 咱們先不妨研究下 Java 提供的 SPI 機制.apache

Java SPI

<<高可用可伸縮微服務架構>> 第3章 Apache Dubbo 框架的原理與實現 中有這樣的一句定義.緩存

SPI 全稱爲 Service Provider Interface, 是 JDK 內置的一種服務提供發現功能, 一種動態替換髮現的機制. 舉個例子, 要想在運行時動態地給一個接口添加實現, 只須要添加一個實現便可.markdown

書中也有個很是形象的腦圖, 展現了 SPI 的使用:架構

image.png

也就是說在咱們代碼中的實現裏, 無需去寫入一個 Factory 工廠, 用 MAP 去包裝一些子類, 最終返回的類型是父接口. 只須要定義好資源文件, 讓父接口與它的子類在文件中寫明, 便可經過設置好的方式拿到全部定義的子類對象:

ServiceLoader<Interface> loaders = ServiceLoader.load(Interface.class)
for(Interface interface : loaders){
	System.out.println(interface.toString());
}
複製代碼

這種方式相比與普通的工廠模式, 確定是更符合開閉原則, 新加入一個子類不用去修改工廠方法, 而是編輯資源文件.

從一個 Demo 開始

按照 SPI 的規範, 我建了一個 demo, 看看具體的實現效果

image.png

image.png

Animal 中定義一個 run() 方法, 而子類實現它.

public interface Animal {
  void run();
}

public class Dog implements Animal {
  @Override
  public void run() {
    System.out.println("狗在跑");
  }
}

public class Horse implements Animal {
  @Override
  public void run() {
    System.out.println("馬在跑");
  }
}
複製代碼

使用 SPI 的加載類, 獲得子類的執行結果:

private static void test() {
  final ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
  
  for (Animal animal : load) {
    System.out.println(animal);
    animal.run();
  }
}
複製代碼

image.png 在調用後咱們獲得以前在資源文件中寫入的實現類, 併成功調取它們各自的 run() 方法.

到這裏我產生一個疑問, 是否每次調用 ServiceLoader.load(Animal.class) 返回的都是同一個對象? 若是是我猜想它是在啓動時加載到緩存了, 若是不是, 可能就是在底層用了反射, 每次調用都有必定消耗. 咱們看看下面的實驗:

public static void main(String[] args) {
  for (int i = 0; i < 2; i++) {
    test();
    System.out.println("----------");
  }
}

private static void test() {
  final ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
  for (Animal animal : load) {
    System.out.println(animal);
    animal.run();
  }
}
複製代碼

image.png

兩次調用出現的對象卻不同, 不禁讓我替其性能揪心一下, 因此咱們先分析下它的代碼, 看看到底怎麼實現.

SPI 的實現

找到 java.util,ServiceLoaders 這個類, 入眼最醒目的就是以前咱們按照規範放置資源文件的目錄

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

  private static final String PREFIX = "META-INF/services/";
}
複製代碼

在 debug PREFIX 屬性的被調用處時, 發現 ServiceLoader.load 實際是使用懶加載的方式, 並無在調用它的時候, 找尋到實際返回類, 而是在遍歷時查找.

它的懶加載具體實如今以下代碼:

public final class ServiceLoader<S> implements Iterable<S> {
  
  public static <S> ServiceLoader<S> load(Class<S> service) {
    // 獲取當前的類加載器 (咱們本身的一般是弟中弟 AppClassLoader )
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
  }
  
  public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
    // 調用構造器初始化對象 (說明每次調用都使用新的 ServiceLoader 對象)
    return new ServiceLoader<>(service, loader);
  }
  
  private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 上面都是將信息放入對象實例屬性中, 這行纔是關鍵調用
    reload();
  }
  
  public void reload() {
    providers.clear();
    // 建立懶加載迭代器, 傳入關鍵的接口 Class 以及加載器
    lookupIterator = new LazyIterator(service, loader);
  }
}

複製代碼

調用 ServiceLoader.load 後關鍵事情都沒幹, 僅僅是將接口 class 和加載器傳給 LazyIterator 這個迭代器的實現類.

看到這能夠猜想, 真正迭代調用返回的對象時, 確定須要迭代器完成實現類的搜索和初始化, 而傳參是 Class 信息和加載器, 實現類的初始化也明顯會是反射了.

看下 LazyIterator 的實現方式, 先從其最開始會被調用到的 hasNext() 開始:

private class LazyIterator implements Iterator<S> {
  
  public boolean hasNext() {
    if (acc == null) {
      return hasNextService();
    } else {
      // ...
    }
  }
  
  private boolean hasNextService() {
    if (nextName != null) {
      return true;
    }
    if (configs == null) {
      try {
        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;
  }
}
複製代碼

image.png

hasNext() 的調用能夠獲取到咱們資源中的類名, 寫入到實例屬性 nextName 中, 並返回 true, 讓迭代器能夠進行 next() 的調用

public S next() {
  if (acc == null) {
    return nextService();
  } else {
    // ...
  }
}

private S nextService() {
  if (!hasNextService()) throw new NoSuchElementException();
  String cn = nextName;
  nextName = null;
  Class<?> c = null;
  try {
    // 反射獲得 Class 對象
    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 subtype");
  }
  try {
    // 初始化對象, 並判斷是否與接口符合
    S p = service.cast(c.newInstance());
    // 將初始化的對象放入hash緩存 (關鍵步驟)
    providers.put(cn, p);
    return p;
  } catch (Throwable x) {
    fail(service, "Provider " + cn + " could not be instantiated", x);
  }
  throw new Error();          // This cannot happen
}
複製代碼

看到這裏咱們明白了, 在初始化後會將對象放入緩存中, key 就是接口 class 二次調用不會再有反射消耗.

那麼以前咱們在測試時的方式爲何會產生不一樣對象實例呢? 緣由就是每次調用 ServiceLoader.load() 都會產生新的 ServiceLoader 對象. 咱們將測試方法改進下:

public static void main(String[] args) {
  // 複用 ServiceLoaders
  final ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
  for (int i = 0; i < 2; i++) {
    test(load);
    System.out.println("----------");
  }
}

private static void test(ServiceLoader<Animal> load) {
  for (Animal animal : load) {
    System.out.println(animal);
    animal.run();
  }
}
複製代碼

image.png

Java SPI 思考

Java SPI 中咱們還有不少的細節沒有描述到, 但主流程就是這些. 咱們以前的兩個疑問點, 如何實現以及性能狀況也能夠獲得解答:

  1. 如何實現: 經過IO流讀取到資源文件, 反射加載對應路徑並生成Class對象, 初始化後放入緩存中
  2. 性能狀況: 首次迭代調用即會有反射調用, 但屢次使用時, 只要保證是用同一個 ServiceLoader 對象, 便可避免屢次反射, 由於會直接複用緩存中的對象.

寫到這我有個很是疑惑的地方, 以前我以爲它和工廠方法很相似但比它有優點, 由於添加子類後僅需用改動資源文件不用變更工廠類.

但我嘗試用 Java SPI 去真正實現時, 發現並不能達到這個效果, 一個重要的緣由是, 資源文件中的各個實現類沒有區分度, 我沒法去篩選出某一個我須要的緩存在 ServiceLoaders 中的實現類.

那麼它的使用場景在哪呢?

JDBC SPI 使用方式

通過查閱資料得知, 在 JDBC 中最關鍵的可插拔式驅動設計就是由 SPI 實現.

Mysql 驅動包 SPI

各個數據庫鏈接包中關於 JDBC 方式實現, 都須要實現其 Driver 接口, 這塊其實用的就是 SPI 的方式, 咱們看看 mysql-connector-java.jar

image.png

那麼 JDK 中的 JDBC 相關類, 是如何實現這塊的? 關鍵類就是 DriverManager

public class DriverManager {
  
  static {
    loadInitialDrivers();
  }
  
  private static void loadInitialDrivers() {
    // ...
    
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
      public Void run() {
				
        // 這裏就是 SPI 的實現, 迭代時實際會 Class.forName() 初始化實現類
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        try{
          while(driversIterator.hasNext()) {
            driversIterator.next();
          }
        } catch(Throwable t) {
          // Do nothing
        }
        return null;
      }
    });
    
    // ...
  }
}
複製代碼

若是代碼中調用到 DriverManager 的靜態方法, 即會觸發上面這些代碼, 而這些代碼的做用即是將 SPI 資源文件中 Driver 實現類所有初始化, 那麼初始化實現類後又有什麼做用呢? 接着看看 com.mysql.jdbc.Driver

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  static {
    try {
      // 調用 DriverManager 的註冊方法, 將此 Driver 實現類註冊到 JDBC 的 Driver 管理器中
      java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
      throw new RuntimeException("Can't register driver!");
    }
  }
}
複製代碼

DriverManager 的註冊方法實現很簡單, 即將入參放入靜態變量做爲全局緩存

public class DriverManager {
	// 緩存 Driver 實現類
  private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

  public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
    registerDriver(driver, null);
  }

  public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException {
    if(driver != null) {
      // 註冊到變量中
      registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
    } else {
      throw new NullPointerException();
    }
  }
}
複製代碼

篩選 Driver: 約定大於配置

正常使用時, 咱們會直接用 DriverManager.getConnection(url, user, passwd) 獲取到鏈接, 但這裏就有疑問了, 咱們在 DriverManager 中註冊了多個 Driver, 爲何這裏能肯定一個惟一 Driver 呢?

先找到 DriverManager 的 getConnection() 方法:

public static Connection getConnection(String url, String user, String password) throws SQLException {
  // ...
  return (getConnection(url, info, Reflection.getCallerClass()));
}

private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException {

  // ...
  
  for(DriverInfo aDriver : registeredDrivers) {
    // isDriverAllowed() 僅是經過 Class.forName() 初始化, 沒有甄別做用
    if(isDriverAllowed(aDriver.driver, callerCL)) {
      try {
        // 最關鍵的點在這行, 篩選工做其實在實現類自身的 connect() 方法中, 會根據傳入的 url 篩選
        Connection con = aDriver.driver.connect(url, info);
        if (con != null) {
          return (con);
        }
      } catch (SQLException ex) {
      }
    } else {
    }

  }

  // ...
}
複製代碼

看看最重要的 Mysql 的 Driver 中如何實現篩選 (Driver 繼承自 NonRegisteringDriver)

public class NonRegisteringDriver implements java.sql.Driver {
	private static final String URL_PREFIX = "jdbc:mysql://";
  private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://";
  private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://";
  public static final String LOADBALANCE_URL_PREFIX = "jdbc:mysql:loadbalance://";
  
  public java.sql.Connection connect(String url, Properties info) throws SQLException {
    // ...
		// parseURL() 會匹配 url 是否符合其所在 Driver 的鏈接方式
    // 這裏就是採用"約定大於配置"的思想, 經過匹配路徑頭作篩選
    if ((props = parseURL(url, info)) == null) {
      return null;
    }

    // ...
  }
  
  public Properties parseURL(String url, Properties defaults) throws java.sql.SQLException {
    // ...
		// 若是 url 不匹配此 Driver 的路徑則返回null, 最外層會繼續嘗試下個 Driver
    if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)
        && !StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) {
      return null;
    }
    
    // ...
  }
}
複製代碼

總結 MySQL & JDBC

看到這裏我想你已經瞭解 MySQL & JDBC 中關於 SPI 的實現方式了, 概括幾點

  • JDBC 中的 DriverManager 會加載 SPI 資源文件, 將 java.sql.Driver 的實現類所有初始化
  • 其實現類初始化時, 會自主建立自身對象並注入到 DriverManager 中進行統一管理
  • DriverManager 對於管理的 Driver 篩選方式是交由 Driver 實現類自身進行的, 它僅負責遍歷並取出可用的 Driver
  • Driver 實現類經過傳入的數據庫 url 頭, 判斷是否該返回自身. 若是判斷爲不然返回 null. JDBC 的 DriverManager 接收到 null 會繼續下個 Driver 實現類的調用.
  • MySql 驅動實選方案是路徑頭匹配, 是一種 約定大於配置的思想

JDBC Demo

寫完這些分析咱們再來看若是實現個簡單的 demo.

先分享個我之前寫的方式

static {
  try {
    // 反射, 該類加載時會在靜態塊中, 向 DriverManager 註冊 Driver
    Class.forName("com.mysql.jdbc.Driver");
  } catch (ClassNotFoundException e) {
    e.printStackTrace();
  }
}

public static void main(String[] args) {
  try (
    final Connection conn = DriverManager.getConnection(url, user, passwd);
    final Statement stmt = conn.createStatement();
    final ResultSet rs = stmt.executeQuery("select count(1) from test")
  ) {
    while (rs.next()) {
      int count = rs.getInt("count(1)");
      System.out.println(count);
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
}
複製代碼

雖然這樣可使用, 但不以爲有多餘的代碼嗎? 看看我新寫的方式

public static void main(String[] args) throws ClassNotFoundException {
  try (
    final Connection conn = DriverManager.getConnection(url, user, passwd);
    final Statement stmt = conn.createStatement();
    final ResultSet rs = stmt.executeQuery("select count(1) from test")
  ) {
    while (rs.next()) {
      int count = rs.getInt("count(1)");
      System.out.println(count);
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
}
複製代碼

僅僅須要這些簡單的代碼便可, DriverManager.getConnection() 被調用時 DriverManager 會自動加載 SPI 中的實現類, 不須要咱們再去 Class.forName() 手動調用 java.mysql.Driver 的初始化.

看到這裏我想你依然明白 SPI 最最重要的做用了. 無需顯式的寫出接口對應的實現類

那麼咱們還有個在 "Java SPI 思考" 中的問題也解開了. **如何區分出 SPI 中要使用的實現類呢? 讓實現類本身斷定便可, 外層調用僅需迭代全部. **

SOUL SPI 實現

Java 中 SPI 的使用方式咱們已經掰開來了解透徹了, 而 Soul 中的 SPI 是本身設計的, 採用 Dubbo 中 SPI 的設計思想. 在 org.dromara.soul.spi.SPI 註釋類上能夠看到相關注釋.

/** * SPI Extend the processing. * All spi system reference the apache implementation of * https://github.com/apache/dubbo/blob/master/dubbo-common/src/main/java/org/apache/dubbo/common/extension. */
複製代碼

Java SPI 缺陷

在上兩個模塊中分析 Java SPI 使用時, 發現了些缺點:

  1. 若是使用 ServiceLoader 不當, 沒有正確利用到它的緩存機制, 會致使每次獲取具體實現類都要反射出類對象以及初始化實例對象, 性能完蛋不說, 每次獲得的對象都不同可能會引起程序問題.
  2. 即每次找尋具體實現類都要迭代一遍才行, 雖然子類少的使用沒什麼影響, 但這種方式仍是很傻. 另外參考 MySQL 驅動中 JDBC 的實現, 還須要自行設計一套比較複雜的篩選機制.

那麼 Soul SPI 的實現, 是如何解決這兩個問題的? 關鍵就在接下來的兩個子模塊中

  • 優化的 ExtensionLoader
  • 加強型 getJoin()

優化的 ExtensionLoader

先來看 SPI 實現項目的全貌, 項目爲 soul-spi:

image.png

其中最核心的類就是 ExtensionLoader, 能夠說是 Soul 版的 ServiceLoader, 它也定義了 SPI 資源文件的路徑位置

public final class ExtensionLoader<T> {
  private static final String SOUL_DIRECTORY = "META-INF/soul/";
}
複製代碼

經過檢查它各個方法的調用處, 咱們找到入口方法 getExtensionLoader()

public final class ExtensionLoader<T> {
  
  private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();
  
  public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
    // ...

    // 根據加載類對象取出緩存中數據, 若是沒有則新建 ExtensionLoader 對象並放入緩存
    ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) LOADERS.get(clazz);
    if (extensionLoader != null) {
      return extensionLoader;
    }
    LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz));
    return (ExtensionLoader<T>) LOADERS.get(clazz);
  }
}
複製代碼

這個方法的做用其實就像是 ServiceLoader 的 load() 方法, 會返回一個 ServiceLoader 對象.

只是 Soul 中的實現改了種方式, 將 ExtensionLoader 對象緩存起來, 這樣 二次調用時傳入相同 Class 對象也會返回一樣的 ExtensionLoader, 避免了 ServiceLoader 使用時不理解其機制致使沒有用到它的緩存, 每次迭代都去反射初始化全部實現類

加強型搜索 getJoin()

再來看看 ExtensionLoader 的 getJoin() 方法, 我將它理解爲 更優的 ServiceLoader 迭代器版實現. 它一樣是作了兩件 ServiceLoader 迭代時作過的事情:

  • 初始化 SPI 中的實現類

  • 將實現類緩存 -> 緩存爲 Key-Value 形式的 Map 集合

基於 K-V 緩存模式, 它還作了一件我最期待的改造:

  • 時間複雜度 O(1) 的直接匹配實現類方式

多層緩存

ExtensionLoader 之因此能作到這種加強型搜索, 無需每次都迭代全部, 是依靠三種不一樣類型的緩存.

這三種緩存我將它分爲二層, 它們各有不一樣用途, 總覽以下:

// 一層緩存
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();

// 二層緩存之一
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();

// 二層緩存之一
private final Map<Class<?>, Object> joinInstances = new ConcurrentHashMap<>();
複製代碼

第一層緩存: cachedInstances

首先是第一層緩存, 它是咱們搜索接口的具體實現類時最早接觸到的, 若是命中它則直接能夠獲得實現類的對象

private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
複製代碼

它的 key 其實就是 Soul SPI 資源文件中咱們配置的信息, 好比 Divide 插件的負載均衡實現類的資源文件

image.png

而它的 value 則是 Holder 對象, 其中存有實現類的對象. 調用 getJoin() 時傳入標識 (好比 random) 得到實現類對象.

public T getJoin(final String name) {
  // ...
  Holder<Object> objectHolder = cachedInstances.get(name);
  Object value = objectHolder.getValue();
  // ...
  return (T) value;
}
複製代碼

第二層緩存之: cachedClasses

cachedClasses 存放的是 標識(random) 與 類對象 的映射

private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
複製代碼

cachedClasses 緩存的信息如何填充的呢? 是直接觸發到檢索 SPI 資源文件, 而後解析成 cachedClasses 緩存. 具體方法在 loadResources()

private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {
  Properties properties = new Properties();
  // 解析資源文件
  properties.load(inputStream);
  properties.forEach((name, classPath) -> {
    // 讀出 K-V 結構並組裝成 classes, 外層調用會包裝到 cachedClasses
    loadClass(classes, name, classPath);
	});
}
複製代碼

第二層緩存之: joinInstances

joinInstances 緩存存放的是 類對象與對象實例 的映射

private final Map<Class<?>, Object> joinInstances = new ConcurrentHashMap<>();
複製代碼

這一層緩存會藉助第二層緩存, 獲得對應標識(random) 的類對象, 並經過類對象初始化實例, 緩存到自身中. 對應實現方法爲 createExtension()

private T createExtension(final String name) {
  Class<?> aClass = getExtensionClasses().get(name);
  Object o = joinInstances.get(aClass);
  if (o == null) {
    joinInstances.putIfAbsent(aClass, aClass.newInstance());
  }
  return (T) o;
}
複製代碼

緩存小結

經過 ExtensionLoader 加載某個接口的實現類時, 緩存調用流程圖以下:

image.png

詳細源碼分析 (可跳過)

// name 理解爲標識, 用於甄別 SPI 文件中, 想要獲取的某個實現類
public T getJoin(final String name) {
  // ...
  // cachedInstances 緩存全部 Holder 對象. Holder 對象的 value 屬性存放具體實現類
  // 我將 cachedInstances 理解爲第一層緩存, 命中則直接返回要找的類
  Holder<Object> objectHolder = cachedInstances.get(name);
  if (objectHolder == null) {
    cachedInstances.putIfAbsent(name, new Holder<>());
    objectHolder = cachedInstances.get(name);
  }
  Object value = objectHolder.getValue();
  // 雙重鎖, 若是沒有命中則調用 createExtension()
  if (value == null) {
    synchronized (cachedInstances) {
      value = objectHolder.getValue();
      if (value == null) {
        value = createExtension(name);
        objectHolder.setValue(value);
      }
    }
  }
  return (T) value;
}
複製代碼
private T createExtension(final String name) {
  // 關鍵代碼, 搜索標識對應的類對象
  Class<?> aClass = getExtensionClasses().get(name);
  if (aClass == null) {
    throw new IllegalArgumentException("name is error");
  }
  // joinInstances 理解爲第二層緩存, K-V 存放類對象與其初始化對象
  Object o = joinInstances.get(aClass);
  if (o == null) {
    try {
      joinInstances.putIfAbsent(aClass, aClass.newInstance());
      o = joinInstances.get(aClass);
    } catch (InstantiationException | IllegalAccessException e) {
      // ...
    }
  }
  return (T) o;
}
複製代碼
public Map<String, Class<?>> getExtensionClasses() {
  // cachedClasses 爲第三層緩存, 存放標識與類對象映射
  Map<String, Class<?>> classes = cachedClasses.getValue();
  if (classes == null) {
    synchronized (cachedClasses) {
      classes = cachedClasses.getValue();
      if (classes == null) {
  			// 構造 classes 緩存, classes 的 K-V 結構爲 標識-類對象
        classes = loadExtensionClass();
        cachedClasses.setValue(classes);
      }
    }
  }
  return classes;
}
複製代碼
private Map<String, Class<?>> loadExtensionClass() {
  // 拿到接口的 SPI 註解
  SPI annotation = clazz.getAnnotation(SPI.class);
  if (annotation != null) {
    String value = annotation.value();
    if (StringUtils.isNotBlank(value)) {
      cachedDefaultName = value;
    }
  }
  // 構造 classes 緩存, classes 的 K-V 結構爲 標識-類對象
  Map<String, Class<?>> classes = new HashMap<>(16);
  loadDirectory(classes);
  return classes;
}
複製代碼
private void loadDirectory(final Map<String, Class<?>> classes) {
  String fileName = SOUL_DIRECTORY + clazz.getName();
  try {
    ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
    // 讀取 SPI 資源文件
    Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fileName)
      : ClassLoader.getSystemResources(fileName);
    if (urls != null) {
      while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        // 構造 classes 緩存, classes 的 K-V 結構爲 標識-類對象
        loadResources(classes, url);
      }
    }
  }
}
複製代碼
private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {
  try (InputStream inputStream = url.openStream()) {
    Properties properties = new Properties();
    properties.load(inputStream);
    // 解析資源文件爲 KV 結構
    properties.forEach((k, v) -> {
      String name = (String) k;
      String classPath = (String) v;
      if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
        try {
          // 加載路徑, 傳入 classes 緩存、標識、類路徑
          loadClass(classes, name, classPath);
        } catch (ClassNotFoundException e) {
          throw new IllegalStateException("load extension resources error", e);
        }
      }
    });
  }
}
複製代碼
private void loadClass(final Map<String, Class<?>> classes, final String name, final String classPath) throws ClassNotFoundException {
  // 將資源文件中的類路徑反射成類對象
  Class<?> subClass = Class.forName(classPath);
  // 拿到實現類的 Join 註解
  Join annotation = subClass.getAnnotation(Join.class);
  Class<?> oldClass = classes.get(name);
  if (oldClass == null) {
    // 放入入參 classes 緩存中, K-V 形式爲 標識-類對象
    classes.put(name, subClass);
  }
}
複製代碼
相關文章
相關標籤/搜索