.Net培訓 |收費模板 收費技術服務 |二次開發
網站建設套餐 |網站訂制
空間域名 |軟件系統開發
RGB顏色查詢對照表
對于高級java程序員如果有人問你什么是SPI?請舉例SPI常見的使用場景或者你曾經使用過的案例?如果你回答不上來,你就不是一個合格的java工程師!
下面分享一篇寫得不不錯的文章。
SPI 全稱為 (Service Provider Interface) ,是JDK內置的一種服務提供發現機制。SPI是一種動態替換發現的機制, 比如有個接口,想運行時動態的給它添加實現,你只需要添加一個實現。我們經常遇到的就是java.sql.Driver接口,其他不同廠商可以針對同一接口做出不同的實現,mysql和postgresql都有不同的實現提供給用戶,而Java的SPI機制可以為某個接口尋找服務實現。
SPI常見的使用場景:
1、jdbc數據庫驅動
2、統一標準的不同廠商實現(接口)
面向的對象的設計里,我們一般推薦模塊之間基于接口編程,模塊之間不對實現類進行硬編碼。一旦代碼里涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。為了實現在模塊裝配的時候不用在程序里動態指明,這就需要一種服務發現機制。java spi就是提供這樣的一個機制:為某個接口尋找服務實現的機制。這有點類似IOC的思想,將裝配的控制權移到了程序之外。
以上文字從別處復制而來,想必你還是一臉懵逼,但不要慌,去搜一下spi你就會感覺更懵逼,因為你搜出來的只會是這個:
那到底啥是spi思想呢?
首先放個圖:我們在“調用方”和“實現方”之間需要引入“接口”,可以思考一下什么情況應該把接口放入調用方,什么時候可以把接口歸為實現方。
先來看看接口屬于實現方的情況,這個很容易理解,實現方提供了接口和實現,我們可以引用接口來達到調用某實現類的功能,這就是我們經常說的api,它具有以下特征:
當接口屬于調用方時,我們就將其稱為spi,全稱為:service provider interface,spi的規則如下:
如下圖所示:
(上圖來自:設計原則:小議 SPI 和 API)
接下來從幾個案例總結下java spi思想
在jdk6里面引進的一個新的特性ServiceLoader,從官方的文檔來說,它主要是用來裝載一系列的service provider。而且ServiceLoader可以通過service provider的配置文件來裝載指定的service provider。當服務的提供者,提供了服務接口的一種實現之后,我們只需要在jar包的META-INF/services/目錄里同時創建一個以服務接口命名的文件。該文件里就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/里的配置文件找到具體的實現類名,并裝載實例化,完成模塊的注入。 可能上面講的有些抽象,下面就結合一個示例來具體講講。
我們現在需要使用一個內容搜索接口,搜索的實現可能是基于文件系統的搜索,也可能是基于數據庫的搜索。
先定義好接口
package com.cainiao.ys.spi.learn; import java.util.List; public interface Search { public List<String> searchDoc(String keyword); }
文件搜索實現
package com.cainiao.ys.spi.learn; import java.util.List; public class FileSearch implements Search{ @Override public List<String> searchDoc(String keyword) { System.out.println("文件搜索 "+keyword); return null; } }
數據庫搜索實現
package com.cainiao.ys.spi.learn; import java.util.List; public class DatabaseSearch implements Search{ @Override public List<String> searchDoc(String keyword) { System.out.println("數據搜索 "+keyword); return null; } }
接下來可以在resources下新建META-INF/services/目錄,然后新建接口全限定名的文件:com.cainiao.ys.spi.learn.Search,里面加上我們需要用到的實現類
com.cainiao.ys.spi.learn.FileSearch
然后寫一個測試方法
package com.cainiao.ys.spi.learn; import java.util.Iterator; import java.util.ServiceLoader; public class TestCase { public static void main(String[] args) { ServiceLoader<Search> s = ServiceLoader.load(Search.class); Iterator<Search> iterator = s.iterator(); while (iterator.hasNext()) { Search search = iterator.next(); search.searchDoc("hello world"); } } }
可以看到輸出結果:文件搜索 hello world
如果在com.cainiao.ys.spi.learn.Search文件里寫上兩個實現類,那最后的輸出結果就是兩行了。 這就是因為ServiceLoader.load(Search.class)在加載某接口時,會去META-INF/services下找接口的全限定名文件,再根據里面的內容加載相應的實現類。
這就是spi的思想,接口的實現由provider實現,provider只用在提交的jar包里的META-INF/services下根據平臺定義的接口新建文件,并添加進相應的實現類內容就好。
那為什么配置文件為什么要放在META-INF/services下面? 可以打開ServiceLoader的代碼,里面定義了文件的PREFIX如下:
private static final String PREFIX = "META-INF/services/"
以上是我們自己的實現,接下來可以看下jdk中DriverManager的spi設計思路
DriverManager是jdbc里管理和注冊不同數據庫driver的工具類。從它設計的初衷來看,和我們前面討論的場景有相似之處。針對一個數據庫,可能會存在著不同的數據庫驅動實現。我們在使用特定的驅動實現時,不希望修改現有的代碼,而希望通過一個簡單的配置就可以達到效果。
我們在使用mysql驅動的時候,會有一個疑問,DriverManager是怎么獲得某確定驅動類的?
我們在運用Class.forName("com.mysql.jdbc.Driver")加載mysql驅動后,就會執行其中的靜態代碼把driver注冊到DriverManager中,以便后續的使用。 代碼如下:
package com.mysql.jdbc; public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } } }
這里可以看到,不同的驅動實現了相同的接口java.sql.Driver,然后通過registerDriver把當前driver加載到DriverManager中 這就體現了使用方提供規則,提供方根據規則把自己加載到使用方中的spi思想
這里有一個有趣的地方,查看DriverManager的源碼,可以看到其內部的靜態代碼塊中有一個loadInitialDrivers方法,在注釋中我們看到用到了上文提到的spi工具類ServiceLoader
/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
點進方法,看到方法里有如下代碼:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> drivers = loadedDrivers.iterator(); println("DriverManager.initialize: jdbc.drivers = " + loadedDrivers);
可見,DriverManager初始化時也運用了spi的思想,使用ServiceLoader把寫到配置文件里的Driver都加載了進來。
我們打開mysql-connector-java的jar包,果然在META-INF/services下發現了上文中提到的接口路徑,打開里面的內容,可以看到是com.mysql.jdbc.Driver
其實對符合DriverManager設定規則的驅動,我們并不用去調用class.forname,直接連接就好.因為DriverManager在初始化的時候已經把所有符合的驅動都加載進去了,避免了在程序中頻繁加載。
但對于沒有符合配置文件規則的驅動,如oracle,它還是需要去顯示調用classforname,再執行靜態代碼塊把驅動加載到manager里,因為它不符合配置文件規則:
最后總結一下jdk spi需要遵循的規范
其實最具spi思想的應該屬于插件開發,我們項目中也用到的這種思想,后面再說,這里具體說一下eclipse的插件思想。
Eclipse使用OSGi作為插件系統的基礎,動態添加新插件和停止現有插件,以動態的方式管理組件生命周期。
一般來說,插件的文件結構必須在指定目錄下包含以下三個文件:
當eclipse啟動時,會遍歷plugins文件夾中的目錄,掃描每個插件的清單文件MANIFEST.MF,并建立一個內部模型來記錄它所找到的每個插件的信息,就實現了動態添加新的插件。
這也意味著是eclipse制定了一系列的規則,像是文件結構、類型、參數等。插件開發者遵循這些規則去開發自己的插件,eclipse并不需要知道插件具體是怎樣開發的,只需要在啟動的時候根據配置文件解析、加載到系統里就好了,是spi思想的一種體現。
Spring中運用到spi思想的地方也有很多,下面隨便列舉幾個
我們在spring中可以通過component-scan標簽來對指定包路徑進行掃描,只要掃到spring制定的@service、@controller等注解,spring自動會把它注入容器。
這就相當于spring制定了注解規范,我們按照這個注解規范開發相應的實現類或controller,spring并不需要感知我們是怎么實現的,他只需要根據注解規范和scan標簽注入相應的bean,這正是spi理念的體現。
spring中有作用域scope的概念。 除了singleton、prototype、request、session等spring為我們提供的域,我們還可以自定義scope。
像是自定義一個 ThreadScope實現Scope接口 再把它注冊到beanFactory中
Scope threadScope = new ThreadScope(); beanFactory.registerScope("thread", threadScope);
接著就能在xml中使用了
<bean id=".." class=".." scope="thread"/>
spring作為使用方提供了自定義scope的規則,提供方根據規則進行編碼和配置,這樣在spring中就能運用我們自定義的scope了,并不需要spring感知我們scope里的實現,這也是平臺使用方制定規則,提供方負責實現的思想。
擴展Spring自定義標簽配置大致需要以下幾個步驟
這樣我們就邊寫出了一個自定義的標簽,spring只是為我們定義好了創建標簽的流程,不用感知我們是如何實現的,我們通過register就把自定義標簽加載到了spring中,實現了spi的思想。
spring里為我們提供了許多屬性編輯器,這時我們如果想把spring配置文件中的字符串轉換成相應的對象進行注入,就要自定義屬性編輯器,這時我們可以按照spring為我們提供的規則來自定義我們的編輯器
自定義好了屬性編輯器后,ConfigurableBeanFactory里面有一個registerCustomEditor方法,此方法的作用就是注冊自定義的編輯器,也是spi思想的體現
我們打開hotspot的源碼,可以看到,代碼分為shared和其他,shared屬于引擎層,os屬于不同行業的實現
點開os,可以發現里面有不同操作系統的不同實現:
不同的廠商會提供hotspot的不同實現,在hotspot啟動的時候,會判斷當前是什么系統來啟動不同的實現,這也是一種spi的思想。
在hotspot啟動時會去執行shared里的代碼,除了shared的其他三個包相當于是外部的一些實現,是不同操作系統開發人員加載到hotspot中,這種分層思想已經算是spi的思想 接著我們可以在shared里看到一個createThread接口,這個接口在不同的os下實現肯定是不一樣的,這就代表著hotspot制定接口,不同的os開發者去捐獻實現,hotspot不用感知是如何實現的,只需要在運行時直接調用接口就好,也是spi的思想。
還有像是Jetty/Tomcat中自定義sessionManager、自定義線程池,dubbo的擴展機制等內容也屬于spi
其實在這里就可以發現,只要是能滿足用戶按照系統規則來自定義,并且可以注冊到系統中的功能點,都帶有著spi的思想
參考 :
https://zhuanlan.zhihu.com/p/28909673