【Java】簡單優(yōu)雅的加載外部 jar 中的 Class|插件化

在鴿了那么億段時(shí)間之后,我又回來了

那么今天主要就是來聊聊如何動(dòng)態(tài)加載一個(gè)jar中的類

想直接看 Wiki 的同學(xué)可以點(diǎn)這里

需求

先來說說為什么會(huì)有這個(gè)需求

我之前做物聯(lián)網(wǎng)相關(guān)業(yè)務(wù)平臺(tái)時(shí)色建,要求這個(gè)平臺(tái)能夠接入各種設(shè)備

但是不同類型不同廠家不同協(xié)議的設(shè)備的接入方式完全不一樣间景,字段也不一樣

比如會(huì)有燈页衙,攝像頭,屏南誊,廣播等等各種各樣的設(shè)備

燈會(huì)有開關(guān)等功能飒赃,攝像頭會(huì)有預(yù)覽回放等功能,屏?xí)胁シ乓曨l等功能聪全,廣播會(huì)有播放音頻調(diào)節(jié)音量等功能

有些設(shè)備通過TCP或是MQTT直連泊藕,有些設(shè)備通過HTTP或是SDK和廠家平臺(tái)對接,還有一些通過OneNetOceanConnect這些第三方物聯(lián)網(wǎng)平臺(tái)對接

就算是相同類型的設(shè)備难礼,比如攝像頭娃圆,也會(huì)有海康的攝像頭和大華的攝像頭等等

所以在一開始的時(shí)候蛾茉,每次對接一種設(shè)備讼呢,就相當(dāng)于寫一個(gè)if分支

我覺得這樣肯定不是長久之計(jì),就考慮將這塊內(nèi)容做個(gè)優(yōu)化

于是想到了用動(dòng)態(tài)屬性加插件化的方式(能夠解決一些痛點(diǎn)谦炬,但是有得就有失悦屏,這是后話了)

動(dòng)態(tài)屬性就先不展開了,而插件化就是通過動(dòng)態(tài)加載jar中的類來實(shí)現(xiàn)

思路

那么這個(gè)插件化要怎么做呢

首先我們先列舉一下這些設(shè)備之間相同和不同的點(diǎn)

相同點(diǎn)

  • 都是設(shè)備
  • 都需要操作(控制键思,查詢等)

不同點(diǎn)

  • 屬性不同
  • 對接方式不同(操作方式不同)

接著我們只要抽象相同點(diǎn)來解決不同點(diǎn)就行了

其中屬性不同可以通過動(dòng)態(tài)屬性的方式解決

而操作方式不同這個(gè)問題就可以定義一個(gè)操作接口

public interface DeviceOperation {

    /**
     * 設(shè)備操作
     *
     * @param device  設(shè)備
     * @param opType  操作類型
     * @param opValue 操作值
     * @return 操作結(jié)果
     */
    OperationResult operate(Device device, String opType, Object opValue);
}

那么當(dāng)我們需要對接捍∨溃康攝像頭時(shí),就可以實(shí)現(xiàn)一個(gè)HikvisionCameraOperation并且打成一個(gè)jar(插件包)吼鳞,然后讓我們的業(yè)務(wù)服務(wù)動(dòng)態(tài)加載這個(gè)類并實(shí)例化看蚜,就可以實(shí)現(xiàn)對海康攝像頭的操作了

這樣實(shí)現(xiàn)的好處是:

  • 設(shè)備操作的代碼不會(huì)和業(yè)務(wù)代碼耦合赔桌,可以單獨(dú)修復(fù)bug更新版本

這樣實(shí)現(xiàn)的壞處是:

  • 由于插件的實(shí)現(xiàn)在不同的項(xiàng)目中供炎,開發(fā)時(shí)調(diào)試起來會(huì)更加麻煩

示例

基于上述的思路,我們先在我們的插件項(xiàng)目中實(shí)現(xiàn)杭驳常康攝像頭的具體操作類HikvisionCameraOperation音诫,然后添加一個(gè)配置文件plugin.properties,設(shè)置一個(gè)屬性device.type=HikvisionCamera即設(shè)備類型為貉┪唬康攝像頭纽竣,最后打包成hikvision-camera.jar

接著我們在業(yè)務(wù)服務(wù)中注入一個(gè)設(shè)備操作服務(wù)實(shí)例DeviceOperationService,添加一個(gè)設(shè)備類型和設(shè)備操作實(shí)現(xiàn)類的緩存Map<String, DeviceOperation>

當(dāng)我們加載hikvision-camera.jar時(shí)茧泪,將提取到的device.typeHikvisionCameraOperation實(shí)例緩存起來

等到我們調(diào)用海康攝像頭的操作功能時(shí)聋袋,先根據(jù)設(shè)備的設(shè)備類型從緩存中獲得對應(yīng)實(shí)現(xiàn)類HikvisionCameraOperation的實(shí)例队伟,然后調(diào)用operate方法就能操作攝像頭了

那么我們現(xiàn)在要怎么實(shí)現(xiàn)動(dòng)態(tài)加載類呢

于是乎我自己實(shí)現(xiàn)了一個(gè)庫

先上一個(gè)簡單的寫法

@Slf4j
@Service
public class DeviceOperationService {

    /**
     * 緩存設(shè)備類型和對應(yīng)的操作對象
     */
    private final Map<String, DeviceOperation> operationMap = new ConcurrentHashMap<>();

    /**
     * 插件提取配置
     */
    private final JarPluginConcept concept = new JarPluginConcept.Builder()
            //回調(diào)到標(biāo)注了@OnPluginExtract的方法
            .extractTo(this)
            .build();

    /**
     * 插件匹配回調(diào)
     *
     * @param operation  匹配到的 DeviceOperation 實(shí)例
     * @param deviceType 配置文件中定義的設(shè)備類型
     */
    @OnPluginExtract
    public void onPluginExtract(DeviceOperation operation, @PluginProperties("device.type") String deviceType) {
        operationMap.put(deviceType, operation);
    }

    /**
     * 加載 jar 插件
     *
     * @param filePath jar 文件路徑
     */
    public void load(String filePath) {
        concept.load(filePath);
    }

上面就是提取插件的寫法

首先定義一個(gè)JarPluginConcept,主要是做一些配置幽勒,如過濾器(按包名嗜侮,類名等等),或者是提取器(如提取類,實(shí)例锈颗,配置文件等等)

接著定義一個(gè)方法并標(biāo)注@OnPluginExtract顷霹,參數(shù)就是你需要的內(nèi)容(可以是類,實(shí)例击吱,或者配置文件中的某個(gè)屬性等等)淋淀,通過extractTo進(jìn)行綁定

最后調(diào)用JarPluginConcept#load傳入jar的文件路徑后,就會(huì)觸發(fā)回調(diào)把設(shè)備類型和對應(yīng)的實(shí)現(xiàn)類放入緩存

這樣我們就能通過設(shè)備類型從緩存中獲得對應(yīng)的實(shí)現(xiàn)類覆醇,實(shí)現(xiàn)特定功能的調(diào)用

@Slf4j
@Service
public class DeviceOperationService {

    /**
     * 緩存設(shè)備類型和對應(yīng)的操作對象
     */
    private final Map<String, DeviceOperation> operationMap = new ConcurrentHashMap<>();

    /**
     * 操作一個(gè)設(shè)備
     *
     * @param device  設(shè)備對象
     * @param opType  操作類型
     * @param opValue 操作值
     * @return 操作結(jié)果
     */
    public OperationResult operate(Device device, String opType, Object opValue) {
        //獲得設(shè)備類型
        String deviceType = device.getDeviceType();
        //根據(jù)設(shè)備類型獲得操作實(shí)現(xiàn)類
        DeviceOperation operation = operationMap.get(deviceType);
        if (operation == null) {
            throw new DeviceOperationNotFoundException(deviceType + " not found");
        }
        return operation.operate(device, opType, opValue);
    }
}

設(shè)計(jì)

在說整個(gè)設(shè)計(jì)思路之前朵纷,先說說我提前想到的一些細(xì)節(jié)想法

因?yàn)榛谏弦粋€(gè)版本的庫(之前實(shí)現(xiàn)過一個(gè)類似功能的庫)我發(fā)現(xiàn)有很多地方不好用,就想著借著這個(gè)庫都優(yōu)化掉

  • 類型推導(dǎo)

之前實(shí)現(xiàn)的庫都是直接指定一個(gè)Class參數(shù)永脓,然后去匹配

后來發(fā)現(xiàn)又有讀取配置文件的需求袍辞,就硬生生加了一個(gè)讀取配置文件的if分支

所以在實(shí)現(xiàn)這個(gè)庫的時(shí)候我就想著,能不能根據(jù)使用者定義的類型來推導(dǎo)

比如方法參數(shù)的類型是Class<DeviceOperation>Class<? extends DeviceOperation>就能推導(dǎo)出是DeviceOperation的類或子類常摧,List<? extends DeviceOperation>就是DeviceOperation的實(shí)現(xiàn)類的實(shí)例列表搅吁,Properties就可能是.properties后綴的配置文件等等

然后再定義一個(gè)接口,支持其他類型的擴(kuò)展落午,這樣就算我的庫里沒對應(yīng)的實(shí)現(xiàn)谎懦,使用者也可以通過自定義來解決一些不支持的類型的問題

  • 動(dòng)態(tài)解析

之前實(shí)現(xiàn)的庫直接會(huì)把所有的.class文件加載成類

但如果我現(xiàn)在只想得到所有的類名或者是里面的配置文件,那么類加載這個(gè)步驟就完全沒必要了

所以我就在想能不能需要提取什么就解析什么板甘,如果我們只要提取類党瓮,就解析類但不解析配置文件,如果只要配置文件盐类,就解析配置文件但不解析類

于是我把jar的解析分成了很多步寞奸,提取文件路徑名稱,轉(zhuǎn)化類名在跳,加載類枪萄,實(shí)例化對象,提取.properties文件名猫妙,加載配置文件為Properties等等

然后不同的解析器會(huì)依賴其他的解析器作為前置解析器

比如我們的方法參數(shù)是Class<? extends DeviceOperation>瓷翻,所以我們需要“加載類(解析器)”,而“加載類(解析器)”又需要依賴“轉(zhuǎn)化類名(解析器)”割坠,“轉(zhuǎn)化類名(解析器)”又需要“提取文件路徑名稱(解析器)”等等齐帚,一層一層往上依賴

可以近似理解為GradleMaven中的依賴傳遞

這樣做的好處就是不會(huì)有一些額外的解析邏輯做無用功,使用者也不需要手動(dòng)添加一堆不知道什么功能的解析器

  • 插件依賴其他的jar

之前實(shí)現(xiàn)的庫沒辦法依賴其他的jar彼哼,如果必須依賴对妄,那么就需要在業(yè)務(wù)服務(wù)中添加依賴才能正常使用

所以我想到只要把依賴的jar也當(dāng)作插件加載進(jìn)來,不就可以加載到對應(yīng)的類了么

比如有些設(shè)備對接需要用到Netty敢朱,那么就可以把Netty的包作為一個(gè)基礎(chǔ)插件剪菱,其他的插件都在Netty這個(gè)插件的基礎(chǔ)上構(gòu)建

框架

接下來就從總體框架講講這個(gè)庫的設(shè)計(jì)思路

首先java中其實(shí)自帶了spi的功能摩瞎,也能夠?qū)崿F(xiàn)一定程度上的插件化

那么這兩者有什么區(qū)別呢

spi的設(shè)計(jì)思想是基于類加載這個(gè)java的獨(dú)有體系(狹義上講),而這個(gè)庫是以“插件”這個(gè)概念為基礎(chǔ)孝常,動(dòng)態(tài)加載類只是針對java在插件化這個(gè)概念上的一種具體實(shí)現(xiàn)方式旗们,你完全可以把一個(gè)Excel作為一個(gè)插件來解析,而“插件”這個(gè)概念也可以應(yīng)用于其他的開發(fā)語言

抽象

插件

從“插件”這個(gè)概念來說构灸,顯而易見上渴,我們需要一個(gè)Plugin接口,然后jar文件可以實(shí)現(xiàn)JarPlugin冻押,Excel可以實(shí)現(xiàn)ExcelPlugin

然后有一個(gè)管理類PluginConcept來加載對應(yīng)的插件

public interface PluginConcept {

    /**
     * 加載插件
     *
     * @param o 插件源
     * @return 插件 {@link Plugin}
     */
    Plugin load(Object o);
}

以加載外部jar為例驰贷,我們可以傳入文件路徑,然后返回一個(gè)JarPlugin

插件工廠

我們可以傳入一個(gè)文件路徑洛巢,也可以傳入一個(gè)File對象括袒,難道我們要一個(gè)一個(gè)枚舉出來么?

顯然不可能稿茉,我們可以定一個(gè)插件工廠锹锰,來匹配輸入對象

/**
 * 插件工廠
 */
public interface PluginFactory {

    /**
     * 是否支持插件創(chuàng)建
     *
     * @param o       插件源
     * @param concept {@link PluginConcept}
     * @return 如果支持返回 true,否則返回 false
     */
    boolean support(Object o, PluginConcept concept);

    /**
     * 創(chuàng)建插件 {@link Plugin}
     *
     * @param o       插件源
     * @param concept {@link PluginConcept}
     * @return 插件 {@link Plugin}
     */
    Plugin create(Object o, PluginConcept concept);
}

這樣漓库,我們可以為jar文件路徑實(shí)現(xiàn)一個(gè)JarPathPluginFactory恃慧,為File對象實(shí)現(xiàn)一個(gè)JarFilePluginFactory,如果需要適配其他類型渺蒿,就實(shí)現(xiàn)一個(gè)對應(yīng)的工廠

插件上下文

之前說過我們把整個(gè)解析邏輯分成了很多步痢士,那么每一步解析出來的內(nèi)容肯定要找地方緩存起來,不可能每次重新解析一遍上一個(gè)步驟

通過定義上下文類PluginContext來緩存整個(gè)解析流程中的所有內(nèi)容

當(dāng)然也提供了對應(yīng)的工廠PluginContextFactory茂装,這樣的話當(dāng)使用者自定義解析器時(shí)如果需要引用其他對象也能十分方便的擴(kuò)展

比如當(dāng)需要用到Spring容器中的Bean時(shí)怠蹂,就可以自定義上下文工廠,創(chuàng)建一個(gè)持有ApplicationContext的上下文

插件過濾器

當(dāng)我們想從jar中提取類時(shí)少态,必然會(huì)先進(jìn)行類加載

而符合條件的類可能就那么幾個(gè)城侧,完全沒有必要把全部的類都加載一遍

通過定義插件過濾器PluginFilter來過濾每一步解析的內(nèi)容,這樣就能減小解析的范圍

比如當(dāng)我們添加了一個(gè)包名過濾器彼妻,這樣只有對應(yīng)包下的類才會(huì)進(jìn)行加載嫌佑,適合類非常多但是只需要提取幾個(gè)核心類的場景

插件匹配器

當(dāng)我們解析完之后,就可以根據(jù)方法的參數(shù)類型從上下文中獲取我們需要的內(nèi)容了

通過定義插件匹配器PluginMatcher來匹配上下文中的內(nèi)容

比如侨歉,參數(shù)類型為Class<?>屋摇,結(jié)合之前提到的類型推導(dǎo),我們就可以從“加載類(解析器)”的解析結(jié)果中獲得需要的類

插件轉(zhuǎn)換器

接下來我們就要看從上下文中獲得的內(nèi)容是否需要轉(zhuǎn)換幽邓,當(dāng)然如果是類的話就不用轉(zhuǎn)換了

但是比如像配置文件的內(nèi)容摊册,我們在上下文中獲得的內(nèi)容可能是Properties對象,而方法參數(shù)類型為LinkedHashMap<String, String>颊艳,這樣的話直接賦值就會(huì)有問題

通過定義插件轉(zhuǎn)換器PluginConvertor來做轉(zhuǎn)換茅特,方便不同類型之間的轉(zhuǎn)換

插件格式器

當(dāng)我們搞定了元素類型之后,還需要判斷容器類型是否匹配

比如我們從上下文中獲得的類數(shù)據(jù)是Map<String, Class<?>>(其中key為文件路徑和名稱)棋枕,而方法參數(shù)的類型定義的是List<Class<?>>或者是Class<?>[]白修,就需要根據(jù)指定的容器類型進(jìn)行格式化

通過定義插件格式器PluginFormatter來適配不同的容器類型

插件事件

事件肯定是必不可少的,加載重斑,卸載兵睛,解析,匹配窥浪,轉(zhuǎn)換祖很,格式化等等,都可以進(jìn)行事件發(fā)布

事件本身和流程上的邏輯擴(kuò)展起來都是十分方便

插件自動(dòng)加載

基本上的內(nèi)容設(shè)計(jì)的差不多了漾脂,但是每次都要手動(dòng)調(diào)用方法是不是有億點(diǎn)點(diǎn)小麻煩

于是我就想到能不能監(jiān)聽某個(gè)目錄路徑假颇,當(dāng)文件新增時(shí)自動(dòng)加載,文件修改時(shí)自動(dòng)重新加載骨稿,文件刪除時(shí)自動(dòng)卸載

通過定義PluginAutoLoader來支持自動(dòng)加載插件

結(jié)束

主要的內(nèi)容就是這么多笨鸡,這個(gè)庫實(shí)現(xiàn)下來倒是對泛型這塊內(nèi)容深入了不少

大家有興趣的話可以捧個(gè)場,有更詳細(xì)的說明坦冠,之后也會(huì)慢慢更新其他的庫


其他的文章

【Spring Boot】一個(gè)注解實(shí)現(xiàn)下載接口

【拿來吧你】JDK動(dòng)態(tài)代理

【Java】異步回調(diào)轉(zhuǎn)為同步返回

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末形耗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辙浑,更是在濱河造成了極大的恐慌激涤,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件判呕,死亡現(xiàn)場離奇詭異倦踢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)佛玄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進(jìn)店門硼一,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人梦抢,你說我怎么就攤上這事般贼。” “怎么了奥吩?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵哼蛆,是天一觀的道長。 經(jīng)常有香客問我霞赫,道長腮介,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任端衰,我火速辦了婚禮叠洗,結(jié)果婚禮上甘改,老公的妹妹穿的比我還像新娘。我一直安慰自己灭抑,他們只是感情好十艾,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腾节,像睡著了一般忘嫉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上案腺,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天庆冕,我揣著相機(jī)與錄音,去河邊找鬼劈榨。 笑死访递,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鞋既。 我是一名探鬼主播力九,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼邑闺!你這毒婦竟也來了跌前?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤陡舅,失蹤者是張志新(化名)和其女友劉穎抵乓,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體靶衍,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡灾炭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了颅眶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜈出。...
    茶點(diǎn)故事閱讀 40,861評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖涛酗,靈堂內(nèi)的尸體忽然破棺而出铡原,到底是詐尸還是另有隱情,我是刑警寧澤商叹,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布燕刻,位于F島的核電站,受9級(jí)特大地震影響剖笙,放射性物質(zhì)發(fā)生泄漏卵洗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一弥咪、第九天 我趴在偏房一處隱蔽的房頂上張望过蹂。 院中可真熱鬧十绑,春花似錦、人聲如沸榴啸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坦报。三九已至,卻和暖如春片择,著一層夾襖步出監(jiān)牢的瞬間潜的,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工啰挪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人亡呵。 一個(gè)月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像硫戈,于是被迫代替她去往敵國和親锰什。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內(nèi)容