當Java SPI遇上Groovy

本文描述一種Java SPI機制與Groovy相結合的方式,實現中借鑑了Dubbo SPI的思想,旨在提供一種更加動態靈活的集成方式。拋磚引玉。

從Java SPI說起

SPI,想必大家對此耳熟能詳,全稱爲 (Service Provider Interface) ,是JDK內置的一種服務提供發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器(java.util.ServiceLoader)讀取配置文件,加載實現類。這樣可以在運行時動態地爲接口替換實現類。正因此特性,我們可以很容易的通過 SPI 機制爲我們的程序提供拓展功能。

SPI機制的約定:

  1. 在META-INF/services/目錄中創建以接口全限定名命名的文件該文件內容爲Api具體實現類的全限定名

  2. 使用ServiceLoader類動態加載META-INF中的實現類

  3. 如SPI的實現類爲Jar則需要放在主程序classPath中

  4. API具體實現類必須有一個不帶參數的構造方法

以JDBC MySQL connector爲例,在mysql-connector-java.jar中, META-INF/services/java.sql.Driver文件中寫明瞭

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

java.sql.DriverManager類初始化時會調用loadInitialDrivers 方法,該方法通過ServiceLoader#load遍歷所有的Driver,從而加載到MySQL driver的實現類。

 

Dubbo SPI

Dubbo 並未使用原生的Java SPI,而是重新實現了一套功能更強的 SPI 機制。

Dubbo框架本身提供了很多的擴展點加載。例如協議、負載均衡、序列化、線程池等等,在具體使用時,只需要使用對應的參數配置即可引入實現。

例如,對於發佈服務時的引用協議,protocol的值可以是dubbo、hessian、http、thrift等等,也可以是用戶定義並實現的某一種協議。

<dubbo:provider protocol="xxx1" />

那麼在具體的實現中,所有的服務協議實現自Protocol接口,不同的名稱的協議通過ExtensionLoader加載,通過 ExtensionLoader,我們可以加載指定的實現類。Dubbo SPI 是通過鍵值對的方式進行配置,這樣我們可以按需加載指定的實現類,而Dubbo SPI 所需的配置文件則放置在 META-INF/dubbo 路徑下,對於com.alibaba.dubbo.rpc.Protocol,其配置內容如下

mock=com.alibaba.dubbo.rpc.support.MockProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
...

在文件中,key爲不同協議的名稱,value爲對應的實現。

如果是用戶自己的實現的Protocol,則需要將相應實現打成jar在classpath下,並且jar文件的 META-INF/dubbo目錄下包含對應的com.alibaba.dubbo.rpc.Protocol接口文件和配置內容。

總結一下,Java SPI 和 Dubbo SPI都首先需要定義一個接口,然後通過一個Loader去加載指定目錄下該接口文件名所定義的實現類。

 

我的SGI

現如今,SpringBoot大行其道,我們的應用war/jar包往往都是Fat Jar, 那麼就意味着如果想要使用SPI,那麼實現類的Jar則需要提前打到Fat Jar中,這樣classLoader才能加載到Jar中的類,那麼就需要預先引入依賴進行控制,而令這個SPI機制變得不那麼的可插拔了。

退一步講,即使是不使用SpringBoot,那麼一個新的實現類引入,必須要放到classpath下的指定目錄,然後免不了重啓這個應用去加載。

那麼能不能更加地動態靈活呢?

由此引出我的項目,基於Groovy實現的SPI,spi-groovy-integration簡稱SGI(https://github.com/timestatic/spi-groovy-integration),通過Groovy的動態靈活的特性,可以隨時隨地加載實現類,從文件、數據庫.(關於Groovy的簡介和Java項目的集成可參考上一篇文章Groovy簡介與使用)。

下面舉個例子:

首先使用 @SGI註解,定義一個接口,(旅行可以有多種方式)

@SGI
public interface TravelStrategy {
    String travel(String destination);
}

實現接口,並使用 @Impl註解,在註解中指定名稱(可以騎自行車去旅行)

@GImpl(value = "bike")
public class BikeTravelStrategy implements TravelStrategy {
    @Override
    public String travel(String destination) {
        return "go to " + destination +  " by bike ~";
    }
}

使用 ExtensionLoader ,查找對應的SGI接口實現類並加載,然後根據名稱調用相應方法(騎自行車去hangzhou)

ExtensionLoader<TravelStrategy> loader = ExtensionLoader.getExtensionLoader(TravelStrategy.class);
for (Class<?> clazz : PackageScanner.findGImplClass(TravelStrategy.class)) {
    loader.addExtension(clazz);
}
loader.getExtension("bike").travel("hangzhou")

也可以從外部文件中加載實現類, 例如將如下內容保存爲NuoyafangzhuoTravelStrategy.groovy

package com.github.sgi.example.service

@GImpl(value = "nuoyafangzhou")
class NuoYaFangZhouTrave implements TravelStrategy {

    @Override
    String travel(String destination) {
        return "go to " + destination +  " by nuo ya fang zhou ~ .";
    }
}

在應用中讀取該文件, 並加載(這樣就可以及時應對發大水的情況了)

String path = "/root/file/NuoyafangzhuoTravelStrategy.groovy";
loader.addExtension(FileScanner.readFile(path));
loader.getExtension("nuoyafangzhou").travel("hangzhou")

當然, 也可以開啓一個定時任務, 定時讀取最新的文件內容重新加載

也可以將實現類註冊爲spring bean

@GImpl(isSpringBean = true, value = "flight")
// ....

// register
ExtensionSpringLoader.getExtensionSpringLoader(TravelStrategy.class).addExtensionSpringContext(script)
    
// get bean 
beanFactory.getBean("flight")

應用場景

結合業務系統的參數, 不同的參數可以調用不同的實現類, 基於Groovy動態的特性, 實現類本身也可以隨時地變化,讓系統更靈活,快速響應業務變化。

在某些環境下,比如需要對接甲方的業務系統,那麼在我們自己的環境中是無法搭建相同的環境,同時對接文檔可能也不夠完善,難以測試,更是無法debug,全靠打日誌推敲以解決問題。因此在對接的過程中免不了要反覆的去傳文件重啓調試,當然也可以通過熱部署或者像是Arthas的redfine重新加載class(用過之後依然感覺還是不夠方便)。那麼在使用了這種SPI機制之後,可以讓代碼的修改和類加載更加快速有效。

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