JAVA的SPI機制
1.什麼是SPI
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴充套件的介面,它可以用來啟用框架擴充套件和替換元件。SPI的作用就是為這些被擴充套件的API尋找服務實現。
3.SPI的簡單實現
下面我們來簡單實現一個jdk的SPI的簡單實現。
首先第一步,定義一組介面:
1 public interface UploadCDN { 2 void upload(String url); 3 }
這個介面分別有兩個實現:
1 public class QiyiCDN implements UploadCDN { //上傳愛奇藝cdn 2 @Override 3 public void upload(String url) { 4 System.out.println("upload to qiyi cdn"); 5 } 6 } 7 8 public class ChinaNetCDN implements UploadCDN {//上傳網宿cdn 9 @Override 10 public void upload(String url) { 11 System.out.println("upload to chinaNet cdn"); 12 } 13 }
然後需要在resources目錄下新建META-INF/services目錄,並且在這個目錄下新建一個與上述介面的全限定名一致的檔案,在這個檔案中寫入介面的實現類的全限定名:
這時,通過serviceLoader載入實現類並呼叫:
1 public static void main(String[] args) { 2 ServiceLoader<UploadCDN> uploadCDN = ServiceLoader.load(UploadCDN.class); 3 for (UploadCDN u : uploadCDN) { 4 u.upload("filePath"); 5 } 6 }
輸出如下:
這樣一個簡單的spi的demo就完成了。可以看到其中最為核心的就是通過ServiceLoader這個類來載入具體的實現類的。
4. SPI原理解析
通過上面簡單的demo,可以看到最關鍵的實現就是ServiceLoader這個類,可以看下這個類的原始碼,如下:
1 public final class ServiceLoader<S> implements Iterable<S> { 2 3 4 //掃描目錄字首 5 private static final String PREFIX = "META-INF/services/"; 6 7 // 被載入的類或介面 8 private final Class<S> service; 9 10 // 用於定位、載入和例項化實現方實現的類的類載入器 11 private final ClassLoader loader; 12 13 // 上下文物件 14 private final AccessControlContext acc; 15 16 // 按照例項化的順序快取已經例項化的類 17 private LinkedHashMap<String, S> providers = new LinkedHashMap<>(); 18 19 // 懶查詢迭代器 20 private java.util.ServiceLoader.LazyIterator lookupIterator; 21 22 // 私有內部類,提供對所有的service的類的載入與例項化 23 private class LazyIterator implements Iterator<S> { 24 Class<S> service; 25 ClassLoader loader; 26 Enumeration<URL> configs = null; 27 String nextName = null; 28 29 //... 30 private boolean hasNextService() { 31 if (configs == null) { 32 try { 33 //獲取目錄下所有的類 34 String fullName = PREFIX + service.getName(); 35 if (loader == null) 36 configs = ClassLoader.getSystemResources(fullName); 37 else 38 configs = loader.getResources(fullName); 39 } catch (IOException x) { 40 //... 41 } 42 //.... 43 } 44 } 45 46 private S nextService() { 47 String cn = nextName; 48 nextName = null; 49 Class<?> c = null; 50 try { 51 //反射載入類 52 c = Class.forName(cn, false, loader); 53 } catch (ClassNotFoundException x) { 54 } 55 try { 56 //例項化 57 S p = service.cast(c.newInstance()); 58 //放進快取 59 providers.put(cn, p); 60 return p; 61 } catch (Throwable x) { 62 //.. 63 } 64 //.. 65 } 66 } 67 }
上面的程式碼只貼出了部分關鍵的實現,有興趣的讀者可以自己去研究,下面貼出比較直觀的spi載入的主要流程供參考:
Spring Boot的擴充套件機制之Spring Factories
Spring Boot中的SPI機制
在Spring中也有一種類似與Java SPI的載入機制。它在META-INF/spring.factories檔案中配置介面的實現類名稱,然後在程式中讀取這些配置檔案並例項化。
這種自定義的SPI機制是Spring Boot Starter實現的基礎。
Spring Factories實現原理
spring-core包裡定義了SpringFactoriesLoader類,這個類實現了檢索META-INF/spring.factories檔案,並獲取指定介面的配置的功能。在這個類中定義了兩個對外的方法:
loadFactories 根據介面類獲取其實現類的例項,這個方法返回的是物件列表。
loadFactoryNames 根據介面獲取其介面類的名稱,這個方法返回的是類名的列表。
上面的兩個方法的關鍵都是從指定的ClassLoader中獲取spring.factories檔案,並解析得到類名列表,具體程式碼如下
private static Map<String, List<String>> loadSpringFactories(
從程式碼中我們可以知道,在這個方法中會遍歷整個ClassLoader中所有jar包下的spring.factories檔案。也就是說我們可以在自己的jar中配置spring.factories檔案,不會影響到其它地方的配置,也不會被別人的配置覆蓋。
spring.factories的是通過Properties解析得到的,所以我們在寫檔案中的內容都是安裝下面這種方式配置的:
com.xxx.interface=com.xxx.classname
如果一個介面希望配置多個實現類,可以使用’,’進行分割