Java中的插件自動發現機制

前言

依賴自動發現機制是爲了提高程序的擴展性,降低代碼的耦合度。在不對核心代碼做修改和配置的前提下,只需在classpath下添加依賴,就能將插件接入到系統中。

遵循的基本原則

  • 面向接口編程

    核心代碼面向接口編程

  • 約定大於配置

    根據約定目錄下的配置,讀取並加載接口實現類

JDK自帶的SPI

SPI 全稱爲 (Service Provider Interface) ,是JDK內置的一種服務提供發現機制(動態替換接口實現)

java spi的具體約定爲:當服務的提供者,提供了服務接口的一種實現之後,在jar包的META-INF/services/目錄裏同時創建一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。 基於這樣一個約定就能很好的找到服務接口的實現類,而不需要再代碼裏制定。jdk提供服務實現查找的一個工具類:java.util.ServiceLoader

通常用來爲框架提供擴展性、兼容性和第三方插件接口:最常見的就是JDBC驅動,不同的數據廠家對應不同的驅動

例如, 有個接口,想運行時動態的給它添加實現,你只需要添加一個實現的依賴包到classpath,在這個依賴包工程中,通過一個文件聲明實現類與SPI的對應關係

在classpath下的MATE-INF/services目錄下創建一個文件,文件名爲JDK的SPI全路徑名,文件內容爲當前依賴包中的該SPI的實現類(可以有多個,用逗號隔開)。文件編碼必須是UTF-8

當JVM啓動時,會掃描classpath MATE-INF/services目錄下所有的文件,得到SPI與實現類的映射關係

通過ServiceLoader來加載SPI對應的實現類

ServiceLoader<HelloInterface> loaders = ServiceLoader.load(HelloInterface);
for (HelloInterface hello : loaders) {
    hello.sayHello();
}

Dubbo實現的SPI

Dubbo的擴展機制是基於SPI思想來實現的,但是並沒有採用JDK中原生的SPI機制,因爲JDK中SPI具有很大的缺點:

JDK中標準的SPI會一次性實例化擴展點所有的實現類,不管這些實例化出來的擴展點實現有沒有被用到。有的擴展點實現初始化時非常的耗時,即使沒有用到也會被加載,這樣就很浪費資源

Dubbo中的SPI概述

Dubbo的SPI機制中增加了對擴展點IOC和AOP的支持,一個擴展點可以直接setter注入到其他的擴展點中

彌補了上述JDK中SPI的缺點,結合Spring的設計思想,細粒度的控制擴展實現類的實例化

SPI在dubbo中的用處

Dubbo作爲靈活的框架,並不會強制所有用戶都一定使用Dubbo提供的某些架構,提供用戶自定義插件的接入

例如註冊中心(Registry),Dubbo提供了zk和redis,但是如果我們更傾向於其他的註冊中心的話,我們可以替換掉Dubbo提供的註冊中心,可以寫一個類似eureka server的註冊中心,替換默認的zk

切換zk和redis,只用在應用層使用dubbo提供的api進行配置;如果想要使用其他的註冊中心實現,可以利用dubbo的SPI機制,添加自定義的註冊中心實現依賴

針對這種可被替換的技術實現點我們稱之爲擴展點,類似的擴展點還有很多,例如Protocol,Filter,Loadbalance等等

微內核

微內核架構 (Microkernel architecture) 模式也被稱爲插件架構 (Plugin architecture) 模式

原本與內核集成在一起的組件會被分離出來,內核提供了特定的接口使得這些組件可以靈活的接入,這些組件在內核的管理下工作,但是這些組件可以獨立的發展、更改(不會對現有系統造成改動),只要符合內核的接口即可。典型的例子比如,Eclipse,IDEA

Dubbo內核對外暴露出擴展點,通過擴展點可以實現定製的符合自己業務需求的功能

Dubbo內核通過ExtensionLoader擴展點加載器來加載各個SPI擴展點。Dubbo內核對擴展是無感的 ,完全不知道擴展的存在 ,內核代碼中不會出現使用具體擴展的硬編碼

在這裏插入圖片描述

術語

擴展點

dubbo中被@SPI註解的接口被稱爲擴展點

擴展

被 @SPI 註解的 Interface 的實現稱爲這個擴展點的一個擴展

實現原理

以rpc模塊爲例

規約

擴展點約定

擴展點必須是 Interface 類型 ,必須被 @SPI 註解 ,滿足這兩點纔是一個擴展點

擴展定義約定

META-INF/services/$擴展點接口的全類名

META-INF/dubbo/$擴展點接口的全類名

META-INF/dubbo/internal/$擴展點接口的全類名

在這裏插入圖片描述

這些路徑下定義的

文件名稱爲 $擴展點接口的全類名(當前項目中定義的SPI接口-被@SPI標記) :

package org.apache.dubbo.rpc;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;

/**
 * Protocol. (API/SPI, Singleton, ThreadSafe)
 */
@SPI("dubbo")
public interface Protocol {

    int getDefaultPort();

    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    void destroy();

}

文件內容是以鍵值對的方式配置擴展點的擴展實現:

echo=org.apache.dubbo.rpc.filter.EchoFilter
generic=org.apache.dubbo.rpc.filter.GenericFilter
genericimpl=org.apache.dubbo.rpc.filter.GenericImplFilter
token=org.apache.dubbo.rpc.filter.TokenFilter
accesslog=org.apache.dubbo.rpc.filter.AccessLogFilter
activelimit=org.apache.dubbo.rpc.filter.ActiveLimitFilter
classloader=org.apache.dubbo.rpc.filter.ClassLoaderFilter
context=org.apache.dubbo.rpc.filter.ContextFilter
consumercontext=org.apache.dubbo.rpc.filter.ConsumerContextFilter
exception=org.apache.dubbo.rpc.filter.ExceptionFilter
executelimit=org.apache.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=org.apache.dubbo.rpc.filter.DeprecatedFilter
compatible=org.apache.dubbo.rpc.filter.CompatibleFilter
timeout=org.apache.dubbo.rpc.filter.TimeoutFilter

默認適應擴展

Protocol SPI被標記了@SPI(“dubbo”),表示Protocol的實現類默認使用DubboProtocol

其實dubbo和實現類DubboProtocol關係類似Spring配置文件中的id和class的關係,不同的是Dubbo的關係是配置在目錄/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol文件中,文件內容爲:

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol

實現機制就類似Spring的bean注入,通過key(dubbo、http、hessian)來找到其實現類

核心實現 - 擴展加載器 ExtensionLoader

擴展加載器控制着 dubbo 內部所有擴展點的初始化、加載擴展的過程

ExtensionLoader 中會存儲兩個靜態屬性 :

  • EXTENSION_LOADERS 保存了內核開放的擴展點對應的 ExtensionLoader 實例對象 (說明了一種擴展點有一個對應的 ExtensionLoader 對象)
  • EXTENSION_INSTANCES 保存了擴展類型 (Class) 和擴展類型的實例對象。

在這裏插入圖片描述

SpringBoot的組件自動裝配

spring-boot-starter-XXX

在使用SpringBoot構建項目時,當需要某個領域的功能時(例如web服務、數據訪問等),只需要在classpath下添加(pom配置文件配置)相應的spring-boot-starter-xxx依賴,然後在啓動類添加開啓對應的組件功能的註解(@EnableXxxXxxAutoCinfiguration),就可以將組件接入到SpringBoot中

SpringCloud就是利用這種機制,在SpringBoot的基礎上開發的微服務工具集

實現

spring-boot-starter-XXX中沒有任何業務實現,作爲橋接器,將組件功能接入到SpringBoot中(被封裝成bean注入到容器中)

SpringBoot與前兩種SPI模式模式的不同在於不是簡單的面向接口,而是面向Spring上下文(通過bean注入和取用的方式來調用組件功能),耦合度更低

橋接邏輯

配置依賴

配置spring-boot-starter-XXX的pom文件,添加這個組件具體實現的依賴,以及AutoConfiguration的相關依賴

定義橋接邏輯

XxxAutoConfiguration ①

@Configuration標記

在配置類中完成:加載配置、環境檢查、第三方功能入口bean

XxxConfiguration ②

實現ApplicationContextAware

獲取操作上下文能力

組件註冊

通過元數據的配置(/MATE-INF/spring.factories或註解)的方式來註冊組件

配置spring.factories

在starter組件工程的classpath下創建/MATE-INF/spring.factories,在這個文件中配置自定義的AutoConfiguration類

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.xxx.XXXAutoConfiguration

添加@EnableXxx ②

spring-boot-starter-XXX中定義,通過@Import指定對應的Configuration類(與AutoConfiguration實現不同,Configuration是實現ApplicationContextAware來獲取對上下文applicationContext的操作能力

所以這個模式的操作空間更多

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

加載接入

在SpringBoot應用啓動時,spring boot會遍歷在各個jar包種META-INF目錄下的spring.factories文件,構建成一個配置類鏈表(按@AutoConfigureAfter的順序),然後按順序執行配置類,將符合條件的bean注入到SpringBoot中的容器中

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章