0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

SpringBoot插件化開發(fā)模式

jf_ro2CN3Fa ? 來源:CSDN ? 2023-06-21 09:45 ? 次閱讀

一、前言

插件化開發(fā)模式正在很多編程語言或技術(shù)框架中得以廣泛的應(yīng)用實(shí)踐,比如大家熟悉的jenkins,docker可視化管理平臺(tái)rancher,以及日常編碼使用的編輯器idea,vscode等,隨處可見的帶有熱插拔功能的插件,讓系統(tǒng)像插了翅膀一樣,大大提升了系統(tǒng)的擴(kuò)展性和伸縮性,也拓展了系統(tǒng)整體的使用價(jià)值,那么為什么要使用插件呢?

1.1 使用插件的好處

1.1.1 模塊解耦

實(shí)現(xiàn)服務(wù)模塊之間解耦的方式有很多,但是插件來說,其解耦的程度似乎更高,而且更靈活,可定制化、個(gè)性化更好。

舉例來說,代碼中可以使用設(shè)計(jì)模式來選擇使用哪種方式發(fā)送短信給下單完成的客戶,問題是各個(gè)短信服務(wù)商并不一定能保證在任何情況下都能發(fā)送成功,怎么辦呢?這時(shí)候設(shè)計(jì)模式也沒法幫你解決這個(gè)問題,如果使用定制化插件的方式,結(jié)合外部配置參數(shù),假設(shè)系統(tǒng)中某種短信發(fā)送不出去了,這時(shí)候就可以利用插件動(dòng)態(tài)植入,切換為不同的廠商發(fā)短信了。

1.1.2 提升擴(kuò)展性和開放性

以spring來說,之所以具備如此廣泛的生態(tài),與其自身內(nèi)置的各種可擴(kuò)展的插件機(jī)制是分不開的,試想為什么使用了spring框架之后可以很方便的對接其他中間件,那就是spring框架提供了很多基于插件化的擴(kuò)展點(diǎn)。

插件化機(jī)制讓系統(tǒng)的擴(kuò)展性得以提升,從而可以豐富系統(tǒng)的周邊應(yīng)用生態(tài)。

1.1.3 方便第三方接入

有了插件之后,第三方應(yīng)用或系統(tǒng)如果要對接自身的系統(tǒng),直接基于系統(tǒng)預(yù)留的插件接口完成一套適合自己業(yè)務(wù)的實(shí)現(xiàn)即可,而且對自身系統(tǒng)的侵入性很小,甚至可以實(shí)現(xiàn)基于配置參數(shù)的熱加載,方便靈活,開箱即用。

1.2 插件化常用實(shí)現(xiàn)思路

java為例,這里結(jié)合實(shí)際經(jīng)驗(yàn),整理一些常用的插件化實(shí)現(xiàn)思路:

spi機(jī)制;

約定配置和目錄,利用反射配合實(shí)現(xiàn);

springboot中的Factories機(jī)制;

java agent(探針)技術(shù);

spring內(nèi)置擴(kuò)展點(diǎn);

第三方插件包,例如:spring-plugin-core;

spring aop技術(shù);

二、Java常用插件實(shí)現(xiàn)方案

2.1 serviceloader方式

serviceloader是java提供的spi模式的實(shí)現(xiàn)。按照接口開發(fā)實(shí)現(xiàn)類,而后配置,java通過ServiceLoader來實(shí)現(xiàn)統(tǒng)一接口不同實(shí)現(xiàn)的依次調(diào)用。而java中最經(jīng)典的serviceloader的使用就是Java的spi機(jī)制。

2.1.1 java spi

SPI全稱 Service Provider Interface ,是JDK內(nèi)置的一種服務(wù)發(fā)現(xiàn)機(jī)制,SPI是一種動(dòng)態(tài)替換擴(kuò)展機(jī)制,比如有個(gè)接口,你想在運(yùn)行時(shí)動(dòng)態(tài)給他添加實(shí)現(xiàn),你只需按照規(guī)范給他添加一個(gè)實(shí)現(xiàn)類即可。比如大家熟悉的jdbc中的Driver接口,不同的廠商可以提供不同的實(shí)現(xiàn),有mysql的,也有oracle的,而Java的SPI機(jī)制就可以為某個(gè)接口尋找服務(wù)的實(shí)現(xiàn)。

下面用一張簡圖說明下SPI機(jī)制的原理

eee64efe-0fd3-11ee-962d-dac502259ad0.png

2.1.2 java spi 簡單案例

如下工程目錄,在某個(gè)應(yīng)用工程中定義一個(gè)插件接口,而其他應(yīng)用工程為了實(shí)現(xiàn)這個(gè)接口,只需要引入當(dāng)前工程的jar包依賴進(jìn)行實(shí)現(xiàn)即可,這里為了演示我就將不同的實(shí)現(xiàn)直接放在同一個(gè)工程下;

eef438ac-0fd3-11ee-962d-dac502259ad0.png

定義接口

publicinterfaceMessagePlugin{

publicStringsendMsg(MapmsgMap);

}

定義兩個(gè)不同的實(shí)現(xiàn)

publicclassAliyunMsgimplementsMessagePlugin{

@Override
publicStringsendMsg(MapmsgMap){
System.out.println("aliyunsendMsg");
return"aliyunsendMsg";
}
}
publicclassTencentMsgimplementsMessagePlugin{

@Override
publicStringsendMsg(MapmsgMap){
System.out.println("tencentsendMsg");
return"tencentsendMsg";
}
}

在resources目錄按照規(guī)范要求創(chuàng)建文件目錄,并填寫實(shí)現(xiàn)類的全類名

ef053544-0fd3-11ee-962d-dac502259ad0.png

自定義服務(wù)加載類

publicstaticvoidmain(String[]args){
ServiceLoaderserviceLoader=ServiceLoader.load(MessagePlugin.class);
Iteratoriterator=serviceLoader.iterator();
Mapmap=newHashMap();
while(iterator.hasNext()){
MessagePluginmessagePlugin=iterator.next();
messagePlugin.sendMsg(map);
}
}

運(yùn)行上面的程序后,可以看到下面的效果,這就是說,使用ServiceLoader的方式可以加載到不同接口的實(shí)現(xiàn),業(yè)務(wù)中只需要根據(jù)自身的需求,結(jié)合配置參數(shù)的方式就可以靈活的控制具體使用哪一個(gè)實(shí)現(xiàn)。

ef2c9e2c-0fd3-11ee-962d-dac502259ad0.png

2.2 自定義配置約定方式

serviceloader其實(shí)是有缺陷的,在使用中必須在META-INF里定義接口名稱的文件,在文件中才能寫上實(shí)現(xiàn)類的類名,如果一個(gè)項(xiàng)目里插件化的東西比較多,那很可能會(huì)出現(xiàn)越來越多配置文件的情況。所以在結(jié)合實(shí)際項(xiàng)目使用時(shí),可以考慮下面這種實(shí)現(xiàn)思路:

A應(yīng)用定義接口;

B,C,D等其他應(yīng)用定義服務(wù)實(shí)現(xiàn);

B,C,D應(yīng)用實(shí)現(xiàn)后達(dá)成SDK的jar;

A應(yīng)用引用SDK或者將SDK放到某個(gè)可以讀取到的目錄下;

A應(yīng)用讀取并解析SDK中的實(shí)現(xiàn)類;

在上文中案例基礎(chǔ)上,我們做如下調(diào)整;

2.2.1 添加配置文件

在配置文件中,將具體的實(shí)現(xiàn)類配置進(jìn)去

server:
port:8081
impl:
name:com.congge.plugins.spi.MessagePlugin
clazz:
-com.congge.plugins.impl.TencentMsg
-com.congge.plugins.impl.AliyunMsg

2.2.2 自定義配置文件加載類

通過這個(gè)類,將上述配置文件中的實(shí)現(xiàn)類封裝到類對象中,方便后續(xù)使用;

importlombok.Getter;
importlombok.Setter;
importlombok.ToString;
importorg.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("impl")
@ToString
publicclassClassImpl{
@Getter
@Setter
Stringname;

@Getter
@Setter
String[]clazz;
}

2.2.3 自定義測試接口

使用上述的封裝對象通過類加載的方式動(dòng)態(tài)的在程序中引入

importcom.congge.config.ClassImpl;
importcom.congge.plugins.spi.MessagePlugin;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.web.bind.annotation.GetMapping;
importorg.springframework.web.bind.annotation.RestController;

importjava.util.HashMap;

@RestController
publicclassSendMsgController{

@Autowired
ClassImplclassImpl;

//localhost:8081/sendMsg
@GetMapping("/sendMsg")
publicStringsendMsg()throwsException{
for(inti=0;i

2.2.4 啟動(dòng)類

@EnableConfigurationProperties({ClassImpl.class})
@SpringBootApplication
publicclassPluginApp{

publicstaticvoidmain(String[]args){
SpringApplication.run(PluginApp.class,args);
}

}

啟動(dòng)工程代碼后,調(diào)用接口:localhost:8081/sendMsg,在控制臺(tái)中可以看到下面的輸出信息,即通過這種方式也可以實(shí)現(xiàn)類似serviceloader的方式,不過在實(shí)際使用時(shí),可以結(jié)合配置參數(shù)進(jìn)行靈活的控制;

ef3e21a6-0fd3-11ee-962d-dac502259ad0.png

2.3 自定義配置讀取依賴jar的方式

更進(jìn)一步,在很多場景下,可能我們并不想直接在工程中引入接口實(shí)現(xiàn)的依賴包,這時(shí)候可以考慮通過讀取指定目錄下的依賴jar的方式,利用反射的方式進(jìn)行動(dòng)態(tài)加載,這也是生產(chǎn)中一種比較常用的實(shí)踐經(jīng)驗(yàn)。

具體實(shí)踐來說,主要為下面的步驟:

應(yīng)用A定義服務(wù)接口;

應(yīng)用B,C,D等實(shí)現(xiàn)接口(或者在應(yīng)用內(nèi)部實(shí)現(xiàn)相同的接口);

應(yīng)用B,C,D打成jar,放到應(yīng)用A約定的讀取目錄下;

應(yīng)用A加載約定目錄下的jar,通過反射加載目標(biāo)方法;

在上述的基礎(chǔ)上,按照上面的實(shí)現(xiàn)思路來實(shí)現(xiàn)一下;

2.3.1 創(chuàng)建約定目錄

在當(dāng)前工程下創(chuàng)建一個(gè)lib目錄,并將依賴的jar放進(jìn)去

ef5405f2-0fd3-11ee-962d-dac502259ad0.png

2.3.2 新增讀取jar的工具類

添加一個(gè)工具類,用于讀取指定目錄下的jar,并通過反射的方式,結(jié)合配置文件中的約定配置進(jìn)行反射方法的執(zhí)行;

@Component
publicclassServiceLoaderUtils{

@Autowired
ClassImplclassImpl;


publicstaticvoidloadJarsFromAppFolder()throwsException{
Stringpath="E:\code-self\bitzpp\lib";
Filef=newFile(path);
if(f.isDirectory()){
for(Filesubf:f.listFiles()){
if(subf.isFile()){
loadJarFile(subf);
}
}
}else{
loadJarFile(f);
}
}

publicstaticvoidloadJarFile(Filepath)throwsException{
URLurl=path.toURI().toURL();
//可以獲取到AppClassLoader,可以提到前面,不用每次都獲取一次
URLClassLoaderclassLoader=(URLClassLoader)ClassLoader.getSystemClassLoader();
//加載
//Methodmethod=URLClassLoader.class.getDeclaredMethod("sendMsg",Map.class);
Methodmethod=URLClassLoader.class.getMethod("sendMsg",Map.class);

method.setAccessible(true);
method.invoke(classLoader,url);
}

publicvoidmain(String[]args)throwsException{
System.out.println(invokeMethod("hello"));;
}

publicStringdoExecuteMethod()throwsException{
Stringpath="E:\code-self\bitzpp\lib";
Filef1=newFile(path);
Objectresult=null;
if(f1.isDirectory()){
for(Filesubf:f1.listFiles()){
//獲取文件名稱
Stringname=subf.getName();
StringfullPath=path+""+name;
//執(zhí)行反射相關(guān)的方法
//ServiceLoaderUtilsserviceLoaderUtils=newServiceLoaderUtils();
//result=serviceLoaderUtils.loadMethod(fullPath);
Filef=newFile(fullPath);
URLurlB=f.toURI().toURL();
URLClassLoaderclassLoaderA=newURLClassLoader(newURL[]{urlB},Thread.currentThread()
.getContextClassLoader());
String[]clazz=classImpl.getClazz();
for(StringclaName:clazz){
if(name.equals("biz-pt-1.0-SNAPSHOT.jar")){
if(!claName.equals("com.congge.spi.BitptImpl")){
continue;
}
ClassloadClass=classLoaderA.loadClass(claName);
if(Objects.isNull(loadClass)){
continue;
}
//獲取實(shí)例
Objectobj=loadClass.newInstance();
Mapmap=newHashMap();
//獲取方法
Methodmethod=loadClass.getDeclaredMethod("sendMsg",Map.class);
result=method.invoke(obj,map);
if(Objects.nonNull(result)){
break;
}
}elseif(name.equals("miz-pt-1.0-SNAPSHOT.jar")){
if(!claName.equals("com.congge.spi.MizptImpl")){
continue;
}
ClassloadClass=classLoaderA.loadClass(claName);
if(Objects.isNull(loadClass)){
continue;
}
//獲取實(shí)例
Objectobj=loadClass.newInstance();
Mapmap=newHashMap();
//獲取方法
Methodmethod=loadClass.getDeclaredMethod("sendMsg",Map.class);
result=method.invoke(obj,map);
if(Objects.nonNull(result)){
break;
}
}
}
if(Objects.nonNull(result)){
break;
}
}
}
returnresult.toString();
}

publicObjectloadMethod(StringfullPath)throwsException{
Filef=newFile(fullPath);
URLurlB=f.toURI().toURL();
URLClassLoaderclassLoaderA=newURLClassLoader(newURL[]{urlB},Thread.currentThread()
.getContextClassLoader());
Objectresult=null;
String[]clazz=classImpl.getClazz();
for(StringclaName:clazz){
ClassloadClass=classLoaderA.loadClass(claName);
if(Objects.isNull(loadClass)){
continue;
}
//獲取實(shí)例
Objectobj=loadClass.newInstance();
Mapmap=newHashMap();
//獲取方法
Methodmethod=loadClass.getDeclaredMethod("sendMsg",Map.class);
result=method.invoke(obj,map);
if(Objects.nonNull(result)){
break;
}
}
returnresult;
}


publicstaticStringinvokeMethod(Stringtext)throwsException{
Stringpath="E:\code-self\bitzpp\lib\miz-pt-1.0-SNAPSHOT.jar";
Filef=newFile(path);
URLurlB=f.toURI().toURL();
URLClassLoaderclassLoaderA=newURLClassLoader(newURL[]{urlB},Thread.currentThread()
.getContextClassLoader());
Classproduct=classLoaderA.loadClass("com.congge.spi.MizptImpl");
//獲取實(shí)例
Objectobj=product.newInstance();
Mapmap=newHashMap();
//獲取方法
Methodmethod=product.getDeclaredMethod("sendMsg",Map.class);
//執(zhí)行方法
Objectresult1=method.invoke(obj,map);
//TODOAccordingtotherequirements,writetheimplementationcode.
returnresult1.toString();
}

publicstaticStringgetApplicationFolder(){
Stringpath=ServiceLoaderUtils.class.getProtectionDomain().getCodeSource().getLocation().getPath();
returnnewFile(path).getParent();
}



}

2.3.3 添加測試接口

添加如下測試接口

@GetMapping("/sendMsgV2")
publicStringindex()throwsException{
Stringresult=serviceLoaderUtils.doExecuteMethod();
returnresult;
}

以上全部完成之后,啟動(dòng)工程,測試一下該接口,仍然可以得到預(yù)期結(jié)果;

ef688cf2-0fd3-11ee-962d-dac502259ad0.png

在上述的實(shí)現(xiàn)中還是比較粗糙的,實(shí)際運(yùn)用時(shí),還需要做較多的優(yōu)化改進(jìn)以滿足實(shí)際的業(yè)務(wù)需要,比如接口傳入類型參數(shù)用于控制具體使用哪個(gè)依賴包的方法進(jìn)行執(zhí)行等;

三、SpringBoot中的插件化實(shí)現(xiàn)

在大家使用較多的springboot框架中,其實(shí)框架自身提供了非常多的擴(kuò)展點(diǎn),其中最適合做插件擴(kuò)展的莫過于spring.factories的實(shí)現(xiàn);

3.1 Spring Boot中的SPI機(jī)制

在Spring中也有一種類似與Java SPI的加載機(jī)制。它在META-INF/spring.factories文件中配置接口的實(shí)現(xiàn)類名稱,然后在程序中讀取這些配置文件并實(shí)例化,這種自定義的SPI機(jī)制是Spring Boot Starter實(shí)現(xiàn)的基礎(chǔ)。

3.2 Spring Factories實(shí)現(xiàn)原理

spring-core包里定義了SpringFactoriesLoader類,這個(gè)類實(shí)現(xiàn)了檢索META-INF/spring.factories文件,并獲取指定接口的配置的功能。在這個(gè)類中定義了兩個(gè)對外的方法:

loadFactories 根據(jù)接口類獲取其實(shí)現(xiàn)類的實(shí)例,這個(gè)方法返回的是對象列表;

loadFactoryNames 根據(jù)接口獲取其接口類的名稱,這個(gè)方法返回的是類名的列表;

上面的兩個(gè)方法的關(guān)鍵都是從指定的ClassLoader中獲取spring.factories文件,并解析得到類名列表,具體代碼如下:

publicstaticListloadFactoryNames(ClassfactoryClass,ClassLoaderclassLoader){
StringfactoryClassName=factoryClass.getName();
try{
Enumerationurls=(classLoader!=null?classLoader.getResources(FACTORIES_RESOURCE_LOCATION):
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
Listresult=newArrayList();
while(urls.hasMoreElements()){
URLurl=urls.nextElement();
Propertiesproperties=PropertiesLoaderUtils.loadProperties(newUrlResource(url));
StringfactoryClassNames=properties.getProperty(factoryClassName);
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
returnresult;
}
catch(IOExceptionex){
thrownewIllegalArgumentException("Unabletoload["+factoryClass.getName()+
"]factoriesfromlocation["+FACTORIES_RESOURCE_LOCATION+"]",ex);
}
}

從代碼中我們可以知道,在這個(gè)方法中會(huì)遍歷整個(gè)ClassLoader中所有jar包下的spring.factories文件,就是說我們可以在自己的jar中配置spring.factories文件,不會(huì)影響到其它地方的配置,也不會(huì)被別人的配置覆蓋。

spring.factories的是通過Properties解析得到的,所以我們在寫文件中的內(nèi)容都是安裝下面這種方式配置的:

com.xxx.interface=com.xxx.classname

如果一個(gè)接口希望配置多個(gè)實(shí)現(xiàn)類,可以使用’,’進(jìn)行分割

3.3 Spring Factories案例實(shí)現(xiàn)

接下來看一個(gè)具體的案例實(shí)現(xiàn)來體驗(yàn)下Spring Factories的使用;

3.3.1 定義一個(gè)服務(wù)接口

自定義一個(gè)接口,里面添加一個(gè)方法;

publicinterfaceSmsPlugin{

publicvoidsendMessage(Stringmessage);

}

3.3.2 定義2個(gè)服務(wù)實(shí)現(xiàn)

實(shí)現(xiàn)類1

publicclassBizSmsImplimplementsSmsPlugin{

@Override
publicvoidsendMessage(Stringmessage){
System.out.println("thisisBizSmsImplsendMessage..."+message);
}
}

實(shí)現(xiàn)類2

publicclassSystemSmsImplimplementsSmsPlugin{

@Override
publicvoidsendMessage(Stringmessage){
System.out.println("thisisSystemSmsImplsendMessage..."+message);
}
}

3.3.3 添加spring.factories文件

在resources目錄下,創(chuàng)建一個(gè)名叫:META-INF的目錄,然后在該目錄下定義一個(gè)spring.factories的配置文件,內(nèi)容如下,其實(shí)就是配置了服務(wù)接口,以及兩個(gè)實(shí)現(xiàn)類的全類名的路徑;

com.congge.plugin.spi.SmsPlugin=
com.congge.plugin.impl.SystemSmsImpl,
com.congge.plugin.impl.BizSmsImpl

3.3.4 添加自定義接口

添加一個(gè)自定義的接口,有沒有發(fā)現(xiàn),這里和java 的spi有點(diǎn)類似,只不過是這里換成了SpringFactoriesLoader去加載服務(wù);

@GetMapping("/sendMsgV3")
publicStringsendMsgV3(Stringmsg)throwsException{
ListsmsServices=SpringFactoriesLoader.loadFactories(SmsPlugin.class,null);
for(SmsPluginsmsService:smsServices){
smsService.sendMessage(msg);
}
return"success";
}

啟動(dòng)工程之后,調(diào)用一下該接口進(jìn)行測試,localhost:8087/sendMsgV3?msg=hello,通過控制臺(tái),可以看到,這種方式能夠正確獲取到系統(tǒng)中可用的服務(wù)實(shí)現(xiàn);

ef7a6242-0fd3-11ee-962d-dac502259ad0.png

利用spring的這種機(jī)制,可以很好的對系統(tǒng)中的某些業(yè)務(wù)邏輯通過插件化接口的方式進(jìn)行擴(kuò)展實(shí)現(xiàn);

四、插件化機(jī)制案例實(shí)戰(zhàn)

結(jié)合上面掌握的理論知識(shí),下面基于Java SPI機(jī)制進(jìn)行一個(gè)接近真實(shí)使用場景的完整的操作步驟;

4.1 案例背景

3個(gè)微服務(wù)模塊,在A模塊中有個(gè)插件化的接口;

在A模塊中的某個(gè)接口,需要調(diào)用插件化的服務(wù)實(shí)現(xiàn)進(jìn)行短信發(fā)送;

可以通過配置文件配置參數(shù)指定具體的哪一種方式發(fā)送短信;

如果沒有加載到任何插件,將走A模塊在默認(rèn)的發(fā)短信實(shí)現(xiàn);

4.1.1 模塊結(jié)構(gòu)

1、biz-pp,插件化接口工程;

2、bitpt,aliyun短信發(fā)送實(shí)現(xiàn);

3、miz-pt,tencent短信發(fā)送實(shí)現(xiàn);

4.1.2 整體實(shí)現(xiàn)思路

本案例完整的實(shí)現(xiàn)思路參考如下:

biz-pp定義服務(wù)接口,并提供出去jar被其他實(shí)現(xiàn)工程依賴;

bitpt與miz-pt依賴biz-pp的jar并實(shí)現(xiàn)SPI中的方法;

bitpt與miz-pt按照API規(guī)范實(shí)現(xiàn)完成后,打成jar包,或者安裝到倉庫中;

biz-pp在pom中依賴bitpt與miz-pt的jar,或者通過啟動(dòng)加載的方式即可得到具體某個(gè)實(shí)現(xiàn);

4.2 biz-pp 關(guān)鍵代碼實(shí)現(xiàn)過程

4.2.1 添加服務(wù)接口

publicinterfaceMessagePlugin{

publicStringsendMsg(MapmsgMap);

}

4.2.2 打成jar包并安裝到倉庫

這一步比較簡單就不展開了

4.2.3 自定義服務(wù)加載工具類

這個(gè)類,可以理解為在真實(shí)的業(yè)務(wù)編碼中,可以根據(jù)業(yè)務(wù)定義的規(guī)則,具體加載哪個(gè)插件的實(shí)現(xiàn)類進(jìn)行發(fā)送短信的操作;

importcom.congge.plugin.spi.MessagePlugin;
importcom.congge.spi.BitptImpl;
importcom.congge.spi.MizptImpl;

importjava.util.*;

publicclassPluginFactory{

publicvoidinstallPlugin(){
Mapcontext=newLinkedHashMap();
context.put("_userId","");
context.put("_version","1.0");
context.put("_type","sms");
ServiceLoaderserviceLoader=ServiceLoader.load(MessagePlugin.class);
Iteratoriterator=serviceLoader.iterator();
while(iterator.hasNext()){
MessagePluginmessagePlugin=iterator.next();
messagePlugin.sendMsg(context);
}
}

publicstaticMessagePlugingetTargetPlugin(Stringtype){
ServiceLoaderserviceLoader=ServiceLoader.load(MessagePlugin.class);
Iteratoriterator=serviceLoader.iterator();
ListmessagePlugins=newArrayList<>();
while(iterator.hasNext()){
MessagePluginmessagePlugin=iterator.next();
messagePlugins.add(messagePlugin);
}
MessagePlugintargetPlugin=null;
for(MessagePluginmessagePlugin:messagePlugins){
booleanfindTarget=false;
switch(type){
case"aliyun":
if(messagePlugininstanceofBitptImpl){
targetPlugin=messagePlugin;
findTarget=true;
break;
}
case"tencent":
if(messagePlugininstanceofMizptImpl){
targetPlugin=messagePlugin;
findTarget=true;
break;
}
}
if(findTarget)break;
}
returntargetPlugin;
}

publicstaticvoidmain(String[]args){
newPluginFactory().installPlugin();
}


}

4.2.4 自定義接口

@RestController
publicclassSmsController{

@Autowired
privateSmsServicesmsService;

@Autowired
privateServiceLoaderUtilsserviceLoaderUtils;

//localhost:8087/sendMsg?msg=sendMsg
@GetMapping("/sendMsg")
publicStringsendMessage(Stringmsg){
returnsmsService.sendMsg(msg);
}

}

4.2.5 接口實(shí)現(xiàn)

@Service
publicclassSmsService{

@Value("${msg.type}")
privateStringmsgType;

@Autowired
privateDefaultSmsServicedefaultSmsService;

publicStringsendMsg(Stringmsg){
MessagePluginmessagePlugin=PluginFactory.getTargetPlugin(msgType);
MapparamMap=newHashMap();
if(Objects.nonNull(messagePlugin)){
returnmessagePlugin.sendMsg(paramMap);
}
returndefaultSmsService.sendMsg(paramMap);
}
}

4.2.6 添加服務(wù)依賴

在該模塊中,需要引入對具體實(shí)現(xiàn)的兩個(gè)工程的jar依賴(也可以通過啟動(dòng)加載的命令方式)




org.springframework.boot
spring-boot-starter-web


 

com.congge
biz-pt
1.0-SNAPSHOT



com.congge
miz-pt
1.0-SNAPSHOT



org.projectlombok
lombok



biz-pp的核心代碼實(shí)現(xiàn)就到此結(jié)束了,后面再具體測試的時(shí)候再繼續(xù);

4.3 bizpt 關(guān)鍵代碼實(shí)現(xiàn)過程

接下來就是插件化機(jī)制中具體的SPI實(shí)現(xiàn)過程,兩個(gè)模塊的實(shí)現(xiàn)步驟完全一致,挑選其中一個(gè)說明,工程目錄結(jié)構(gòu)如下:

ef91c1c6-0fd3-11ee-962d-dac502259ad0.png

4.3.1 添加對biz-app的jar的依賴

將上面biz-app工程打出來的jar依賴過來



com.congge
biz-app
1.0-SNAPSHOT


4.3.2 添加MessagePlugin接口的實(shí)現(xiàn)

publicclassBitptImplimplementsMessagePlugin{

@Override
publicStringsendMsg(MapmsgMap){
ObjectuserId=msgMap.get("userId");
Objecttype=msgMap.get("_type");
//TODO參數(shù)校驗(yàn)
System.out.println("====userId:"+userId+",type:"+type);
System.out.println("aliyunsendmessagesuccess");
return"aliyunsendmessagesuccess";
}
}

4.3.3 添加SPI配置文件

按照前文的方式,在resources目錄下創(chuàng)建一個(gè)文件,注意文件名稱為SPI中的接口全名,文件內(nèi)容為實(shí)現(xiàn)類的全類名

com.congge.spi.BitptImpl

4.3.4 將jar安裝到倉庫中

完成實(shí)現(xiàn)類的編碼后,通過maven命令將jar安裝到倉庫中,然后再在上一步的biz-app中引入即可;

4.4 效果演示

啟動(dòng)biz-app服務(wù),調(diào)用接口:localhost:8087/sendMsg?msg=sendMsg,可以看到如下效果

efa30a3a-0fd3-11ee-962d-dac502259ad0.png

為什么會(huì)出現(xiàn)這個(gè)效果呢?因?yàn)槲覀冊趯?shí)現(xiàn)類配置了具體使用哪一種方式進(jìn)行短信的發(fā)送,而加載插件的時(shí)候正好能夠找到對應(yīng)的服務(wù)實(shí)現(xiàn),這樣的話就給當(dāng)前的業(yè)務(wù)提供了一個(gè)較好的擴(kuò)展點(diǎn)。

efb5ac76-0fd3-11ee-962d-dac502259ad0.png

五、寫在文末

從當(dāng)前的趨勢來看,插件化機(jī)制的思想已經(jīng)遍布各種編程語言,框架,中間件,開源工具等領(lǐng)域,因此掌握插件化的實(shí)現(xiàn)機(jī)制對于當(dāng)下做程序?qū)崿F(xiàn),或架構(gòu)設(shè)計(jì)方面都有著很重要的意義,值得深入研究,本篇到此結(jié)束,感謝觀看!




審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • SPI接口
    +關(guān)注

    關(guān)注

    0

    文章

    258

    瀏覽量

    34228
  • JAVA語言
    +關(guān)注

    關(guān)注

    0

    文章

    138

    瀏覽量

    20025
  • 解耦控制
    +關(guān)注

    關(guān)注

    0

    文章

    29

    瀏覽量

    10188
  • SpringBoot
    +關(guān)注

    關(guān)注

    0

    文章

    172

    瀏覽量

    145

原文標(biāo)題:SpringBoot 插件化開發(fā)模式,強(qiáng)烈推薦!

文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    鴻蒙實(shí)戰(zhàn)開發(fā)學(xué)習(xí):【HiView插件開發(fā)

    Hiview是一個(gè)跨平臺(tái)的終端設(shè)備維測服務(wù)集,其中是由插件管理平臺(tái)和插件實(shí)現(xiàn)的各自功能構(gòu)成整套系統(tǒng)。 本文描述了hiview插件開發(fā)的全部流程。
    的頭像 發(fā)表于 03-12 11:52 ?1113次閱讀
    鴻蒙實(shí)戰(zhàn)<b class='flag-5'>開發(fā)</b>學(xué)習(xí):【HiView<b class='flag-5'>插件</b><b class='flag-5'>開發(fā)</b>】

    SpringBoot知識(shí)總結(jié)

    SpringBoot干貨學(xué)習(xí)總結(jié)
    發(fā)表于 08-01 10:40

    怎么學(xué)習(xí)SpringBoot

    SpringBoot學(xué)習(xí)之路(X5)- 整合JPA
    發(fā)表于 06-10 14:52

    springboot集成mqtt

    springboot集成mqtt,大綱一.數(shù)據(jù)入庫1.數(shù)據(jù)入庫解決方案二.開發(fā)實(shí)時(shí)訂閱發(fā)布展示頁面1.及時(shí)通訊技術(shù)2.技術(shù)整合
    發(fā)表于 07-16 07:53

    怎樣去使用springboot

    怎樣去使用springboot呢?學(xué)習(xí)springboot需要懂得哪些?
    發(fā)表于 10-25 07:13

    面向無線傳感網(wǎng)絡(luò)的構(gòu)件化開發(fā)方法

    構(gòu)件化的開發(fā)模式使開發(fā)者在開發(fā)過程中能充分調(diào)用構(gòu)件庫中現(xiàn)有的構(gòu)件為其服務(wù)。研究了構(gòu)件化開發(fā)模式
    發(fā)表于 06-23 15:31 ?10次下載

    jquery插件寫法及用法(jQuery插件開發(fā)全解析)

    如今做web開發(fā),jquery 幾乎是必不可少的,同時(shí)jquery插件也是不斷的被大家所熟知,以及運(yùn)用。最近在搞這個(gè)jquery插件,發(fā)現(xiàn)它的牛逼之處,所以講一講jQuery插件的寫法
    發(fā)表于 12-03 09:21 ?1w次閱讀
    jquery<b class='flag-5'>插件</b>寫法及用法(jQuery<b class='flag-5'>插件</b><b class='flag-5'>開發(fā)</b>全解析)

    如何使用Myeclipse進(jìn)行java可視化開發(fā)

    本文檔的主要內(nèi)容詳細(xì)介紹的是如何使用Myeclipse進(jìn)行java可視化開發(fā)。實(shí)現(xiàn)Java的可視化開發(fā)
    發(fā)表于 01-10 10:38 ?5次下載
    如何使用Myeclipse進(jìn)行java可視<b class='flag-5'>化開發(fā)</b>

    數(shù)據(jù)可視化開發(fā)平臺(tái)PageNow上線了

    PageNow是一款基于SpringBoot+Vue構(gòu)建的數(shù)據(jù)可視化開發(fā)平臺(tái)。
    的頭像 發(fā)表于 03-02 08:53 ?3570次閱讀

    JavaScript模塊化開發(fā)實(shí)驗(yàn)---webpack入門

    JavaScript模塊化開發(fā)實(shí)驗(yàn)---webpack入門(現(xiàn)代電源技術(shù)試題及答案)-文檔為JavaScript模塊化開發(fā)實(shí)驗(yàn)---webpack入門總結(jié)文檔,是一份不錯(cuò)的參考資料,感興趣的可以下載看看,,,,,,,,,,,,,
    發(fā)表于 09-17 14:49 ?7次下載
    JavaScript模塊<b class='flag-5'>化開發(fā)</b>實(shí)驗(yàn)---webpack入門

    SpringBoot中MybatisX插件的簡單使用教程

    MybatisX 是一款基于 IDEA 的快速開發(fā)插件,方便在使用mybatis以及mybatis-plus開始時(shí)簡化繁瑣的重復(fù)操作,提高開發(fā)速率。
    的頭像 發(fā)表于 02-21 09:49 ?1120次閱讀

    什么是 SpringBoot

    本文從為什么要有 `SpringBoot`,以及 `SpringBoot` 到底方便在哪里開始入手,逐步分析了 `SpringBoot` 自動(dòng)裝配的原理,最后手寫了一個(gè)簡單的 `start` 組件,通過實(shí)戰(zhàn)來體會(huì)了 `
    的頭像 發(fā)表于 04-07 11:28 ?1183次閱讀
    什么是 <b class='flag-5'>SpringBoot</b>?

    SpringBoot常用注解及使用方法1

    基于 SpringBoot 平臺(tái)開發(fā)的項(xiàng)目數(shù)不勝數(shù),與常規(guī)的基于`Spring`開發(fā)的項(xiàng)目最大的不同之處,SpringBoot 里面提供了大量的注解用于快速
    的頭像 發(fā)表于 04-07 11:51 ?588次閱讀

    SpringBoot常用注解及使用方法2

    基于 SpringBoot 平臺(tái)開發(fā)的項(xiàng)目數(shù)不勝數(shù),與常規(guī)的基于Spring開發(fā)的項(xiàng)目最大的不同之處,SpringBoot 里面提供了大量的注解用于快速
    的頭像 發(fā)表于 04-07 11:52 ?554次閱讀

    什么是springBoot業(yè)務(wù)組件化開發(fā)?談?wù)?b class='flag-5'>SpringBoot業(yè)務(wù)組件化

    首先,談一談什么是“springBoot業(yè)務(wù)組件化開發(fā)”,最近一直在開發(fā)一直面臨這一個(gè)問題,就是相同的業(yè)務(wù)場景場景在一個(gè)項(xiàng)目中使用了,又需要再另外一個(gè)項(xiàng)目中復(fù)用,一遍又一遍的復(fù)制代碼,然后想將該業(yè)務(wù)的代碼在不同的項(xiàng)目中維護(hù)起來真
    的頭像 發(fā)表于 07-20 11:30 ?737次閱讀
    什么是<b class='flag-5'>springBoot</b>業(yè)務(wù)組件<b class='flag-5'>化開發(fā)</b>?談?wù)?b class='flag-5'>SpringBoot</b>業(yè)務(wù)組件化