日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JAVA拾遗--关于SPI机制

發(fā)布時間:2025/3/21 编程问答 19 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JAVA拾遗--关于SPI机制 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

JDK提供的SPI(Service Provider Interface)機(jī)制,可能很多人不太熟悉,因?yàn)檫@個機(jī)制是針對廠商或者插件的,也可以在一些框架的擴(kuò)展中看到。其核心類java.util.ServiceLoader可以在jdk1.8的文檔中看到詳細(xì)的介紹。雖然不太常見,但并不代表它不常用,恰恰相反,你無時無刻不在用它。玄乎了,莫急,思考一下你的項(xiàng)目中是否有用到第三方日志包,是否有用到數(shù)據(jù)庫驅(qū)動?其實(shí)這些都和SPI有關(guān)。再來思考一下,現(xiàn)代的框架是如何加載日志依賴,加載數(shù)據(jù)庫驅(qū)動的,你可能會對class.forName(“com.mysql.jdbc.Driver”)這段代碼不陌生,這是每個java初學(xué)者必定遇到過的,但如今的數(shù)據(jù)庫驅(qū)動仍然是這樣加載的嗎?你還能找到這段代碼嗎?這一切的疑問,將在本篇文章結(jié)束后得到解答。

首先介紹SPI機(jī)制是個什么東西

實(shí)現(xiàn)一個自定義的SPI

1 項(xiàng)目結(jié)構(gòu)

  • invoker是我們的用來測試的主項(xiàng)目。
  • interface是針對廠商和插件商定義的接口項(xiàng)目,只提供接口,不提供實(shí)現(xiàn)。
  • good-printer,bad-printer分別是兩個廠商對interface的不同實(shí)現(xiàn),所以他們會依賴于interface項(xiàng)目。 這個簡單的demo就是讓大家體驗(yàn),在不改變invoker代碼,只更改依賴的前提下,切換interface的實(shí)現(xiàn)廠商。
  • 2 interface模塊

    2.1 moe.cnkirito.spi.api.Printer

    public interface Printer { void print(); }

    interface只定義一個接口,不提供實(shí)現(xiàn)。規(guī)范的制定方一般都是比較牛叉的存在,這些接口通常位于java,javax前綴的包中。這里的Printer就是模擬一個規(guī)范接口。

    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>

    規(guī)范的具體實(shí)現(xiàn)類必然要依賴規(guī)范接口

    3.2 moe.cnkirito.spi.api.GoodPrinter

    public class GoodPrinter implements Printer {public void print() {System.out.println("你是個好人~");} }

    作為Printer規(guī)范接口的實(shí)現(xiàn)一

    3.3 resources\META-INF\services\moe.cnkirito.spi.api.Printer

    moe.cnkirito.spi.api.GoodPrinter

    這里需要重點(diǎn)說明,每一個SPI接口都需要在自己項(xiàng)目的靜態(tài)資源目錄中聲明一個services文件,文件名為實(shí)現(xiàn)規(guī)范接口的類名全路徑,在此例中便是moe.cnkirito.spi.api.Printer,在文件中,則寫上一行具體實(shí)現(xiàn)類的全路徑,在此例中便是moe.cnkirito.spi.api.GoodPrinter。 這樣一個廠商的實(shí)現(xiàn)便完成了。

    4 bad-printer模塊

    我們在按照和good-printer模塊中定義的一樣的方式,完成另一個廠商對Printer規(guī)范的實(shí)現(xiàn)。

    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

    這樣,另一個廠商的實(shí)現(xiàn)便完成了。

    5 invoker模塊

    這里的invoker便是我們自己的項(xiàng)目了。如果一開始我們想使用廠商good-printer的Printer實(shí)現(xiàn),是需要將其的依賴引入。

    <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 編寫調(diào)用主類

    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提供的用于加載固定類路徑下文件的一個加載器,正是它加載了對應(yīng)接口聲明的實(shí)現(xiàn)類。

    5.2 打印結(jié)果1

    你是個好人~

    如果在后續(xù)的方案中,想替換廠商的Printer實(shí)現(xiàn),只需要將依賴更換

    <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>

    調(diào)用主類無需變更代碼,這符合開閉原則

    5.3 打印結(jié)果2

    我抽煙,喝酒,蹦迪,但我知道我是好女孩~

    是不是很神奇呢?這一切對于調(diào)用者來說都是透明的,只需要切換依賴即可!

    SPI在實(shí)際項(xiàng)目中的應(yīng)用

    先總結(jié)下有什么新知識,resources/META-INF/services下的文件似乎我們之前沒怎么接觸過,ServiceLoader也沒怎么接觸過。那么現(xiàn)在我們打開自己項(xiàng)目的依賴,看看有什么發(fā)現(xiàn)。

    1.在mysql-connector-java-xxx.jar中發(fā)現(xiàn)了META-INF\services\java.sql.Driver文件,里面只有兩行記錄:

    com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver

    我們可以分析出,java.sql.Driver是一個規(guī)范接口,com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver則是mysql-connector-java-xxx.jar對這個規(guī)范的實(shí)現(xiàn)接口。

    2.在jcl-over-slf4j-xxxx.jar中發(fā)現(xiàn)了META-INF\services\org.apache.commons.logging.LogFactory文件,里面只有一行記錄:

    org.apache.commons.logging.impl.SLF4JLogFactory

    相信不用我贅述,大家都能理解這是什么含義了

    3.更多的還有很多,有興趣可以自己翻一翻項(xiàng)目路徑下的那些jar包 既然說到了數(shù)據(jù)庫驅(qū)動,索性再多說一點(diǎn),還記得一道經(jīng)典的面試題:class.forName(“com.mysql.jdbc.Driver”)到底做了什么事?

    先思考下:自己會怎么回答?

    都知道class.forName與類加載機(jī)制有關(guān),會觸發(fā)執(zhí)行com.mysql.jdbc.Driver類中的靜態(tài)方法,從而使主類加載數(shù)據(jù)庫驅(qū)動。如果再追問,為什么它的靜態(tài)塊沒有自動觸發(fā)?可答:因?yàn)閿?shù)據(jù)庫驅(qū)動類的特殊性質(zhì),JDBC規(guī)范中明確要求Driver類必須向DriverManager注冊自己,導(dǎo)致其必須由class.forName手動觸發(fā),這可以在java.sql.Driver中得到解釋。完美了嗎?還沒,來到最新的DriverManager源碼中,可以看到這樣的注釋,翻譯如下:

    DriverManager 類的方法 getConnection 和 getDrivers 已經(jīng)得到提高以支持 Java Standard Edition Service Provider 機(jī)制。 JDBC 4.0 Drivers 必須包括 META-INF/services/java.sql.Driver文件。此文件包含 java.sql.Driver 的 JDBC 驅(qū)動程序?qū)崿F(xiàn)的名稱。例如,要加載 my.sql.Driver 類,META-INF/services/java.sql.Driver 文件需要包含下面的條目: my.sql.Driver 應(yīng)用程序不再需要使用 Class.forName()顯式地加載 JDBC 驅(qū)動程序。當(dāng)前使用 Class.forName() 加載 JDBC 驅(qū)動程序的現(xiàn)有程序?qū)⒃诓蛔餍薷牡那闆r下繼續(xù)工作。

    可以發(fā)現(xiàn),Class.forName已經(jīng)被棄用了,所以,這道題目的最佳回答,應(yīng)當(dāng)是和面試官牽扯到JAVA中的SPI機(jī)制,進(jìn)而聊聊加載驅(qū)動的演變歷史。 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; }

    當(dāng)然那,本節(jié)的內(nèi)容還是主要介紹SPI,驅(qū)動這一塊這是引申而出,如果不太理解,可以多去翻一翻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!");}} }

    邏輯很簡單,在靜態(tài)塊中往DriverManager注冊自己.那么為什么它的靜態(tài)塊沒有自動觸發(fā),這個回答和作者不太一樣了. 我的理解是JVM只要加載類那么一定會執(zhí)行靜態(tài)塊中的代碼,換句話說沒執(zhí)行也就是這個類根本沒加載進(jìn)內(nèi)存.因?yàn)镃lassLoader是按需加載模式,這也就是你用了一個jar包,可以不全部引入他的依賴原因所在,只要不觸發(fā)對應(yīng)的類加載,那么即使沒有引入jar,也不會報ClassNotFoundException. 那么Class.forName作用是什么?作用是觸發(fā)類加載,告訴JVM我這個系統(tǒng)運(yùn)行需要加載這個類,那么JVM加載時會自動觸發(fā)相應(yīng)的靜態(tài)塊代碼執(zhí)行. 那么SPI與Class.forName有什么不同?本質(zhì)都是觸發(fā)加載,實(shí)例化出對象,只是SPI對于開發(fā)者來說是被動,Class.forName是主動.

    SPI在擴(kuò)展方面的應(yīng)用

    SPI不僅僅是為廠商指定的標(biāo)準(zhǔn),同樣也為框架擴(kuò)展提供了一個思路。框架可以預(yù)留出SPI接口,這樣可以在不侵入代碼的前提下,通過增刪依賴來擴(kuò)展框架。前提是,框架得預(yù)留出核心接口,也就是本例中interface模塊中類似的接口,剩下的適配工作便留給了開發(fā)者。

    例如我的上一篇文章 https://www.cnkirito.moe/2017/11/07/spring-cloud-sleuth/ 中介紹的motan中Filter的擴(kuò)展,便是采用了SPI機(jī)制,熟悉這個設(shè)定之后再回頭去了解一些框架的SPI擴(kuò)展就不會太陌生了。

    總結(jié)

    以上是生活随笔為你收集整理的JAVA拾遗--关于SPI机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。