-
-
本文通過探析JDK提供的,在開源專案中比較常用的Java SPI機制,希望給大家在實際開發實踐、學習開源專案提供參考,
1 SPI是什么
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實作或者擴展的API,它可以用來啟用框架擴展和替換組件,
整體機制圖如下:

Java SPI 實際上是“基于介面的編程+策略模式+組態檔”組合實作的動態加載機制,
系統設計的各個抽象,往往有很多不同的實作方案,在面向的物件的設計里,一般推薦模塊之間基于介面編程,模塊之間不對實作類進行硬編碼,一旦代碼里涉及具體的實作類,就違反了可拔插的原則,如果需要替換一種實作,就需要修改代碼,為了實作在模塊裝配的時候能不在程式里動態指明,這就需要一種服務發現機制,
Java SPI就是提供這樣的一個機制:為某個介面尋找服務實作的機制,有點類似IOC的思想,就是將裝配的控制權移到程式之外,在模塊化設計中這個機制尤其重要,所以SPI的核心思想就是解耦,2 使用場景
概括地說,適用于:呼叫者根據實際使用需要,啟用、擴展、或者替換框架的實作策略
比較常見的例子:
- 資料庫驅動加載介面實作類的加載
JDBC加載不同型別資料庫的驅動 - 日志門面介面實作類加載
SLF4J加載不同提供商的日志實作類 - Spring
Spring中大量使用了SPI,比如:對servlet3.0規范對ServletContainerInitializer的實作、自動型別轉換Type Conversion SPI(Converter SPI、Formatter SPI)等 - Dubbo
Dubbo中也大量使用SPI的方式實作框架的擴展, 不過它對Java提供的原生SPI做了封裝,允許用戶擴展實作Filter介面
3 使用介紹
要使用Java SPI,需要遵循如下約定:
- 1、當服務提供者提供了介面的一種具體實作后,在jar包的META-INF/services目錄下創建一個以“介面全限定名”為命名的檔案,內容為實作類的全限定名;
- 2、介面實作類所在的jar包放在主程式的classpath中;
- 3、主程式通過java.util.ServiceLoder動態裝載實作模塊,它通過掃描META-INF/services目錄下的組態檔找到實作類的全限定名,把類加載到JVM;
- 4、SPI的實作類必須攜帶一個不帶引數的構造方法;
示例代碼
步驟1、定義一組介面 (假設是org.foo.demo.IShout),并寫出介面的一個或多個實作,(假設是org.foo.demo.animal.Dog、org.foo.demo.animal.Cat),
public interface IShout { void shout(); } public class Cat implements IShout { @Override public void shout() { System.out.println("miao miao"); } } public class Dog implements IShout { @Override public void shout() { System.out.println("wang wang"); } }步驟2、在 src/main/resources/ 下建立 /META-INF/services 目錄, 新增一個以介面命名的檔案 (org.foo.demo.IShout檔案),內容是要應用的實作類(這里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一個類),
檔案位置
- src -main -resources - META-INF - services - org.foo.demo.IShout檔案內容
org.foo.demo.animal.Dog org.foo.demo.animal.Cat步驟3、使用 ServiceLoader 來加載組態檔中指定的實作,
public class SPIMain { public static void main(String[] args) { ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class); for (IShout s : shouts) { s.shout(); } } }代碼輸出:
wang wang miao miao4 原理決議
首先看ServiceLoader類的簽名類的成員變數:
public final class ServiceLoader<S> implements Iterable<S>{ private static final String PREFIX = "META-INF/services/"; // 代表被加載的類或者介面 private final Class<S> service; // 用于定位,加載和實體化providers的類加載器 private final ClassLoader loader; // 創建ServiceLoader時采用的訪問控制背景關系 private final AccessControlContext acc; // 快取providers,按實體化的順序排列 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 懶查找迭代器 private LazyIterator lookupIterator; ...... }參考具體ServiceLoader具體原始碼,代碼量不多,加上注釋一共587行,梳理了一下,實作的流程如下:
- 1 應用程式呼叫ServiceLoader.load方法
ServiceLoader.load方法內先創建一個新的ServiceLoader,并實體化該類中的成員變數,包括:- loader(ClassLoader型別,類加載器)
- acc(AccessControlContext型別,訪問控制器)
- providers(LinkedHashMap<String,S>型別,用于快取加載成功的類)
- lookupIterator(實作迭代器功能)
- 2 應用程式通過迭代器介面獲取物件實體
ServiceLoader先判斷成員變數providers物件中(LinkedHashMap<String,S>型別)是否有快取實體物件,如果有快取,直接回傳,
如果沒有快取,執行類的裝載,實作如下: - (1) 讀取META-INF/services/下的組態檔,獲得所有能被實體化的類的名稱,值得注意的是,ServiceLoader可以跨越jar包獲取META-INF下的組態檔,具體加載配置的實作代碼如下:
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); }- (2) 通過反射方法Class.forName()加載類物件,并用instance()方法將類實體化,
- (3) 把實體化后的類快取到providers物件中,(LinkedHashMap<String,S>型別)
然后回傳實體物件,
5 總結
優點:
使用Java SPI機制的優勢是實作解耦,使得第三方服務模塊的裝配控制的邏輯與呼叫者的業務代碼分離,而不是耦合在一起,應用程式可以根據實際業務情況啟用框架擴展或替換框架組件,缺點:
- 雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取,也就是介面的實作類全部加載并實體化一遍,如果你并不想用某些實作類,它也被加載并實體化了,這就造成了浪費,獲取某個實作類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個引數來獲取對應的實作類,
- 多個并發多執行緒使用ServiceLoader類的實體是不安全的,
- 資料庫驅動加載介面實作類的加載
-
來源:https://my.oschina.net/1Gk2fdm43/blog/4336812
歡迎關注公眾號 【碼農開花】一起學習成長
我會一直分享Java干貨,也會分享免費的學習資料課程和面試寶典
回復:【計算機】【設計模式】【面試】有驚喜哦
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/249695.html
標籤:Java
