溫習(xí) SPI 機(jī)制 (Java SPI 、Spring SPI、Dubbo SPI)
SPI 全稱為 Service Provider Interface,是一種服務(wù)發(fā)現(xiàn)機(jī)制。
SPI 的本質(zhì)是將接口實(shí)現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類。這樣可以在運(yùn)行時(shí),動(dòng)態(tài)為接口替換實(shí)現(xiàn)類。正因此特性,我們可以很容易的通過(guò) SPI 機(jī)制為我們的程序提供拓展功能。
圖片
1 Java SPI 示例
本節(jié)通過(guò)一個(gè)示例演示 Java SPI 的使用方法。首先,我們定義一個(gè)接口,名稱為 Robot。
public interface Robot {
    void sayHello();
}接下來(lái)定義兩個(gè)實(shí)現(xiàn)類,分別為 OptimusPrime 和 Bumblebee。
public class OptimusPrime implements Robot {
    
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
  
}
public class Bumblebee implements Robot {
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
  
}接下來(lái) META-INF/services 文件夾下創(chuàng)建一個(gè)文件,名稱為 Robot 的全限定名 org.apache.spi.Robot。文件內(nèi)容為實(shí)現(xiàn)類的全限定的類名,如下:
org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee做好所需的準(zhǔn)備工作,接下來(lái)編寫(xiě)代碼進(jìn)行測(cè)試。
public class JavaSPITest {
    @Test
    public void sayHello() throws Exception {
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
        System.out.println("Java SPI");
        // 1. forEach 模式
        serviceLoader.forEach(Robot::sayHello);
        // 2. 迭代器模式
        Iterator<Robot> iterator = serviceLoader.iterator();
        while (iterator.hasNext()) {
            Robot robot = iterator.next();
          //System.out.println(robot);
          //robot.sayHello();
        }
    }
}最后來(lái)看一下測(cè)試結(jié)果,如下 :
圖片
2 經(jīng)典 Java SPI 應(yīng)用 : JDBC DriverManager
在JDBC4.0 之前,我們開(kāi)發(fā)有連接數(shù)據(jù)庫(kù)的時(shí)候,通常先加載數(shù)據(jù)庫(kù)相關(guān)的驅(qū)動(dòng),然后再進(jìn)行獲取連接等的操作。
// STEP 1: Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
// STEP 2: Open a connection
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);JDBC4.0之后使用了 Java 的 SPI 擴(kuò)展機(jī)制,不再需要用 Class.forName("com.mysql.jdbc.Driver") 來(lái)加載驅(qū)動(dòng),直接就可以獲取 JDBC 連接。
接下來(lái),我們來(lái)看看應(yīng)用如何加載 MySQL JDBC 8.0.22 驅(qū)動(dòng):
圖片
首先 DriverManager類是驅(qū)動(dòng)管理器,也是驅(qū)動(dòng)加載的入口。
/**
 * Load the initial JDBC drivers by checking the System property
 * jdbc.properties and then use the {@code ServiceLoader} mechanism
 */
static {
     loadInitialDrivers();
     println("JDBC DriverManager initialized");
}在 Java 中,static 塊用于靜態(tài)初始化,它在類被加載到 Java 虛擬機(jī)中時(shí)執(zhí)行。
靜態(tài)塊會(huì)加載實(shí)例化驅(qū)動(dòng),接下來(lái)我們看看loadInitialDrivers 方法。
圖片
加載驅(qū)動(dòng)代碼包含四個(gè)步驟:
- 系統(tǒng)變量中獲取有關(guān)驅(qū)動(dòng)的定義。
 - 使用 SPI 來(lái)獲取驅(qū)動(dòng)的實(shí)現(xiàn)類(字符串的形式)。
 - 遍歷使用 SPI 獲取到的具體實(shí)現(xiàn),實(shí)例化各個(gè)實(shí)現(xiàn)類。
 - 根據(jù)第一步獲取到的驅(qū)動(dòng)列表來(lái)實(shí)例化具體實(shí)現(xiàn)類。
 
我們重點(diǎn)關(guān)注 SPI 的用法,首先看第二步,使用 SPI 來(lái)獲取驅(qū)動(dòng)的實(shí)現(xiàn)類 , 對(duì)應(yīng)的代碼是:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);這里沒(méi)有去 META-INF/services目錄下查找配置文件,也沒(méi)有加載具體實(shí)現(xiàn)類,做的事情就是封裝了我們的接口類型和類加載器,并初始化了一個(gè)迭代器。
接著看第三步,遍歷使用SPI獲取到的具體實(shí)現(xiàn),實(shí)例化各個(gè)實(shí)現(xiàn)類,對(duì)應(yīng)的代碼如下:
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍歷所有的驅(qū)動(dòng)實(shí)現(xiàn)
while(driversIterator.hasNext()) {
    driversIterator.next();
}在遍歷的時(shí)候,首先調(diào)用driversIterator.hasNext()方法,這里會(huì)搜索 classpath 下以及 jar 包中所有的META-INF/services目錄下的java.sql.Driver文件,并找到文件中的實(shí)現(xiàn)類的名字,此時(shí)并沒(méi)有實(shí)例化具體的實(shí)現(xiàn)類。
然后是調(diào)用driversIterator.next()方法,此時(shí)就會(huì)根據(jù)驅(qū)動(dòng)名字具體實(shí)例化各個(gè)實(shí)現(xiàn)類了,現(xiàn)在驅(qū)動(dòng)就被找到并實(shí)例化了。
3 Java SPI 機(jī)制源碼解析
我們根據(jù)第一節(jié) JDK SPI 示例,學(xué)習(xí) ServiceLoader 類的實(shí)現(xiàn)。
ServiceLoader類
進(jìn)入 ServiceLoader 類的load方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
     ClassLoader cl = Thread.currentThread().getContextClassLoader();
     return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service , ClassLoader loader) {
     return new ServiceLoader<>(service, loader);
}上面的代碼,load 方法會(huì)通過(guò)傳遞的服務(wù)類型和類加載器classLoader 創(chuàng)建一個(gè) ServiceLoader 對(duì)象。
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();
}
// 緩存已經(jīng)被實(shí)例化的服務(wù)提供者,按照實(shí)例化的順序存儲(chǔ)
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public void reload() {
     providers.clear();
     lookupIterator = new LazyIterator(service, loader);
}私有構(gòu)造器會(huì)創(chuàng)建懶迭代器 LazyIterator 對(duì)象 ,所謂懶迭代器,就是對(duì)象初始化時(shí),僅僅是初始化,只有在真正調(diào)用迭代方法時(shí),才執(zhí)行加載邏輯。
示例代碼中創(chuàng)建完 serviceLoader 之后,接著調(diào)用iterator()方法:
Iterator<Robot> iterator = serviceLoader.iterator();
// 迭代方法實(shí)現(xiàn)
public Iterator<S> iterator() {
     return new Iterator<S>() {
          Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
          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();
          }
     };
}迭代方法的實(shí)現(xiàn)本質(zhì)是調(diào)用懶迭代器 lookupIterator 的 hasNext() 和 next() 方法。
1)hasNext() 方法
public boolean hasNext() {
      if (acc == null) {
           return hasNextService();
      } else {
           PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                 public Boolean run() { return hasNextService(); }
           };
           return AccessController.doPrivileged(action, acc);
      }
}public S next() {
       if (acc == null) {
            return nextService();
       } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                 public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
       }
}懶迭代器的hasNextService方法首先會(huì)通過(guò)加載器通過(guò)文件全名獲取配置對(duì)象 Enumeration<URL> configs ,然后調(diào)用解析parse方法解析classpath下的META-INF/services/目錄里以服務(wù)接口命名的文件。
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;
}當(dāng) hasNextService 方法返回 true , 我們可以調(diào)用迭代器的 next 方法 ,本質(zhì)是調(diào)用懶加載器 lookupIterator 的 next() 方法:
2)next() 方法
Robot robot = iterator.next();
// 調(diào)用懶加載器 lookupIterator 的  `next()` 方法
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 subtype");
          }
          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
  }通過(guò)反射方法 Class.forName() 加載類對(duì)象,并用newInstance方法將類實(shí)例化,并把實(shí)例化后的類緩存到providers對(duì)象中,(LinkedHashMap<String,S>類型,然后返回實(shí)例對(duì)象。
4 Java SPI 機(jī)制的缺陷
通過(guò)上面的解析,可以發(fā)現(xiàn),我們使用 JDK SPI 機(jī)制的缺陷 :
- 不能按需加載,需要遍歷所有的實(shí)現(xiàn),并實(shí)例化,然后在循環(huán)中才能找到我們需要的實(shí)現(xiàn)。如果不想用某些實(shí)現(xiàn)類,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了,這就造成了浪費(fèi)。
 - 獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過(guò) Iterator 形式獲取,不能根據(jù)某個(gè)參數(shù)來(lái)獲取對(duì)應(yīng)的實(shí)現(xiàn)類。
 - 多個(gè)并發(fā)多線程使用 ServiceLoader 類的實(shí)例是不安全的。
 
5 Spring SPI 機(jī)制
Spring SPI 沿用了 Java SPI 的設(shè)計(jì)思想,Spring 采用的是 spring.factories 方式實(shí)現(xiàn) SPI 機(jī)制,可以在不修改 Spring 源碼的前提下,提供 Spring 框架的擴(kuò)展性。
圖片
1)創(chuàng)建 MyTestService 接口
public interface MyTestService {
    void printMylife();
}2)創(chuàng)建 MyTestService 接口實(shí)現(xiàn)類
- WorkTestService :
 
public class WorkTestService implements MyTestService {
    public WorkTestService(){
        System.out.println("WorkTestService");
    }
    public void printMylife() {
        System.out.println("我的工作");
    }
}- FamilyTestService :
 
public class FamilyTestService implements MyTestService {
    public FamilyTestService(){
        System.out.println("FamilyTestService");
    }
    public void printMylife() {
        System.out.println("我的家庭");
    }
}3)在資源文件目錄,創(chuàng)建一個(gè)固定的文件 META-INF/spring.factories。
#key是接口的全限定名,value是接口的實(shí)現(xiàn)類
com.courage.platform.sms.demo.service.MyTestService = com.courage.platform.sms.demo.service.impl.FamilyTestService,com.courage.platform.sms.demo.service.impl.WorkTestService4)運(yùn)行代碼
// 調(diào)用 SpringFactoriesLoader.loadFactories 方法加載 MyTestService 接口所有實(shí)現(xiàn)類的實(shí)例
List<MyTestService> myTestServices = SpringFactoriesLoader.loadFactories(
            MyTestService.class,
            Thread.currentThread().getContextClassLoader()
);
for (MyTestService testService : myTestServices) {
     testService.printMylife();
}運(yùn)行結(jié)果:
FamilyTestService
WorkTestService
我的家庭
我的工作Spring SPI 機(jī)制非常類似 ,但還是有一些差異:
- Java SPI 是一個(gè)服務(wù)提供接口對(duì)應(yīng)一個(gè)配置文件,配置文件中存放當(dāng)前接口的所有實(shí)現(xiàn)類,多個(gè)服務(wù)提供接口對(duì)應(yīng)多個(gè)配置文件,所有配置都在 services 目錄下。
 - Spring SPI 是一個(gè) spring.factories 配置文件存放多個(gè)接口及對(duì)應(yīng)的實(shí)現(xiàn)類,以接口全限定名作為key,實(shí)現(xiàn)類作為value來(lái)配置,多個(gè)實(shí)現(xiàn)類用逗號(hào)隔開(kāi),僅 spring.factories 一個(gè)配置文件。
 
和 Java SPI 一樣,Spring SPI 也無(wú)法獲取某個(gè)固定的實(shí)現(xiàn),只能按順序獲取所有實(shí)現(xiàn)。
6 Dubbo SPI 機(jī)制
基于 Java SPI 的缺陷無(wú)法支持按需加載接口實(shí)現(xiàn)類,Dubbo 并未使用 Java SPI,而是重新實(shí)現(xiàn)了一套功能更強(qiáng)的 SPI 機(jī)制。
Dubbo SPI 的相關(guān)邏輯被封裝在了 ExtensionLoader 類中,通過(guò) ExtensionLoader,我們可以加載指定的實(shí)現(xiàn)類。
Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內(nèi)容如下:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee與 Java SPI 實(shí)現(xiàn)類配置不同,Dubbo SPI 是通過(guò)鍵值對(duì)的方式進(jìn)行配置,這樣我們可以按需加載指定的實(shí)現(xiàn)類。
另外,在測(cè)試 Dubbo SPI 時(shí),需要在 Robot 接口上標(biāo)注 @SPI 注解。
下面來(lái)演示 Dubbo SPI 的用法:
public class DubboSPITest {
    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}測(cè)試結(jié)果如下 :
圖片
另外,Dubbo SPI 除了支持按需加載接口實(shí)現(xiàn)類,還增加了 IOC 和 AOP 等特性 。















 
 
 














 
 
 
 