SPI的全名為Service Provider Interface.普通開發人員可能不熟悉,因為這個是針對廠商或者外掛程式的。在java.util.ServiceLoader的文檔裡有比較詳細的介紹。究其思想,其實是和"Callback"差不多。“Callback”的思想是在我們調用API的時候,我們可以自己寫一段邏輯代碼,傳入到API裡面,API內部在合適的時候會調用它,從而實現某種程度的“定製”。
典型的是Collections.sort(List<T> list,Comparator<? super T> c)這個方法,它的第二個參數是一個實現Comparator介面的執行個體。我們可以根據自己的定序寫一個類,實現此介面,傳入此方法,那麼這個方法就會根據我們的規則對list進行排序。
把這個思想擴充開來,我們用SPI來重新實現上面的例子。客戶把自己的定序寫成一個類,並且打包成Jar檔案,這個Jar檔案裡面必須有META-INF目錄,其下又有services目錄,其下有一個文字檔,檔案名稱即為介面的全名:java.util.Comparator。
--META-INF
--services
--java.util.Comparator
檔案內容只有一行:
com.company1.ComparatorProvider
這一行是你實現了Comparator介面的類的全名,它的代碼如下:
package com.company1;import java.util.Comparator;import com.mycompany.myapp.MyItem;public class ComparatorProvider implements Comparator<MyItem>{ @Override public int compare(MyItem o1, MyItem o2) { //依據name排序 return o1.getName().compareTo(o2.getName()); }}編譯打包後,把它放到你的主程式的class path裡。下面是你的主程式: //從class path中所有Jar的META-INF目錄中搜尋,找到合適的類並載入。 private static ServiceLoader<Comparator> serviceLoader = ServiceLoader.load(Comparator.class); public static void main(String[] args) { List<MyItem> myList = new ArrayList<MyItem>(); myList.add(new MyItem(2,"c","hhh")); myList.add(new MyItem(3,"k","ooo")); myList.add(new MyItem(4,"d","ppp")); myList.add(new MyItem(5,"b","ggg")); showList(myList); Collections.sort(myList,getCompartor()); showList(myList); } @SuppressWarnings("unchecked") private static Comparator<MyItem> getCompartor() { for(Comparator service : serviceLoader) { return (Comparator<MyItem>)service; } return null; }
要注意的是serviceLoader開始只是載入類,執行個體化要到第一次用的時候。類MyItem和方法showList並不重要,所以你不必在意。你可以按照這個規則,寫另外一個定序的Jar,隨時可以更換你的定序.
------------------------------
最近看到公司的一些架構和之前看到的開源的一些架構的一些服務發現和接入都採用了java的spi機制。
所以簡單的總結下java spi機制的思想。
我們系統裡抽象的各個模組,往往有很多不同的實現方案,比如日誌模組的方案,xml解析模組、jdbc模組的方案等。面向的對象的設計裡,我們一般推薦模組之間基於介面編程,模組之間不對實作類別進行寫入程式碼。一旦代碼裡涉及具體的實作類別,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。
為了實現在模組裝配的時候能不在程式裡動態指明,這就需要一種服務發現機制。java spi就是提供這樣的一個機制:為某個介面尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程式之外,在模組化設計中這個機制尤其重要。
java spi的具體約定如下 :
當服務的提供者,提供了服務介面的一種實現之後,在jar包的META-INF/services/目錄裡同時建立一個以服務介面命名的檔案。該檔案裡就是實現該服務介面的具體實作類別。而當外部程式裝配這個模組的時候,就能通過該jar包META-INF/services/裡的設定檔找到具體的實作類別名,並裝載執行個體化,完成模組的注入。
基於這樣一個約定就能很好的找到服務介面的實作類別,而不需要再代碼裡制定。
jdk提供服務實現尋找的一個工具類:java.util.ServiceLoader
例子
1.common-logging
apache最早提供的日誌的門面介面。只有介面,沒有實現。具體方案由各供應商實現,發現日誌供應商是通過掃描 META-INF/services/org.apache.commons.logging.LogFactory設定檔,通過讀取該檔案的內容找到日誌提工商實作類別。只要我們的日誌實現裡包含了這個檔案,並在檔案裡制定 LogFactory工廠介面的實作類別即可。
2.jdbc
jdbc4.0以前,開發人員還需要基於Class.forName("xxx")的方式來裝載驅動,jdbc4也基於spi的機制來發現驅動供應商了,可以通過META-INF/services/java.sql.Driver檔案裡指定實作類別的方式來暴露驅動提供者。
3.自己編寫簡單例子
假設有一個內容搜尋系統,分為展示和搜尋兩個模組。展示和搜尋基於介面編程。搜尋的實現可能是基於檔案系統的搜尋,也可能是基於資料庫的搜尋。執行個體代碼如下
Search.java: 搜尋介面 Java代碼 package search; import java.util.List; import definition.Doc; public interface Search { List<Doc> search(String keyword); }
FileSearch.java:檔案系統的搜尋實現 Java代碼 package search; import java.util.List; import definition.Doc; public class FileSearch implements Search { @Override public List<Doc> search(String keyword) { System.out.println("now use file system search. keyword:" + keyword); return null; } }
DatabaseSearch.java Java代碼 package search; import java.util.List; import definition.Doc; public class DatabaseSearch implements Search { @Override public List<Doc> search(String keyword) { System.out.println("now use database search. keyword:" + keyword); return null; } }
SearchTest.java