Java SPI詳解


1.什么是SPI

     SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的接口,它可以用來啟用框架擴展和替換組件。 SPI的作用就是為這些被擴展的API尋找服務實現。

2.SPI和API的使用場景

    API (Application Programming Interface)在大多數情況下,都是實現方制定接口並完成對接口的實現調用方僅僅依賴接口調用,且無權選擇不同實現。 從使用人員上來說,API 直接被應用開發人員使用。

    SPI (Service Provider Interface)調用方來制定接口規范,提供給外部來實現,調用方在調用時則選擇自己需要的外部實現。  從使用人員上來說,SPI 被框架擴展人員使用。

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加載的主要流程供參考:

 

5.dubbo SPI

    dubbo作為一個高度可擴展的rpc框架,也依賴於java的spi,並且dubbo對java原生的spi機制作出了一定的擴展,使得其功能更加強大。

首先,從上面的java spi的原理中可以了解到,java的spi機制有着如下的弊端:

  • 只能遍歷所有的實現,並全部實例化。
  • 配置文件中只是簡單的列出了所有的擴展實現,而沒有給他們命名。導致在程序中很難去准確的引用它們。
  • 擴展如果依賴其他的擴展,做不到自動注入和裝配。
  • 擴展很難和其他的框架集成,比如擴展里面依賴了一個Spring bean,原生的Java SPI不支持。

    dubbo的spi有如下幾個概念:

    (1)擴展點:一個接口。

    (2)擴展:擴展(接口)的實現。

    (3)擴展自適應實例:其實就是一個Extension的代理,它實現了擴展點接口。在調用擴展點的接口方法時,會根據實際的參數來決定要使用哪個擴展。dubbo會根據接口中的參數,自動地決定選擇哪個實現。

    (4)@SPI:該注解作用於擴展點的接口上,表明該接口是一個擴展點。

    (5)@Adaptive:@Adaptive注解用在擴展接口的方法上。表示該方法是一個自適應方法。Dubbo在為擴展點生成自適應實例時,如果方法有@Adaptive注解,會為該方法生成對應的代碼。

    dubbo的spi也會從某些固定的路徑下去加載配置文件,並且配置的格式與java原生的不一樣,類似於property文件的格式:

     下面將基於dubbo去實現一個簡單的擴展實現。首先,要實現LoadBalance這個接口,當然這個接口是被注解標注的可以擴展的:

 1 @SPI("random")
 2 public interface LoadBalance {
 3     @Adaptive({"loadbalance"})
 4     <T> Invoker<T> select(List<Invoker<T>> var1, URL var2, Invocation var3) throws RpcException;
 5 }
 6 
 7 public class DemoLoadBalance implements LoadBalance {
 8 
 9     @Override
10     public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
11         System.out.println("my demo loadBalance is used, hahahahh");
12         return invokers.get(0);//選擇第一個
13     }
14 }

     然后,需要在duboo SPI的掃描目錄下,添加配置文件,注意配置文件的名稱要和擴展點的接口名稱對應起來:

     還需要在dubbo的spring配置中顯式的聲明,使用上面自己實現的負載均衡策略:

1  <dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" />

    然后,啟動dubbo,調用service,就可以發現確實是使用了自定義的負載策略:

     至此,dubbo的spi的demo也完成了。

    dubbo spi的原理和jdk的實現稍有不同,大概流程如下圖,具體的實現讀者可以自己了解下源碼。

6.總結

    關於spi的詳解到此就結束了,總結下spi能帶來的好處:

  • 不需要改動源碼就可以實現擴展,解耦。
  • 實現擴展對原來的代碼幾乎沒有侵入性。
  • 只需要添加配置就可以實現擴展,符合開閉原則。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM