jdk中有一個spi的機制,可能很多人聽都沒聽過,我以前也沒有聽說過,我擦(╯—﹏—)╯(┷━━━┷
因爲一個接口可以有很多個不同的實現類嘛,而spi機制的作用就是使用配置文件可以動態的加載實現類;
而dubbo中對java原生的spi機制進行了擴充,後面我們會看到dubbo源碼中spi機制無處不在;
現在我們先學習一下java原生的spi機制
1.java原生的spi
首先我們需要創建一個maven項目,什麼依賴都不需要,能打印出hello world就行了
然後我們新建一些文件,如下圖所示
一個接口,兩個實現類:
package com.protagonist; public interface ISayName { void say(); }
package com.protagonist; public class SayEnglishName implements ISayName{ @Override public void say() { System.out.println("English:hello cool java boy"); } }
package com.protagonist; public class SayChineseName implements ISayName { @Override public void say() { System.out.println("中文:哈嘍,你好帥呀๑乛◡乛๑"); } }
執行結果下圖所示,可以看到正確的加載到了配置文件裏面的所有實現類,然後分別調用它們的say方法;
2.java中spi機制的分析
spi全稱是Service Provider Interface,這個是針對廠商或者插件的一種機制,用於一些服務提供給第三方實現或者擴展,更多的內容我就不去複製粘貼了,可以看看這篇博客 ,說的比較通俗易懂,嘿嘿๑乛◡乛๑
舉個例子,jdk提供數據庫驅動的接口, 然後不同的公司根據這些接口實現自己的產品,例如mysql,oracle驅動就是最經典的了;
下面我們可以簡單的看一下mysql驅動加載的時候,首先導入依賴
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency>
一般我們jdbc的原始代碼是這樣的:
public static void test() { private String URL = "jdbc:mysql://localhost:3306/T_USER?useUnicode=true&characterEncoding=UTF-8"; private String USER = "root"; private String PASSWORD = "123456";Class.forName(
"com.mysql.jdbc.Driver"
);
//1 加載數據庫驅動(現在的jdbc已經不需要顯示的加載驅動了,這一行可以不要) Connection connection = DriverManager.getConnection(URL,USER,PASSWORD); // 2 獲取鏈接connection PreparedStatement preparedStatement = connection.prepareStatement("insert into test (name, sex) values (?,?)"); // 3 通過statement對象執行sql preparedStatement.setString(1, "xx"); preparedStatement.setString(2, "yy"); Boolean result = preparedStatement.execute(); // 4 獲取返回結果 }
然後我們可以看看java.sql.DriverManager這個類,這個類就是我們使用jdbc時候,獲取連接的:
由此看來只要是自己手動的使用jdbc或者持久層框架中封裝了這句代碼:DriverManager.getConnection(URL,USER,PASSWORD) ,去獲取數據庫連接,啓動服務的時候,就會去遍歷所有jar包下的META-INF/services目錄,找到文件名稱爲java.sql.Driver的文件,取出其中所有實現類的全路徑,然後去實例化就可以使用了;
那可能有的人就會又問了,爲啥非要是META-INF/services目錄呀,你猜的吧?(-_-メ)
其實我們可以跟着ServiceLoader.load方法一直往裏面看看
到這裏應該就知道爲什麼是加載META-INF/services/目錄下了吧!
有興趣的還可以看看ServiceLoader類的parse和parseLine方法,這裏是詳細的解析META-INF/serivces/下文件內容的,下圖所示:
在解析了文件中所有的實現類的全路徑的時候,返回的是一個List<String>, 裏面存放的就是一個個實現類的全路徑,然後我們在調用ServiceLoader迭代器方法做循環的時候,其實就是使用反射Class.forName(cn, false, loader)的方式去動態的加載實現類
3. 打破雙親委派機制
不知道大家有沒有看到上圖中Class.forName(cn, false, loader),這一行代碼有個loader,這是一個類加載器(是在最開始load方法的時候就實例化的),大家知道這裏爲什麼要有一個類加載器麼?或者說這個類加載器有啥用?
首先這裏默認你已經熟悉了雙親委派機制了,雙親委派機制就是爲了保證系統安全,jdk已經定義過的類,我們就不能再寫一個相同類名的類了;
但是這裏有個問題,如果有這麼幾個類,String類,Teacher類,Student類,, 其中String類肯定是要啓動類加載器加載的吧,然後另外兩個類是應用類加載器加載的,我們可以在Teacher類中使用String name = new Strign("小王老師"), 那麼我們可以在String類中使用引用Teacher類和Student嗎?
我們剛剛說的spi就有這個問題,廠商實現的類爲什麼可以在jdk使用啊?jdk的類都是啓動類加載器和擴展類加載器加載的,而廠商實現的類都是應用類加載器加載的。
所以jdk給spi打破了這個雙親委託機制,可以把一個ClassLoader
置於一個線程的實例之中,使該ClassLoader
成爲一個相對共享的實例.這樣即使是啓動類加載器中的代碼也可以通過這種方式訪問應用類加載器中的類了;
這裏是真的很重要!
如果上面說的你可能沒有看懂,我也查了很多資料,在一個老哥的博客中有段話說的挺好的,只需要看我貼出來的這部分就可以了(類加載器是組合的哦,不是繼承!)
以JDBC加載驅動爲例: 在JDBC4.0之後支持SPI方式加載java.sql.Driver的實現類。SPI實現方式爲,通過ServiceLoader.load(Driver.class)方法,
去各自實現Driver接口的lib的META-INF/services/java.sql.Driver文件裏找到實現類的名字,通過Thread.currentThread().getContextClassLoader()類加載器
加載實現類並返回實例。 驅動加載的過程大致如上,那麼是在什麼地方打破了雙親委派模型呢? 先看下如果不用Thread.currentThread().getContextClassLoader()加載器加載,整個流程會怎麼樣。 1.從META-INF/services/java.sql.Driver文件得到實現類名字DriverA 2.Class.forName("xx.xx.DriverA")來加載實現類 3.Class.forName()方法默認使用當前類的ClassLoader,JDBC是在DriverManager類裏調用Driver的,當前類也就是DriverManager,
它的加載器是BootstrapClassLoader。 4.用BootstrapClassLoader去加載非rt.jar包裏的類xx.xx.DriverA,就會找不到 5.要加載xx.xx.DriverA需要用到AppClassLoader或其他自定義ClassLoader 6.最終矛盾出現在,要在BootstrapClassLoader加載的類裏,調用AppClassLoader去加載實現類 這樣就出現了一個問題:如何在父加載器加載的類中,去調用子加載器去加載類? 1.jdk提供了兩種方式,Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader()一般都指向AppClassLoader,
他們能加載classpath中的類 2.SPI則用Thread.currentThread().getContextClassLoader()來加載實現類,實現在覈心包裏的基礎類調用用戶代碼
4.總結
沒想到一個java原生的spi不知不覺的寫了這麼多(´⊙ω⊙`),就很離譜!
其實總結一下,spi其實就是講接口和實現類的定義放在了配置文件中,項目啓動的時候,根據接口全名A,就會去找所有jar包中META-INF/services/目錄下找到對應的A文件,在A文件中寫着有所有對於A的實現類,通過io流的方式讀取並解析成List<String>,然後我們調用ServiceLoader的迭代器方法的時候,就會遍歷這個集合,取出所有的實現類全路徑,通過反射實例化對象,然後調用方法;
本篇博客還通過mysql驅動實際的看了一下spi的原理,然後而且還說了spi打破了類加載的雙親委派機制,以及jdk中是怎麼打破的(在java歷史上有好幾次打破了雙親委派機制,有興趣的可以自己瞭解一下)
現在說個問題,jdk原生的spi是會將文件中所有實現類都給加載實例化,但是有的時候我們只會使用到其中一個呀?全部加載了多浪費資源啊!這個問題會在dubbo中會解決,dubbo中有一套自己的spi機制,可以說是在jdk基礎上優化了性能,而且還給擴展了一些新的功能,後續的再說( ̄▽ ̄)ノ