JAVA拾遗--关于SPI机制
JDK提供的SPI(Service Provider Interface)機制,可能很多人不太熟悉,因為這個機制是針對廠商或者插件的,也可以在一些框架的擴展中看到。其核心類java.util.ServiceLoader可以在jdk1.8的文檔中看到詳細的介紹。雖然不太常見,但并不代表它不常用,恰恰相反,你無時無刻不在用它。玄乎了,莫急,思考一下你的項目中是否有用到第三方日志包,是否有用到數據庫驅動?其實這些都和SPI有關。再來思考一下,現代的框架是如何加載日志依賴,加載數據庫驅動的,你可能會對class.forName(“com.mysql.jdbc.Driver”)這段代碼不陌生,這是每個java初學者必定遇到過的,但如今的數據庫驅動仍然是這樣加載的嗎?你還能找到這段代碼嗎?這一切的疑問,將在本篇文章結束后得到解答。
首先介紹SPI機制是個什么東西
實現一個自定義的SPI
1 項目結構
2 interface模塊
2.1 moe.cnkirito.spi.api.Printer
public interface Printer { void print(); }
interface只定義一個接口,不提供實現。規范的制定方一般都是比較牛叉的存在,這些接口通常位于java,javax前綴的包中。這里的Printer就是模擬一個規范接口。
3 good-printer模塊
3.1 good-printer\pom.xml
<dependencies><dependency><groupId>moe.cnkirito</groupId><artifactId>interface</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>規范的具體實現類必然要依賴規范接口
3.2 moe.cnkirito.spi.api.GoodPrinter
public class GoodPrinter implements Printer {public void print() {System.out.println("你是個好人~");} }作為Printer規范接口的實現一
3.3 resources\META-INF\services\moe.cnkirito.spi.api.Printer
moe.cnkirito.spi.api.GoodPrinter這里需要重點說明,每一個SPI接口都需要在自己項目的靜態資源目錄中聲明一個services文件,文件名為實現規范接口的類名全路徑,在此例中便是moe.cnkirito.spi.api.Printer,在文件中,則寫上一行具體實現類的全路徑,在此例中便是moe.cnkirito.spi.api.GoodPrinter。 這樣一個廠商的實現便完成了。
4 bad-printer模塊
我們在按照和good-printer模塊中定義的一樣的方式,完成另一個廠商對Printer規范的實現。
4.1 bad-printer\pom.xml
<dependencies><dependency><groupId>moe.cnkirito</groupId><artifactId>interface</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>4.2 moe.cnkirito.spi.api.BadPrinter
public class BadPrinter implements Printer {public void print() {System.out.println("我抽煙,喝酒,蹦迪,但我知道我是好女孩~");} }4.3 resources\META-INF\services\moe.cnkirito.spi.api.Printer
moe.cnkirito.spi.api.BadPrinter這樣,另一個廠商的實現便完成了。
5 invoker模塊
這里的invoker便是我們自己的項目了。如果一開始我們想使用廠商good-printer的Printer實現,是需要將其的依賴引入。
<dependencies><dependency><groupId>moe.cnkirito</groupId><artifactId>interface</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>moe.cnkirito</groupId><artifactId>good-printer</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>5.1 編寫調用主類
public class MainApp {public static void main(String[] args) {ServiceLoader<Printer> printerLoader = ServiceLoader.load(Printer.class);for (Printer printer : printerLoader) {printer.print();}} }ServiceLoader是java.util提供的用于加載固定類路徑下文件的一個加載器,正是它加載了對應接口聲明的實現類。
5.2 打印結果1
你是個好人~如果在后續的方案中,想替換廠商的Printer實現,只需要將依賴更換
<dependencies><dependency><groupId>moe.cnkirito</groupId><artifactId>interface</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>moe.cnkirito</groupId><artifactId>bad-printer</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>調用主類無需變更代碼,這符合開閉原則
5.3 打印結果2
我抽煙,喝酒,蹦迪,但我知道我是好女孩~是不是很神奇呢?這一切對于調用者來說都是透明的,只需要切換依賴即可!
SPI在實際項目中的應用
先總結下有什么新知識,resources/META-INF/services下的文件似乎我們之前沒怎么接觸過,ServiceLoader也沒怎么接觸過。那么現在我們打開自己項目的依賴,看看有什么發現。
1.在mysql-connector-java-xxx.jar中發現了META-INF\services\java.sql.Driver文件,里面只有兩行記錄:
com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver我們可以分析出,java.sql.Driver是一個規范接口,com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver則是mysql-connector-java-xxx.jar對這個規范的實現接口。
2.在jcl-over-slf4j-xxxx.jar中發現了META-INF\services\org.apache.commons.logging.LogFactory文件,里面只有一行記錄:
org.apache.commons.logging.impl.SLF4JLogFactory相信不用我贅述,大家都能理解這是什么含義了
3.更多的還有很多,有興趣可以自己翻一翻項目路徑下的那些jar包 既然說到了數據庫驅動,索性再多說一點,還記得一道經典的面試題:class.forName(“com.mysql.jdbc.Driver”)到底做了什么事?
先思考下:自己會怎么回答?
都知道class.forName與類加載機制有關,會觸發執行com.mysql.jdbc.Driver類中的靜態方法,從而使主類加載數據庫驅動。如果再追問,為什么它的靜態塊沒有自動觸發?可答:因為數據庫驅動類的特殊性質,JDBC規范中明確要求Driver類必須向DriverManager注冊自己,導致其必須由class.forName手動觸發,這可以在java.sql.Driver中得到解釋。完美了嗎?還沒,來到最新的DriverManager源碼中,可以看到這樣的注釋,翻譯如下:
DriverManager 類的方法 getConnection 和 getDrivers 已經得到提高以支持 Java Standard Edition Service Provider 機制。 JDBC 4.0 Drivers 必須包括 META-INF/services/java.sql.Driver文件。此文件包含 java.sql.Driver 的 JDBC 驅動程序實現的名稱。例如,要加載 my.sql.Driver 類,META-INF/services/java.sql.Driver 文件需要包含下面的條目: my.sql.Driver 應用程序不再需要使用 Class.forName()顯式地加載 JDBC 驅動程序。當前使用 Class.forName() 加載 JDBC 驅動程序的現有程序將在不作修改的情況下繼續工作。
可以發現,Class.forName已經被棄用了,所以,這道題目的最佳回答,應當是和面試官牽扯到JAVA中的SPI機制,進而聊聊加載驅動的演變歷史。 java.sql.DriverManager
public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null; }當然那,本節的內容還是主要介紹SPI,驅動這一塊這是引申而出,如果不太理解,可以多去翻一翻jdk1.8中Driver和DriverManager的源碼,相信會有不小的收獲。
這里我的理解與作者的不大相同
先看下com.mysql.jdbc.Driver
package com.mysql.jdbc;import java.sql.DriverManager; import java.sql.SQLException;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!");}} }邏輯很簡單,在靜態塊中往DriverManager注冊自己.那么為什么它的靜態塊沒有自動觸發,這個回答和作者不太一樣了. 我的理解是JVM只要加載類那么一定會執行靜態塊中的代碼,換句話說沒執行也就是這個類根本沒加載進內存.因為ClassLoader是按需加載模式,這也就是你用了一個jar包,可以不全部引入他的依賴原因所在,只要不觸發對應的類加載,那么即使沒有引入jar,也不會報ClassNotFoundException. 那么Class.forName作用是什么?作用是觸發類加載,告訴JVM我這個系統運行需要加載這個類,那么JVM加載時會自動觸發相應的靜態塊代碼執行. 那么SPI與Class.forName有什么不同?本質都是觸發加載,實例化出對象,只是SPI對于開發者來說是被動,Class.forName是主動.
SPI在擴展方面的應用
SPI不僅僅是為廠商指定的標準,同樣也為框架擴展提供了一個思路。框架可以預留出SPI接口,這樣可以在不侵入代碼的前提下,通過增刪依賴來擴展框架。前提是,框架得預留出核心接口,也就是本例中interface模塊中類似的接口,剩下的適配工作便留給了開發者。
例如我的上一篇文章 https://www.cnkirito.moe/2017/11/07/spring-cloud-sleuth/ 中介紹的motan中Filter的擴展,便是采用了SPI機制,熟悉這個設定之后再回頭去了解一些框架的SPI擴展就不會太陌生了。
總結
以上是生活随笔為你收集整理的JAVA拾遗--关于SPI机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面试必备:缓存穿透,缓存雪崩的四种解决方
- 下一篇: 弹幕,你知道是怎样练成的?