如何在項(xiàng)目中引入SPI

開閉原則是面向?qū)ο蟪绦蛟O(shè)計(jì)的終極目標(biāo),它使軟件實(shí)體擁有一定的適應(yīng)性和靈活性的同時(shí)具備穩(wěn)定性和延續(xù)性。當(dāng)應(yīng)用的需求改變時(shí),在不修改軟件實(shí)體的源代碼或者二進(jìn)制代碼的前提下,可以擴(kuò)展模塊的功能,使其滿足新的需求。SPI 就是開閉原則的一種實(shí)現(xiàn)。本文將帶領(lǐng)同學(xué)們了解 SPI ,對比 Dubbo SPI 與 Java SPI ,同時(shí)用一個(gè) Dubbo SPI 替換 Java SPI 的實(shí)踐項(xiàng)目,來演示如何將 SPI 機(jī)制引入日常項(xiàng)目中。

什么是 SPI

SPI 全稱是 Service Provider Interface,是一種將服務(wù)接口與服務(wù)實(shí)現(xiàn)分離以達(dá)到解耦、可以提升程序可擴(kuò)展性的機(jī)制。引入服務(wù)提供者就是引入了 SPI 接口的實(shí)現(xiàn)者,通過本地的注冊發(fā)現(xiàn)獲取到具體的實(shí)現(xiàn)類,可以在運(yùn)行時(shí),動態(tài)為接口替換實(shí)現(xiàn)類,實(shí)現(xiàn)服務(wù)的熱插拔。

Java SPI

Java SPI 中有四個(gè)重要的組件:

  1. 服務(wù)接口:一個(gè)定義了服務(wù)提供者實(shí)現(xiàn)類契約方法的接口或者抽象類。
  2. 服務(wù)實(shí)現(xiàn):實(shí)際提供服務(wù)的實(shí)現(xiàn)類。
  3. SPI 配置文件:文件名必須存在于 META-INF/services 目錄中。文件名應(yīng)與服務(wù)提供商接口完全限定名完全相同。文件中的每一行都有一個(gè)實(shí)現(xiàn)服務(wù)類詳細(xì)信息,即服務(wù)提供者類的完全限定名。
  4. ServiceLoader: Java SPI 關(guān)鍵類,用于加載服務(wù)提供者接口的服務(wù)。ServiceLoader 中有各種實(shí)用程序方法,用于獲取特定的實(shí)現(xiàn)、迭代它們或再次重新加載服務(wù)。

小試牛刀

服務(wù)接口

現(xiàn)有一個(gè)壓縮與解壓服務(wù)接口,有一個(gè)壓縮方法 compress ,一個(gè)解壓方法 decompress,入?yún)⒊鰠⒍际亲止?jié)數(shù)組:

package cn.ppphuang.demoserver.serviceproviders;

public interface Compresser {
    byte[] compress(byte[] bytes);
    byte[] decompress(byte[] bytes);
}

服務(wù)實(shí)現(xiàn)

有兩個(gè)實(shí)現(xiàn)類,假設(shè)一個(gè)使用Gzip算法來實(shí)現(xiàn):

package cn.ppphuang.demoserver.serviceproviders;

import java.nio.charset.StandardCharsets;

public class GzipCompresser implements Compresser{
    @Override
    public byte[] compress(byte[] bytes) {
        return "compress by Gzip".getBytes(StandardCharsets.UTF_8);
    }
    @Override
    public byte[] decompress(byte[] bytes) {
        return "decompress by Gzip".getBytes(StandardCharsets.UTF_8);
    }
}

另一個(gè)使用Zip算法來實(shí)現(xiàn):

package cn.ppphuang.demoserver.serviceproviders;

import java.nio.charset.StandardCharsets;

public class ZipCompresser implements Compresser {
    @Override
    public byte[] compress(byte[] bytes) {
        return "compress by Zip".getBytes(StandardCharsets.UTF_8);
    }
    @Override
    public byte[] decompress(byte[] bytes) {
        return "decompress by Zip".getBytes(StandardCharsets.UTF_8);
    }
}

SPI 配置文件

然后在項(xiàng)目 META-INF/services 文件夾(如果 resources 目錄下沒有,創(chuàng)建該目錄)下創(chuàng)建 cn.ppphuang.demoserver.serviceproviders.Compresser 文件,文件名是壓縮服務(wù)接口類的全限定類名,兩行內(nèi)容分別是剛剛兩個(gè)接口實(shí)現(xiàn)類的全限定類名,如下:

cn.ppphuang.demoserver.serviceproviders.GzipCompresser
cn.ppphuang.demoserver.serviceproviders.ZipCompresser

通過 ServiceLoader 加載服務(wù)

main 方法中通過 ServiceLoader.load(Compresser.class) 獲取該服務(wù)的所有實(shí)現(xiàn)類,遍歷實(shí)例調(diào)用方法。

public static void main(String[] args) {
  ServiceLoader<Compresser> serviceLoader = ServiceLoader.load(Compresser.class);
  for (Compresser service : serviceLoader) {
    System.out.println(service.getClass().getClassLoader());
    byte[] compress = service.compress("Hello".getBytes(StandardCharsets.UTF_8));
    System.out.println(new String(compress));
    byte[] decompress = service.decompress("Hello".getBytes(StandardCharsets.UTF_8));
    System.out.println(new String(decompress));
  }
}

輸出結(jié)果

sun.misc.Launcher$AppClassLoader@18b4aac2
compress by Gzip
decompress by Gzip
sun.misc.Launcher$AppClassLoader@18b4aac2
compress by Zip
decompress by Zip

由輸出結(jié)果可以看到,不需要我們自己去實(shí)例化兩個(gè)實(shí)現(xiàn)類,就可以直接調(diào)用。加載實(shí)現(xiàn)類的類加載器是 AppClassLoader

如果你還有這個(gè)接口的其他實(shí)現(xiàn),你可以在別的包里實(shí)現(xiàn)這個(gè)接口,然后在實(shí)現(xiàn)類所在包的 META-INF/services 文件夾下創(chuàng)建 **cn.ppphuang.demoserver.serviceproviders.Compresser 文件,文件內(nèi)容為你的實(shí)現(xiàn)類的全限定類名。ServiceLoader **會去尋找所有包下的 META-INF/services/cn.ppphuang.demoserver.serviceproviders.Compresser 文件,加載并實(shí)例化文件內(nèi)容中每一行的實(shí)現(xiàn)類。

使用場景

SPI 機(jī)制使用的非常廣泛,我們以 JDBC 為例看 SPI 如何使用。

JDBC 使用 SPI 加載不同類型數(shù)據(jù)庫的驅(qū)動,下面是我們常用的使用 JDBC 操作 MySql 數(shù)據(jù)庫的示例代碼,沒有顯式指定使用哪種數(shù)據(jù)庫驅(qū)動,依然可以正常使用。

public static void main(String[] args) throws SQLException, ClassNotFoundException {
    Connection conn = null;
    try {
      conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "root");
    } catch (SQLException e) {
      System.out.println("數(shù)據(jù)庫連接失敗");
    }
    Statement statement = conn.createStatement();
    ResultSet resultSet = statement.executeQuery("select * from user where id = 1");
    while (resultSet.next()) {
      System.out.println(resultSet.getString(2));
    }
}

來看 DriverManager 類的代碼,靜態(tài)代碼塊中調(diào)用 loadInitialDrivers 方法加載數(shù)據(jù)庫驅(qū)動,方法里使用 ServiceLoader.load(Driver.class) 加載驅(qū)動類。

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
  }
  private static void loadInitialDrivers() {
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
      public Void run() {
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        while(driversIterator.hasNext()) {
          driversIterator.next();
        }
        return null;
      }
    });
    }
}

我們打開 mysql-connector-java 包的 META-INF/services 文件夾,果然有 java.sql.Driver 類的 SPI 配置文件,文件內(nèi)容的第一行就是 MySQL 的連接驅(qū)動類的全限定類名。

META-INF/services

再看 com.mysql.jdbc.Driver 驅(qū)動類,靜態(tài)方法中實(shí)例化了自己,并將自己注冊到 DriverManager 中。

以上就是為什么我們不指定驅(qū)動類還可以正常使用的原因。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

我們總結(jié)一下 JDBC 自適應(yīng)驅(qū)動的流程:

  1. 實(shí)現(xiàn)了 java.sql.Driver 的驅(qū)動包,按照 SPI 的約定,在 META-INF/services/java.sql.Driver 文件中指定具體的驅(qū)動類。
  2. DriverManager 利用 ServiceLoader 去掃描各個(gè) jar 包下的 META-INF/services/java.sql.Driver 文件,加載并初始化文件內(nèi)容中指定的驅(qū)動實(shí)現(xiàn)類。
  3. 初始化具體的實(shí)現(xiàn)類,就會自動向 DriverManager 注冊當(dāng)前實(shí)現(xiàn)類到 DriverManager 中的 registeredDrivers。
  4. 使用 DriverManager.getConnection 連接數(shù)據(jù)庫時(shí),getConnection 中會循環(huán) registeredDrivers 嘗試校驗(yàn)并連接數(shù)據(jù)庫

美中不足

使用 Java SPI 能方便得解耦模塊,使得接口的定義與具體業(yè)務(wù)實(shí)現(xiàn)分離。應(yīng)用程序可以根據(jù)實(shí)際業(yè)務(wù)情況啟用或替換具體組件。

但是也有一些缺點(diǎn):

  • 不能按需加載。雖然 ServiceLoader 做了延遲載入,但是基本只能通過遍歷全部獲取,也就是接口的實(shí)現(xiàn)類得全部載入并實(shí)例化。如果你并不想用某些實(shí)現(xiàn)類,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了,這就造成了浪費(fèi)。
  • 獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據(jù)某個(gè)參數(shù)來獲取對應(yīng)的實(shí)現(xiàn)類。
  • 多個(gè)并發(fā)多線程使用 ServiceLoader 類的實(shí)例是不安全的。

Dubbo SPI

為了使用更加完美的 SPI 機(jī)制,優(yōu)化 Java SPI 的弊端,很多廠商自己實(shí)現(xiàn)了一套 SPI 機(jī)制,比如 Dubbo 。

Dubbo SPI 擴(kuò)展能力的特性:

  • 按需加載。Dubbo 的擴(kuò)展能力不會一次性實(shí)例化所有實(shí)現(xiàn),而是用哪個(gè)擴(kuò)展類則實(shí)例化哪個(gè)擴(kuò)展類,減少資源浪費(fèi)。
  • 增加擴(kuò)展類的 IOC 能力。Dubbo 的擴(kuò)展能力并不僅僅只是發(fā)現(xiàn)擴(kuò)展服務(wù)實(shí)現(xiàn)類,而是在此基礎(chǔ)上更進(jìn)一步,如果該擴(kuò)展類的屬性依賴其他對象,則 Dubbo 會自動的完成該依賴對象的注入功能。
  • 增加擴(kuò)展類的 AOP 能力。Dubbo 擴(kuò)展能力會自動的發(fā)現(xiàn)擴(kuò)展類的包裝類,完成包裝類的構(gòu)造,增強(qiáng)擴(kuò)展類的功能。
  • 具備動態(tài)選擇擴(kuò)展實(shí)現(xiàn)的能力。Dubbo 擴(kuò)展會基于參數(shù),在運(yùn)行時(shí)動態(tài)選擇對應(yīng)的擴(kuò)展類,提高了 Dubbo 的擴(kuò)展能力。
  • 可以對擴(kuò)展實(shí)現(xiàn)進(jìn)行排序。能夠基于用戶需求,指定擴(kuò)展實(shí)現(xiàn)的執(zhí)行順序。
  • 提供擴(kuò)展點(diǎn)的 Adaptive 能力。該能力可以使的一些擴(kuò)展類在 consumer 端生效,一些擴(kuò)展類在 provider 端生效。

Dubbo SPI 加載擴(kuò)展的工作流程:


加載擴(kuò)展工作流程

主要步驟為 4 個(gè):

  • 讀取并解析配置文件。
  • 緩存所有擴(kuò)展實(shí)現(xiàn)。
  • 基于用戶執(zhí)行的擴(kuò)展名,實(shí)例化對應(yīng)的擴(kuò)展實(shí)現(xiàn)。

案例解析

在dubbo中,下面這種獲取擴(kuò)展類的方法很常見,通過指定接口類 ThreadPool.class 的實(shí)現(xiàn)類的別名 eager 即可獲取到對應(yīng)的實(shí)現(xiàn)類。

ThreadPool threadPool = ExtensionLoader.getExtensionLoader(ThreadPool.class).getExtension("eager")
//EagerThreadPool

相應(yīng)的配置文件在 META-INF/dubbo/internal/com.alibaba.dubbo.common.threadpool.ThreadPool ,內(nèi)容如下:

fixed=com.alibaba.dubbo.common.threadpool.support.fixed.FixedThreadPool
cached=com.alibaba.dubbo.common.threadpool.support.cached.CachedThreadPool
limited=com.alibaba.dubbo.common.threadpool.support.limited.LimitedThreadPool
eager=com.alibaba.dubbo.common.threadpool.support.eager.EagerThreadPool

來看與 Java SPI 配置文件的區(qū)別,配置文件都在 META-INF 文件夾下,但是具體路徑不同; Java SPI 配置文件的每一行只是實(shí)現(xiàn)類的全限定類名,Dubbo SPI 配置文件里全限定名前都有一個(gè)別名,可以通過別名獲取到該實(shí)現(xiàn)類。

來看 ThreadPool 接口,該接口有一個(gè) @SPI("fixed") 注解,注解的 value 是 **fixed **。表明該接口的默認(rèn)實(shí)現(xiàn)是別名為 fixed 的實(shí)現(xiàn)類,即 FixedThreadPool 。

@SPI("fixed")
public interface ThreadPool {
    @Adaptive({Constants.THREADPOOL_KEY})
    Executor getExecutor(URL url);
}

@SPI 注解:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
    /**
     * default extension name
     */
    String value() default "";
}

那么可以這樣獲取該類的默認(rèn)實(shí)現(xiàn)類:

ThreadPool defaultExtension = ExtensionLoader.getExtensionLoader(ThreadPool.class).getDefaultExtension();
//FixedThreadPool

源碼分析

Dubbo SPI 的關(guān)鍵類是 ExtensionLoader 。先看類的幾個(gè)重要靜態(tài)屬性,看完就能知道為什么上例中的配置文件為什么在 META-INF/dubbo/internal/ 中了。還有幾個(gè) ConcurrentHashMap 用于緩存數(shù)據(jù),后續(xù)的方法中都會用到。

public class ExtensionLoader<T> {
    private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

    private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
  
        private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
  
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>();
  
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();
}

通過 getExtensionLoader 方法獲取某個(gè)接口類型的 ExtensionLoader 對象時(shí),會判斷是否是 interface ,也會通過 withExtensionAnnotation 方法判斷該接口是否有 @SPI 注解,沒有的話會拋出異常,表明該接口不是一個(gè)可以擴(kuò)展的接口。然后從 EXTENSION_LOADERS 中獲取實(shí)例,沒有就實(shí)例化一個(gè),然后返回。

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null)
      throw new IllegalArgumentException("Extension type == null");
    if (!type.isInterface()) {
      throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
    }
    if (!withExtensionAnnotation(type)) {
      throw new IllegalArgumentException("Extension type(" + type +
                                         ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
    }

    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
      EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
      loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}
private static <T> boolean withExtensionAnnotation(Class<T> type) {
    return type.isAnnotationPresent(SPI.class);
}

有了 ExtensionLoader 實(shí)例就可以調(diào)用 getExtension 方法指定實(shí)現(xiàn)類的別名,來獲取該實(shí)現(xiàn)類的實(shí)例。

如果 "true".equals(name) 就返回該接口通過 @SPI 注解指定的默認(rèn)實(shí)現(xiàn)類。

判斷 cachedInstances 中是否有該實(shí)現(xiàn)類的緩存數(shù)據(jù),返回值是 Holder 對象,這個(gè)對象可以看作為一個(gè)數(shù)據(jù)承載對象,通過 holder.get() 可以獲取到對象里承載的數(shù)據(jù),這里就是接口實(shí)現(xiàn)類的實(shí)例化對象。如果 cachedInstances 中獲取不到 Holder 對象,就會調(diào)用 createExtension 方法獲取接口的具體實(shí)現(xiàn)類對象,放入承載對象中,然后就可以返回實(shí)現(xiàn)類的實(shí)例。(可以看到這里使用了常用的 double check方法)

public T getExtension(String name) {
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        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.set(instance);
                }
            }
        }
        return (T) instance;
    }

createExtension 方法中通過 getExtensionClasses().get(name) 方法獲取到別名為 name 的接口實(shí)現(xiàn)類 Class,然后通過 clazz.newInstance() 實(shí)例化返回。

private T createExtension(String name) {
    Class<?> clazz = getExtensionClasses().get(name);
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                                      type + ")  could not be instantiated: " + t.getMessage(), t);
    }
}

那么 getExtensionClasses().get(name) 方法如何獲取到別名指定的類呢?我們繼續(xù)追代碼會發(fā)現(xiàn)這樣的調(diào)用鏈:

getExtensionClasses() -> loadExtensionClasses() -> loadDirectory() -> loadResource() -> loadClass() 。

鑒于篇幅,筆者不一一貼出代碼,只拿重要的節(jié)點(diǎn)來描述:

在這整個(gè)調(diào)用鏈中會維護(hù)一個(gè) Map<String, Class<?>> extensionClasses,key 為實(shí)現(xiàn)類的別名, value 為該實(shí)現(xiàn)類。 getExtensionClasses().get(name) 就是從這個(gè) map 中獲取 name 別名的實(shí)現(xiàn)類。

loadDirectory() 會找到所有包中 META-INF/dubbo/internal/ 路徑下指定接口類名的文件。在上例中就是 META-INF/dubbo/internal/com.alibaba.dubbo.common.threadpool.ThreadPool 。

loadResource() 會解析每一個(gè)文件的內(nèi)容:

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
  ...
    while ((line = reader.readLine()) != 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(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
      } 
    }
  ...
}

代碼中可以看見讀到的每一行內(nèi)容按照 = 號分隔,前面是實(shí)現(xiàn)類的別名,后面是實(shí)現(xiàn)類的全限定類名。有了別名的全限定類名就可以通過 Class.forName(line, true, classLoader) 獲取 Class ,然后將別名與 Class 傳給 loadClass() 。

loadClass() 中會將別名與 Class 寫入到上文中提到的 extensionClasses 這個(gè) map 中,這樣 getExtensionClasses().get(name) 就能方便獲取了。

錦上添花

講到這里, Dubbo SPI 的主要流程應(yīng)該已經(jīng)講完了,但是 Dubbo SPI 中對 Java SPI 的增強(qiáng)還沒有提及,比如增加擴(kuò)展類的 IOC 能力;增加擴(kuò)展類的 AOP 能力等。這些在 ExtensionLoader 類中都有體現(xiàn),感興趣的同學(xué)可以查看代碼,相信你一定能看到這些具體的實(shí)現(xiàn)。

實(shí)戰(zhàn)

如何將Dubbo SPI引入項(xiàng)目

了解了 Dubbo SPI 的實(shí)現(xiàn)原理,那怎么在我們的項(xiàng)目中使用 Dubbo SPI 呢?現(xiàn)在我們在一個(gè)現(xiàn)有使用 Java SPI 的項(xiàng)目中引入 Dubbo SPI ,通過這個(gè)實(shí)踐讓你更深入了解 Dubbo SPI 的原理。

這個(gè)項(xiàng)目是一個(gè)簡單 RPC 項(xiàng)目,原本用來序列化、和解壓縮的接口實(shí)現(xiàn)類都是通過 Java SPI 來加載到項(xiàng)目中的:

cn.ppphuang.rpcspringstarter.common.protocol.JavaSerializeMessageProtocol
cn.ppphuang.rpcspringstarter.common.protocol.KryoMessageProtocol
cn.ppphuang.rpcspringstarter.common.protocol.ProtoBufSerializeMessageProtocol
public static Map<String, MessageProtocol> buildSupportMessageProtocol() {
    HashMap<String, MessageProtocol> supportMessageProtocol = new HashMap<>();
    ServiceLoader<MessageProtocol> loader = ServiceLoader.load(MessageProtocol.class);
    for (MessageProtocol messageProtocol : loader) {
        MessageProtocolAno annotation = messageProtocol.getClass().getAnnotation(MessageProtocolAno.class);
        Assert.notNull(annotation, "message protocol name can not be empty!");
        supportMessageProtocol.put(annotation.value(), messageProtocol);
    }
    return supportMessageProtocol;
}

必須通過 ServiceLoader.load(MessageProtocol.class) 獲取所有的接口實(shí)現(xiàn)類,然后放入到 map 中以供后續(xù)取用,不能指定實(shí)例化某一個(gè)實(shí)現(xiàn)類。因?yàn)槲覀冃蛄谢蛘呓鈮嚎s實(shí)現(xiàn)類的選擇都是通過項(xiàng)目的啟動配置文件來決定的,項(xiàng)目啟動時(shí)只會選擇配置中指定的這個(gè)實(shí)現(xiàn)類,所以加載并實(shí)例化所有的實(shí)現(xiàn)類就會浪費(fèi)資源。

我們來用 Dubbo SPI 替換 Java SPI :

創(chuàng)建 @SPI 注解,這里因?yàn)槲覀兺ㄟ^配置文件決定默認(rèn)實(shí)現(xiàn)類,所有注解沒有 value 值:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SPI {
}

創(chuàng)建 Holder 對象承載類:

public class Holder<T> {
    private volatile T value;
    public T get() {
        return value;
    }
    public void set(T value) {
        this.value = value;
    }
}

創(chuàng)建 ExtensionLoader 類,代碼較長建議查看附錄鏈接中的源代碼:

public class ExtensionLoader<T> {
    private static final String SERVICES_DIRECTORY = "META-INF/services/";
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {}
    public T getExtension(String name) {}
    private T createExtension(String name) {}
    private Map<String, Class<?>> getExtensionClasses() {}
    private Map<String, Class<?>> loadExtensionClasses() {}
    private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {}
    private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {
      final int ei = line.indexOf('=');    
      //配置文件內(nèi)容格式兼容 Java SPI
      if (ei > 0) {
        String name = line.substring(0, ei).trim();
        String clazzName = line.substring(ei + 1).trim();
        if (name.length() > 0 && clazzName.length() > 0) {
          Class<?> clazz = classLoader.loadClass(clazzName);
          extensionClasses.put(name, clazz);
        }
      } else {
        Class<?> clazz = classLoader.loadClass(line);
        //使用類注解中的指定別名
        SPIExtension annotation = clazz.getAnnotation(SPIExtension.class);
        String name = annotation.value();
        extensionClasses.put(name, clazz);
      }
    }
}

類中的主要流程與 Dubbo SPI 中基本類似,刪減了一些不需要的增強(qiáng)功能,主要實(shí)現(xiàn)類的選擇性加載。同時(shí)也加入了自己的一些修改:

  1. 配置文件路徑兼容 Java SPI ,也放到 META-INF/services/ 文件夾下。
  2. 配置文件內(nèi)容格式兼容 Java SPI ,通過 loadResource 方法中的改動來實(shí)現(xiàn) 。
    1. SPI 文件的格式為 xxxx 時(shí),按照實(shí)現(xiàn)類中 @SPIExtension 注解的 value 名稱作為別名。com.alibaba.dubbo.common.compiler.support.JdkCompiler。
    2. SPI 文件的格式為 xxx=xxxx 時(shí),xxx 為別名。jdk=com.alibaba.dubbo.common.compiler.support.JdkCompiler。

有了這樣的兼容處理,不需要改動配置文件就可以直接替換 Java SPI 。

@SPIExtension 注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SPIExtension {
    String value();
}
@SPI
public interface MessageProtocol {
  //表明該接口是支持 SPI 擴(kuò)展的接口
}
@SPIExtension("kryo")
public class KryoMessageProtocol implements MessageProtocol {
  //表明該實(shí)現(xiàn)類的默認(rèn)別名是 kryo
  //配置文件中可以使用 = 號設(shè)置新別名來覆蓋該別名
}
@SPIExtension("protobuf")
public class ProtoBufSerializeMessageProtocol implements MessageProtocol {
  //表明該實(shí)現(xiàn)類的默認(rèn)別名是 protobuf
  //配置文件中可以使用 = 號設(shè)置新別名來覆蓋該別名
}

這樣就可以使用 Dubbo SPI 加載 Java SPI 機(jī)制下的類,項(xiàng)目中的實(shí)現(xiàn)類按需加載,不需要像 Java SPI 那樣遍歷實(shí)例化的所有對象了:

MessageProtocol protocol = ExtensionLoader.getExtensionLoader(MessageProtocol.class).getExtension("kryo");
MessageProtocol protocol = ExtensionLoader.getExtensionLoader(MessageProtocol.class).getExtension("protobuf");

整個(gè)替換代碼比較簡單,容易看懂,而且項(xiàng)目中也保留了其他接口 Java SPI 擴(kuò)展的方式,可以對照項(xiàng)目中已經(jīng)替換的 Dubbo SPI 擴(kuò)展加載方式來閱讀理解。

https://github.com/PPPHUANG/rpc-spring-starter

參考

https://dubbo.apache.org/zh/docs/concepts/extensibility/

https://github.com/apache/dubbo

一位后端寫碼師,一位黑暗料理制造者。公眾號:DailyHappy

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容