在鴿了那么億段時(shí)間之后,我又回來了
那么今天主要就是來聊聊如何動(dòng)態(tài)加載一個(gè)jar
中的類
需求
先來說說為什么會(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)對接,還有一些通過OneNet
或OceanConnect
這些第三方物聯(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.type
和HikvisionCameraOperation
實(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)加載類呢
先上一個(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)化類名(解析器)”又需要“提取文件路徑名稱(解析器)”等等齐帚,一層一層往上依賴
可以近似理解為Gradle
或Maven
中的依賴傳遞
這樣做的好處就是不會(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ì)慢慢更新其他的庫