­

Java-深入理解ServiceLoader類與SPI機制

  • 2020 年 2 月 18 日
  • 筆記

Java-ServiceLoader類與SPI機制

引子

對於Java中的Service類和SPI機制的透徹理解,也算是對Java類載入模型的掌握的不錯的一個反映。

了解一個不太熟悉的類,那麼從使用案例出發,讀懂源程式碼以及程式碼內部執行邏輯是一個不錯的學習方式。


一、使用案例

通常情況下,使用ServiceLoader來實現SPI機制。 SPI 全稱為 (Service Provider Interface) ,是JDK內置的一種服務提供發現機制。SPI是一種動態替換髮現的機制, 比如有個介面,想運行時動態的給它添加實現,你只需要添加一個實現。

SPI機制可以歸納為如下的圖:

起始這樣說起來還是比較抽象,那麼下面舉一個具體的例子,案例為JDBC的調用例子:

案例如下:

JDBC中的介面即為:java.sql.Driver

SPI機制的實現核心類為:java.util.ServiceLoader

Provider則為:com.mysql.jdbc.Driver

外層調用則是我們進行增刪改查JDBC操作所在的程式碼塊,但是對於那些現在還沒有學過JDBC的小夥伴來說(不難學~),這可能會有點難理理解,所以我這裡就舉一個使用案例:

按照上圖的SPI執行邏輯,我們需要寫一個介面、至少一個介面的實現類、以及外層調用的測試類。

但是要求以這樣的目錄書結構來定義項目文件,否則SPI機制無法實現(類載入機制相關,之後會講):

E:.  │  MyTest.java  │  ├─com  │  └─fisherman  │      └─spi  │          │  HelloInterface.java  │          │  │          └─impl  │                  HelloJava.java  │                  HelloWorld.java  │  └─META-INF      └─services              com.fisherman.spi.HelloInterface

其中:

  1. MyTest.java為測試java文件,負責外層調用;
  2. HelloInterface.java為介面文件,等待其他類將其實現;
  3. HelloJava.java 以及 HelloWorld.java 為介面的實現類;
  4. META-INF └─services com.fisherman.spi.HelloInterface 為配置文件,負責類載入過程中的路徑值。

首先給出介面的邏輯:

public interface HelloInterface {      void sayHello();  }

其次,兩個實現類的程式碼:

public class HelloJava implements HelloInterface {      @Override      public void sayHello() {          System.out.println("HelloJava.");      }  }
public class HelloWorld implements HelloInterface {      @Override      public void sayHello() {          System.out.println("HelloWorld.");      }  }

然後,配置文件:com.fisherman.spi.HelloInterface

com.fisherman.spi.impl.HelloWorld  com.fisherman.spi.impl.HelloJava

最後測試文件:

public class MyTest26 {        public static void main(String[] args) {            ServiceLoader<HelloInterface> loaders = ServiceLoader.load(HelloInterface.class);            for (HelloInterface in : loaders) {              in.sayHello();          }        }    }

測試文件運行後的控制台輸出:

HelloWorld.  HelloJava.

我們從控制台的列印資訊可知我們成功地實現了SPI機制,通過 ServiceLoader 類實現了等待實現的介面和實現其介面的類之間的聯繫。

下面我們來深入探討以下,SPI機制的內部實現邏輯。


二、ServiceLoader類的內部實現邏輯

Service類的構造方法是私有的,所以我們只能通過掉用靜態方法的方式來返回一個ServiceLoader的實例:

方法的參數為被實現結構的Class對象。

ServiceLoader<HelloInterface> loaders = ServiceLoader.load(HelloInterface.class); 

其內部實現邏輯如所示,不妨按調用步驟來分步講述:

1.上述load方法的源程式碼:

public static <S> ServiceLoader<S> load(Class<S> service) {      ClassLoader cl = Thread.currentThread().getContextClassLoader();      return ServiceLoader.load(service, cl);  }

完成的工作:

  1. 得到當前執行緒的上下文載入器,用於後續載入實現了介面的類
  2. 調用另一個load方法的重載版本(多了一個類載入器的引用參數)

2.被調用的另一個load重載方法的源程式碼:

    public static <S> ServiceLoader<S> load(Class<S> service,                                              ClassLoader loader)      {          return new ServiceLoader<>(service, loader);      }

完成的工作:

  • 調用了類ServiceLoader的私有構造器

3.私有構造器的源程式碼:

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();  }

完成的工作:

  1. 空指針和安全性的一些判斷以及處理;
  2. 並對兩個重要要的私有實例變數進行了賦值: private final Class<S> service; private final ClassLoader loader;
  3. reload()方法來迭代器的清空並重新賦值

SercviceLoader的初始化跑完如上程式碼就結束了。但是實際上聯繫待實現介面和實現介面的類之間的關係並不只是在構造ServiceLoader類的過程中完成的,而是在迭代器的方法hasNext()中實現的。

這個聯繫通過動態調用的方式實現,其程式碼分析就見下一節吧:


三、動態調用的實現

在使用案例中寫的forEach語句內部邏輯就是迭代器,迭代器的重要方法就是hasNext()

ServiceLoader是一個實現了介面Iterable介面的類。

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);      }  }

拋出複雜的確保安全的操作,可以將上述程式碼看作就是調用了方法:hasNextService.

hasNextService()方法的源程式碼:

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;  }

上述程式碼中比較重要的程式碼塊是:

String fullName = PREFIX + service.getName();              if (loader == null)                  configs = ClassLoader.getSystemResources(fullName);

此處PREFIX(前綴)是一個常量字元串(用於規定配置文件放置的目錄,使用相對路徑,說明其上層目錄為以項目名為名的文件夾):

private static final String PREFIX = "META-INF/services/";

那麼fullName會被賦值為:"META-INF/services/com.fisherman.spi.HelloInterface"

然後調用方法getSystemResourcesgetResources將fullName參數視作為URL,返回配置文件的URL集合 。

pending = parse(service, configs.nextElement());

parse方法是憑藉 參數1:介面的Class對象 和 參數2:配置文件的URL來解析配置文件,返回值是含有配置文件裡面的內容,也就是實現類的全名(包名+類名)字元串的迭代器;

最後調用下面的程式碼,得到下面要載入的類的完成類路徑字元串,相對路徑。在使用案例中,此值就可以為:

com.fisherman.spi.impl.HelloWorldcom.fisherman.spi.impl.HelloJava

nextName = pending.next();

這僅僅是迭代器判斷是否還有下一個迭代元素的方法,而獲取每輪迭代元素的方法為:nextService()方法。

nextService()方法源碼:

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  }

拋出一些負責安全以及處理異常的程式碼,核心程式碼為:

1.得到介面實現類的完整類路徑字元串:

String cn = nextName;

2使用loader引用的類載入器來載入cn指向的介面實現類,並返回其Class對象(但是不初始化此類):

c = Class.forName(cn, false, loader);

3.調用Class對象的newInstance()方法來調用無參構造方法,返回Provider實例:

S p = service.cast(c.newInstance());
//cast方法只是在null和類型檢測通過的情況下進行了簡單的強制類型轉換  public T cast(Object obj) {      if (obj != null && !isInstance(obj))          throw new ClassCastException(cannotCastMsg(obj));      return (T) obj;  }

4.將Provider實例放置於providers指向的HashMap中:

providers.put(cn, p);

5.返回provider實例:

return p;

ServiceLoader類的小總結:

  1. 利用創建ServiceLoader類的執行緒對象得到上下文類載入器,然後將此載入器用於載入provider類;
  2. 利用反射機制來得到provider的類對象,再通過類對象的newInstance方法得到provider的實例;
  3. ServiceLoader負責provider類載入的過程數據類的動態載入;
  4. provider類的相對路徑保存於配置文件中,需要完整的包名,如:com.fisherman.spi.impl.HelloWorld

四、總結與評價

  1. SPI的理念:通過動態載入機制實現面向介面編程,提高了框架和底層實現的分離;
  2. ServiceLoader 類提供的 SPI 實現方法只能通過遍歷迭代的方法實現獲得Provider的實例對象,如果要註冊了多個介面的實現類,那麼顯得效率不高;
  3. 雖然通過靜態方法返回,但是每一次Service.load方法的調用都會產生一個ServiceLoader實例,不屬於單例設計模式;
  4. ServiceLoader與ClassLoader是類似的,都可以負責一定的類載入工作,但是前者只是單純地載入特定的類,即要求實現了Service介面的特定實現類;而後者幾乎是可以載入所有Java類;
  5. 對於SPi機制的理解有兩個要點:
    1. 理解動態載入的過程,知道配置文件是如何被利用,最終找到相關路徑下的類文件,並載入的;
    2. 理解 SPI 的設計模式:介面框架 和底層實現程式碼分離
  6. 之所以將ServiceLoader類內部的迭代器對象稱為LazyInterator,是因為在ServiceLoader對象創建完畢時,迭代器內部並沒有相關元素引用,只有真正迭代的時候,才會去解析、載入、最終返回相關類(迭代的元素);

五、相關引用

ServiceLoader使用及原理分析

Create Extensible Applications using Java ServiceLoader