<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文理解Java中的SPI機(jī)制

          共 3818字,需瀏覽 8分鐘

           ·

          2021-06-07 18:17

          SPI機(jī)制簡(jiǎn)介

          服務(wù)提供者接口(Service Provider Interface,簡(jiǎn)寫(xiě)為SPI)是JDK內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機(jī)制。可以用來(lái)加載框架擴(kuò)展和替換組件,主要是被框架的開(kāi)發(fā)人員使用。在java.util.ServiceLoader的文檔里有比較詳細(xì)的介紹。

          系統(tǒng)里抽象的各個(gè)模塊,往往有很多不同的實(shí)現(xiàn)方案,比如日志模塊的方案、xml解析模塊、jdbc模塊的方案等。面向?qū)ο蟮脑O(shè)計(jì)推薦模塊之間基于接口編程,模塊之間不對(duì)實(shí)現(xiàn)類進(jìn)行硬編碼。一旦代碼里涉及具體的實(shí)現(xiàn)類,就違反了可拔插的原則:如果需要替換組建的一種實(shí)現(xiàn),就需要修改框架的代碼。SPI機(jī)制正是解決這個(gè)問(wèn)題。

          Java中SPI機(jī)制主要思想是將裝配的控制權(quán)移到程序之外,是“基于接口的編程+策略模式+配置文件”組合實(shí)現(xiàn)的動(dòng)態(tài)加載機(jī)制,有點(diǎn)類似Spring的IOC機(jī)制。在模塊化設(shè)計(jì)中這個(gè)機(jī)制尤其重要,其核心思想就是解耦。

          SPI的接口是Java核心庫(kù)的一部分,是由引導(dǎo)類加載器(Bootstrap Classloader)來(lái)加載的。SPI的實(shí)現(xiàn)類是由系統(tǒng)類加載器(System ClassLoader)來(lái)加載的。

          引導(dǎo)類加載器在加載時(shí)是無(wú)法找到SPI的實(shí)現(xiàn)類的,因?yàn)殡p親委派模型中規(guī)定,引導(dǎo)類加載器BootstrapClassloader無(wú)法委派系統(tǒng)類加載器AppClassLoader來(lái)加載。該如何解決此問(wèn)題?

          線程上下文類加載由此誕生,它的出現(xiàn)也破壞了類加載器的雙親委派模型,使得程序可以進(jìn)行逆向類加載。有關(guān)這部分知識(shí)在最后補(bǔ)充說(shuō)明。

          應(yīng)用場(chǎng)景

          Java提供了很多SPI,允許第三方為這些接口提供實(shí)現(xiàn)。

          常見(jiàn)的SPI使用場(chǎng)景:

          1. JDBC加載不同類型的數(shù)據(jù)庫(kù)驅(qū)動(dòng)。

          2. 日志門面接口實(shí)現(xiàn)類加載,SLF4J加載不同提供商的日志實(shí)現(xiàn)類。

          3. Spring中大量使用了SPI。可以在spring.factories中加上我們自定義的自動(dòng)配置類,事件監(jiān)聽(tīng)器或初始化器等。
            3.1 對(duì)servlet3.0規(guī)范。
            3.2 對(duì)ServletContainerInitializer的實(shí)現(xiàn)。

          4. Dubbo里面有很多個(gè)組件,每個(gè)組件在框架中都是以接口的形成抽象出來(lái)。具體的實(shí)現(xiàn)又分很多種,在程序執(zhí)行時(shí)根據(jù)用戶的配置來(lái)按需取接口的實(shí)現(xiàn)。如果Dubbo的某個(gè)內(nèi)置實(shí)現(xiàn)不符合業(yè)務(wù)需求,那么只需要利用其SPI機(jī)制將新的業(yè)務(wù)實(shí)現(xiàn)替換掉Dubbo的實(shí)現(xiàn)即可。

          這些SPI的接口是由Java核心庫(kù)來(lái)提供,而SPI的實(shí)現(xiàn)則是作為Java應(yīng)用所依賴的jar包被包含進(jìn)類路徑(CLASSPATH)中。例如:JDBC的實(shí)現(xiàn)mysql就是通過(guò)Maven被依賴進(jìn)來(lái)。

          SPI具體約定

          Java SPI的具體約定:當(dāng)服務(wù)的提供者,提供了服務(wù)接口的某種實(shí)現(xiàn)之后,在jar包的META-INF/services/目錄里同時(shí)創(chuàng)建一個(gè)以服務(wù)接口命名的文件。該文件里就是實(shí)現(xiàn)該服務(wù)接口的具體實(shí)現(xiàn)類。而當(dāng)外部程序裝配這個(gè)模塊的時(shí)候,就能通過(guò)該jar包META-INF/services/里的配置文件找到具體的實(shí)現(xiàn)類名,并裝載實(shí)例化,完成模塊的注入。基于這樣一個(gè)約定就能實(shí)現(xiàn)服務(wù)接口與實(shí)現(xiàn)的解耦。

          Java SPI機(jī)制的缺點(diǎn)

          1. 不能按需加載,需要遍歷所有的實(shí)現(xiàn),并實(shí)例化,然后在循環(huán)中才能找到我們需要的實(shí)現(xiàn)。如果不想用某些實(shí)現(xiàn)類,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了,這就造成了浪費(fèi)。

          2. 多個(gè)并發(fā)多線程使用ServiceLoader類的實(shí)例是不安全的。

          3. 擴(kuò)展如果依賴其他的擴(kuò)展,做不到自動(dòng)注入和裝配。

          4. 不提供類似于Spring的IOC和AOP功能。

          5. 擴(kuò)展很難和其他的框架集成,比如擴(kuò)展里面依賴了一個(gè)Spring bean,原生的Java SPI不支持。

          針對(duì)以上的不足點(diǎn),在生產(chǎn)環(huán)境的SPI機(jī)制選擇時(shí),可以考慮使用dubbo實(shí)現(xiàn)的SPI機(jī)制。感興趣的同學(xué)可以自行查看,或等博客的后續(xù)更新。

          SPI實(shí)例

          下面用一個(gè)簡(jiǎn)單的代碼實(shí)例,演示SPI的使用方法。

          1. 代碼編寫(xiě)

          定義需要的接口,然后編碼接口的實(shí)現(xiàn)類。

          1. 增加配置文件

          在項(xiàng)目的\src\main\resources\下創(chuàng)建\META-INF\services目錄,并增加一個(gè)配置文件,這個(gè)文件必須以接口的全限定類名保持一致,例如:com.xiaohui.spi.HelloService。然后在配置文件中寫(xiě)入具體實(shí)現(xiàn)類的全限定類名,如有多個(gè)則換行寫(xiě)入。

          1. 使用JDK來(lái)載入

          使用JDK提供的ServiceLoader.load()來(lái)加載配置文件中的描述信息,完成類加載操作。

          補(bǔ)充說(shuō)明SPI加載

          有關(guān)雙親委派的講解,請(qǐng)查看博客《Java類加載及對(duì)象創(chuàng)建過(guò)程詳解

          為什么需要破壞雙親委派?

          在某些情況下父類加載器需要委托子類加載器去加載class文件。受到雙親委派加載范圍的限制,父類加載器無(wú)法加載到需要的文件。

          如何破壞雙親委派?

          雙親委派模型并不是一個(gè)強(qiáng)制性的約束模型,而是java設(shè)計(jì)者推薦給開(kāi)發(fā)者的類加載器實(shí)現(xiàn)方式,在java項(xiàng)目中大部分的類加載器都遵循這個(gè)模型,但也有例外,到目前為止,雙親委派模型主要出現(xiàn)過(guò)三次較大規(guī)模的“被破壞”情況。

          雙親委派模型的第一次“被破壞”其實(shí)發(fā)生在雙親委派模型出現(xiàn)之前——即JDK1.2發(fā)布之前。由于雙親委派模型是在JDK1.2之后才被引入的,而類加載器和抽象類java.lang.ClassLoader則是JDK1.0時(shí)候就已經(jīng)存在,面對(duì)已經(jīng)存在 的用戶自定義類加載器的實(shí)現(xiàn)代碼,Java設(shè)計(jì)者引入雙親委派模型時(shí)不得不做出一些妥協(xié)。為了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一個(gè)新的proceted方法findClass(),在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是重寫(xiě)loadClass()方法,因?yàn)樘摂M在進(jìn)行類加載的時(shí)候會(huì)調(diào)用加載器的私有方法loadClassInternal(),而這個(gè)方法的唯一邏輯就是去調(diào)用自己的loadClass()。JDK1.2之后已不再提倡用戶再去覆蓋loadClass()方法,應(yīng)當(dāng)把自己的類加載邏輯寫(xiě)到findClass()方法中,在loadClass()方法的邏輯里,如果父類加載器加載失敗,則會(huì)調(diào)用自己的findClass()方法來(lái)完成加載,這樣就可以保證新寫(xiě)出來(lái)的類加載器是符合雙親委派模型的。

          雙親委派模型的第二次“被破壞”是這個(gè)模型自身的缺陷所導(dǎo)致的,雙親委派模型很好地解決了各個(gè)類加載器的基礎(chǔ)類統(tǒng)一問(wèn)題(越基礎(chǔ)的類由越上層的加載器進(jìn)行加載),基礎(chǔ)類之所以被稱為“基礎(chǔ)”,是因?yàn)樗鼈兛偸亲鳛楸徽{(diào)用代碼調(diào)用的API。但是,如果基礎(chǔ)類又要調(diào)用用戶的代碼,那該怎么辦呢。

          為了解決這個(gè)困境,Java設(shè)計(jì)團(tuán)隊(duì)只好引入了一個(gè)不太優(yōu)雅的設(shè)計(jì):線程上下文件類加載器(Thread Context ClassLoader)。這個(gè)類加載器可以通過(guò)java.lang.Thread類的setContextClassLoader()方法進(jìn)行設(shè)置,如果創(chuàng)建線程時(shí)還未設(shè)置,它將會(huì)從父線程中繼承一個(gè);如果在應(yīng)用程序的全局范圍內(nèi)都沒(méi)有設(shè)置過(guò),那么這個(gè)類加載器默認(rèn)就是應(yīng)用程序類加載器。使用這個(gè)線程上下文類加載器去加載所需要的代碼,也就是父類加載器請(qǐng)求子類加載器去完成類加載動(dòng)作,這種行為實(shí)際上就是打通了雙親委派模型的層次結(jié)構(gòu)來(lái)逆向使用類加載器,已經(jīng)違背了雙親委派模型,但這也是無(wú)可奈何的事情。Java中所有涉及SPI的加載動(dòng)作基本上都采用這種方式,例如JNDI,JDBC,JCE,JAXB和JBI等。

          雙親委派模型的第三次“被破壞”是由于用戶對(duì)程序的動(dòng)態(tài)性的追求導(dǎo)致的,例如OSGi的出現(xiàn)。在OSGi環(huán)境下,類加載器不再是雙親委派模型中的樹(shù)狀結(jié)構(gòu),而是進(jìn)一步發(fā)展為網(wǎng)狀結(jié)構(gòu)。

          破壞雙親委派的舉例

          以tomcat為例,講解如何破壞雙親委派,屬于上述講解的第二次破壞。

          如果有10個(gè)Web應(yīng)用程序都用到了spring的話,可以把Spring的jar包放到common或shared目錄下讓這些程序共享。Spring的作用是管理每個(gè)web應(yīng)用程序的bean,getBean時(shí)自然要能訪問(wèn)到應(yīng)用程序的類,而用戶的程序是放在/WebApp/WEB-INF目錄中的(由WebAppClassLoader加載),那么在CommonClassLoader或SharedClassLoader中的Spring容器如何去加載并不在其加載范圍的用戶程序(/WebApp/WEB-INF/)中的Class呢?

          Spring統(tǒng)統(tǒng)使用線程上下文加載器(ContextClassLoade)來(lái)加載類,無(wú)需理會(huì)被放在哪里。ContextClassLoader默認(rèn)存放了WebAppClassLoader的引用,由于它是在運(yùn)行時(shí)被放在了線程中,所以不管當(dāng)前程序處于何處(BootstrapClassLoader或是ExtClassLoader等),在任何需要的時(shí)候都可以用Thread.currentThread().getContextClassLoader()取出應(yīng)用程序類加載器來(lái)完成需要的操作。

          參考:

          1. 《深入理解java虛擬機(jī)》


          瀏覽 48
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日日爽夜夜| 日韩日逼视频 | 天堂中文资源库 | 全部免费A片在线在线观看 | 北条麻妃熟女在线 |