SPI 機(jī)制,「可插拔」的奧義所在!
大家好,我是小菜。一個(gè)希望能夠成為 吹著牛X談架構(gòu) 的男人!如果你也想成為我想成為的人,不然點(diǎn)個(gè)關(guān)注做個(gè)伴,讓小菜不再孤單!
本文主要介紹
SPI 機(jī)制如有需要,可以參考
如有幫助,不忘 點(diǎn)贊 ?
微信公眾號(hào)已開啟,菜農(nóng)曰,沒(méi)關(guān)注的同學(xué)們記得關(guān)注哦!
我們上篇文章講到了 Java 中 Agent 用法,不少小伙伴都覺(jué)得該方式比較偏門,平常開發(fā)不常用(幾乎沒(méi)用)。其實(shí)不然,不常用是跟項(xiàng)目掛鉤,項(xiàng)目不常用不代表該方法機(jī)制不常用,因此很多時(shí)候我們學(xué)習(xí)不能坐井觀天,認(rèn)為項(xiàng)目中沒(méi)用到就可以不學(xué),跟著項(xiàng)目成長(zhǎng)往往不能成長(zhǎng)~!
上篇跳轉(zhuǎn)入口:Java 高級(jí)用法,寫個(gè)代理侵入你?
那么這篇我們將繼續(xù)講 Java 中的另一個(gè)知識(shí)點(diǎn),也就是 SPI 機(jī)制,乍聽感覺(jué)依然陌生,這時(shí)可別再打退堂鼓!往下看你就會(huì)發(fā)現(xiàn)原來(lái)平時(shí)開發(fā)中經(jīng)??吹?!
一、SPI
我們這篇文章以問(wèn)題作為導(dǎo)向,用問(wèn)題來(lái)驅(qū)動(dòng)學(xué)習(xí),小菜先拋出幾個(gè)問(wèn)題,下面將針對(duì)這幾個(gè)問(wèn)題進(jìn)行解釋并擴(kuò)展
什么是 SPI ? SPI 和 API 的區(qū)別? 平常中有使用到 SPI 嗎?
1、什么是 SPI
SPI 是三個(gè)單詞的縮寫 Service Provider Interface,字面意思:服務(wù)提供接口。它是 Java 提供的一套用來(lái)被第三方實(shí)現(xiàn)或者擴(kuò)展的接口,它可以用來(lái)啟用框架擴(kuò)展和替換組件。具體作用便是為這些被擴(kuò)展的 API 尋找服務(wù)實(shí)現(xiàn)。
而Java SPI 便是 JDK 內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機(jī)制,常用于創(chuàng)建可擴(kuò)展、可替換組件的應(yīng)用程序,是java中模塊化與插件化的關(guān)鍵。
這里我們提到了兩個(gè)概念,分別是 模塊化和插件化。模塊化很好理解,就是將一個(gè)項(xiàng)目分成多個(gè)模塊,模塊間可能存在相互依賴(也就是通過(guò) maven 的方式),有使用微服務(wù)開發(fā)的同學(xué)就毫不陌生了,如果沒(méi)有使用微服務(wù)開發(fā)也不打緊,單體項(xiàng)目中為了界定 control,service,repository層,也會(huì)將每個(gè)領(lǐng)域單獨(dú)提取成模塊,而不是以目錄的方式~

2、類加載機(jī)制
上面我們已經(jīng)說(shuō)到了 SPI 較為粗淺的概念,小菜這里不打算直接深入 SPI,在深入 SPI 之前,我們先了解一下 ?Java 中的類加載機(jī)制。類加載機(jī)制可能實(shí)際開發(fā)中并不會(huì)去在意,但是它卻無(wú)處不在,而這個(gè)也是面試的一大熱點(diǎn)話題。
在JVM中,類加載器默認(rèn)是使用雙親委派原則,默認(rèn)的類加載器包括Bootstarp ClassLoader、Extension ClassLoader 和 System ClassLoader(Application ClassLoader),當(dāng)然可能還有自定義類加載器~自定義類加載器可以通過(guò)繼承 java.lang.classloader 來(lái)實(shí)現(xiàn)
各個(gè)類加載器作用范圍如下:
Bootstrap ClassLoader:負(fù)責(zé)加載 JDK 自帶的 rt.jar 包中的類文件,是所有類加載的父類 Extension ClassLoader:負(fù)責(zé)加載 java 的擴(kuò)展類庫(kù)從 jre/lib/ect或 java.ext.dirs 系統(tǒng)屬性指定的目錄下加載類 System ClassLoader:負(fù)責(zé)從 classpath 環(huán)境變量中加載類文件
類加載繼承關(guān)系圖如下:

1)雙親委派模型
什么是雙親委派模型?
當(dāng)一個(gè)類加載器收到加載類的任務(wù)時(shí),會(huì)先交給自己的父加載器去完成,一級(jí)一級(jí)往上,因此最后都會(huì)傳遞到 Bootstrap ClassLoader 進(jìn)行加載,只有當(dāng)父加載器無(wú)法完成加載任務(wù)的時(shí)候,才會(huì)嘗試自己進(jìn)行加載
為什么要這樣設(shè)計(jì)呢?
1、采用雙親委派原則可以避免相同類重復(fù)加載,每個(gè)加載器在進(jìn)行類加載任務(wù)的時(shí)候都會(huì)委派給自己的父類加載器進(jìn)行加載,如果父類加載無(wú)法加載才自己進(jìn)行加載,避免重復(fù)加載的局面
2、可以保證類加載的安全性,不管是哪個(gè)加載器加載這個(gè)類,最終都是委托給頂層的加載器進(jìn)行加載,保證任何加載器最終得到的都是同一個(gè)類對(duì)象
加載過(guò)程如下:

這樣做的缺陷?
子類加載器可以使用父類加載器已經(jīng)加載過(guò)的類,而父類加載器無(wú)法使用子類加載器加載過(guò)的類(類似繼承的關(guān)系)。這里就可以扯到 Java SPI 了,Java 提供了很多服務(wù)提供者接口(SPI),它可以允許第三方為這些接口提供實(shí)現(xiàn),比如數(shù)據(jù)庫(kù)中的 SPI 服務(wù) - JDBC,這些 SPI 的接口由Java核心類提供,實(shí)現(xiàn)者確實(shí)第三方,這樣就會(huì)存在問(wèn)題,提供者由 Bootstrap ClassLoader加載,而實(shí)現(xiàn)者是由第三方自定義類加載器加載,而這個(gè)時(shí)候頂層類加載就無(wú)法使用子類加載器加載過(guò)的類

解決方法
想要解決這個(gè)問(wèn)題就得打破雙親委派原則
可以使用線程上下文類加載器(ContextClassLoader)加載
Java 應(yīng)用上下文加載器默認(rèn)是使用AppClassLoader,想要在父類加載器使用到子類加載器加載的類可以使用 Thread.currentThread().getContextClassLoader()
比如我們想要加載資源可以使用以下方式:
//?使用線程上下文類加載器加載資源
public?static?void?main(String[]?args)?throws?Exception{
????String?name?=?"java/sql/Array.class";
????Enumeration?urls?=?Thread.currentThread().getContextClassLoader().getResources(name);
????while?(urls.hasMoreElements())?{
????????URL?url?=?urls.nextElement();
????????System.out.println(url.toString());
????}
}
3、Java SPI
說(shuō)完類加載機(jī)制,我們?cè)倩氐?Java SPI 來(lái),我們先通過(guò)例子熟悉下 SPI 的使用方式
使用過(guò)程圖如下:

更加通俗的理解,SPI 實(shí)際上就是一種策略模式的實(shí)現(xiàn),基于接口編程再配合上配置文件來(lái)讀取。這也符合我們的編程方式:可插拔~
使用例子如下:
項(xiàng)目結(jié)構(gòu):

ICustomSvc:服務(wù)提供接口(也就是 SPI)CustomSvcOne/CustomSvcTwo:實(shí)現(xiàn)者(這里直接在一個(gè)項(xiàng)目中簡(jiǎn)單實(shí)現(xiàn),也可以通過(guò) jar 包導(dǎo)入的方式實(shí)現(xiàn))cbuc.life.spi.service.ICustomSvc:配置文件
文件內(nèi)容:

然后我們啟動(dòng) CustomTest 查看控制臺(tái)結(jié)果

可以看到是可以加載到我們的實(shí)現(xiàn)類的方法,而這也就意味著已經(jīng)實(shí)現(xiàn)了SPI 的功能
1)實(shí)現(xiàn)原理
其實(shí)我們上面使用SPI的時(shí)候可以看到一個(gè)關(guān)鍵的類那就是ServiceLoader ,該類位于 java.util包下,我們直接點(diǎn)進(jìn) load() 方法查看如何調(diào)用
點(diǎn)進(jìn) load() 方法我們首先看到以下代碼

該塊代碼只是簡(jiǎn)單的聲明了使用線程上下文加載器,我們繼續(xù)跟進(jìn) ServiceLoader.load(service, cl)

該塊代碼也沒(méi)啥內(nèi)容,聲明返回了 ServiceLoader 對(duì)象,這個(gè)對(duì)象有什么文章?我們可以查看這個(gè)類聲明
public?final?class?ServiceLoader<S>?implements?Iterable<S>{}
可以看到這個(gè)對(duì)象實(shí)現(xiàn)了 Iterable 接口,說(shuō)明具有迭代的方法,可以猜測(cè)這樣是為了取出我們定義 SPI 的所有實(shí)現(xiàn)類。
該類的構(gòu)造函數(shù)如下

重點(diǎn)在于 reload() 方法,我們繼續(xù)跟進(jìn)

這里將注釋一起截取出來(lái),我們可以看到這句話 方法將惰性查找實(shí)例化,說(shuō)明了上述說(shuō)到實(shí)現(xiàn) Iterable 接口的用處,我們這里可以先點(diǎn)進(jìn) iterator() 方法查看是如何實(shí)現(xiàn)的

可以看到有個(gè)關(guān)鍵的緩存,該緩存存儲(chǔ) provider,每次操作的時(shí)候都會(huì)去該緩存中查找,如果存在則返回,否則采用 LazyIterator 進(jìn)行查找,我們進(jìn)行進(jìn)入到LazyIterator類中查看如何實(shí)現(xiàn),由于該類代碼過(guò)長(zhǎng),我們直接截取關(guān)鍵代碼,有興趣的同學(xué)可以自行查看完整代碼:

看到該代碼的實(shí)現(xiàn)頓時(shí)豁然開朗了,我們看到了熟悉的目錄名 META-INF/services/,該代碼會(huì)去指定目錄下獲取文件資源,然后通過(guò)上傳傳入的線程上下文類加載器進(jìn)行類加載,這樣子我們的 SPI 實(shí)現(xiàn)類就可以供項(xiàng)目使用了~ 看完不得不感嘆 妙啊~
到這里為止,我們就已經(jīng)拆解了 JAVA SPI 的使用以及實(shí)現(xiàn)原理,看完后是不是覺(jué)得該技巧也沒(méi)有離我們很遠(yuǎn)~!
4、小結(jié)
使用 Java SPI 機(jī)制更好的實(shí)現(xiàn)了 可插拔 的開發(fā)理念,使得第三方服務(wù)模塊的裝配與調(diào)用者的業(yè)務(wù)代碼相分離,也就是 解耦 的概念,我們應(yīng)用程序可以根據(jù)實(shí)際業(yè)務(wù)需要進(jìn)行動(dòng)態(tài)插拔。
二、擴(kuò)展
Spring SPI
當(dāng)然 SPI 機(jī)制不僅僅在 JDK 中實(shí)現(xiàn),我們?nèi)粘i_發(fā)用到的 Spring 以及 Dubbo 框架都有對(duì)應(yīng)的 SPI 機(jī)制。在Spring Boot中好多配置和實(shí)現(xiàn)都有默認(rèn)的實(shí)現(xiàn),我們?nèi)绻胍薷哪承┡渲?,我們只需要在配置文件中寫上?duì)應(yīng)的配置,那么項(xiàng)目應(yīng)用的便是我們定義的配置內(nèi)容,而這種方式就是采用 SPI 實(shí)現(xiàn)的。
Java SPI 與 Spring SPI 的區(qū)別
JDK 使用的加載工具類是 ServiceLoader,而 Spring 使用的是SpringFactoriesLoaderJDK 目錄命名方式是 META-INF/services/提供方接口全類名,而 Spring 使用的是META-INF/spring-factories
在使用 Spring Boot 中我們會(huì)將想要注入 IOC 容器的類將全類限定名寫到 META-INF/spring.factories文件中,在 Spring Boot 程序啟動(dòng)的時(shí)候就會(huì)由 SpringFactoriesLoader 進(jìn)行加載,掃描每個(gè) jar 包 class-path 目錄下的 META-INF/spring.factories 配置文件,然后解析 properties 文件,找到指定名稱的配置后返回

所以說(shuō) SPI 在我們實(shí)際開發(fā)中隨處可見,不止 Spring ,比如JDBC加載數(shù)據(jù)庫(kù)驅(qū)動(dòng),SLF4J加載不同提供商的日志實(shí)現(xiàn)還有 Dubbo 使用SPI的方式實(shí)現(xiàn)框架的擴(kuò)展等等
不要空談,不要貪懶,和小菜一起做個(gè)吹著牛X做架構(gòu)的程序猿吧~點(diǎn)個(gè)關(guān)注做個(gè)伴,讓小菜不再孤單。咱們下文見!
今天的你多努力一點(diǎn),明天的你就能少說(shuō)一句求人的話!我是小菜,一個(gè)和你一起變強(qiáng)的男人。
??微信公眾號(hào)已開啟,菜農(nóng)曰,沒(méi)關(guān)注的同學(xué)們記得關(guān)注哦!
