Spring Boot 如何實(shí)現(xiàn)插件化開發(fā)模式
閱讀本文大概需要 20?分鐘。
來(lái)自:blog.csdn.net/zhangcongyi420/article/details/131139599
一、前言
1.1 使用插件的好處
1.1.1 模塊解耦
1.1.2 提升擴(kuò)展性和開放性
1.1.3 方便第三方接入
1.2 插件化常用實(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方式
2.1.1 java spi

2.1.2 java spi 簡(jiǎn)單案例

public?interface?MessagePlugin?{
?
????public?String?sendMsg(Map?msgMap);
?
}
public?class?AliyunMsg?implements?MessagePlugin?{
?
????@Override
????public?String?sendMsg(Map?msgMap)?{
????????System.out.println("aliyun?sendMsg");
????????return?"aliyun?sendMsg";
????}
}
public?class?TencentMsg?implements?MessagePlugin?{
?
????@Override
????public?String?sendMsg(Map?msgMap)?{
????????System.out.println("tencent?sendMsg");
????????return?"tencent?sendMsg";
????}
}

?public?static?void?main(String[]?args)?{
????????ServiceLoader?serviceLoader?=?ServiceLoader.load(MessagePlugin.class);
????????Iterator?iterator?=?serviceLoader.iterator();
????????Map?map?=?new?HashMap();
????????while?(iterator.hasNext()){
????????????MessagePlugin?messagePlugin?=?iterator.next();
????????????messagePlugin.sendMsg(map);
????????}
????}

2.2 自定義配置約定方式
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)類;
2.2.1 添加配置文件
server?:
??port?:?8081
impl:
??name?:?com.congge.plugins.spi.MessagePlugin
??clazz?:
????-?com.congge.plugins.impl.TencentMsg
????-?com.congge.plugins.impl.AliyunMsg
2.2.2 自定義配置文件加載類
import?lombok.Getter;
import?lombok.Setter;
import?lombok.ToString;
import?org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("impl")
@ToString
public?class?ClassImpl?{
????@Getter
????@Setter
????String?name;
?
????@Getter
????@Setter
????String[]?clazz;
}
2.2.3 自定義測(cè)試接口
import?com.congge.config.ClassImpl;
import?com.congge.plugins.spi.MessagePlugin;
import?org.springframework.beans.factory.annotation.Autowired;
import?org.springframework.web.bind.annotation.GetMapping;
import?org.springframework.web.bind.annotation.RestController;
?
import?java.util.HashMap;
?
@RestController
public?class?SendMsgController?{
?
????@Autowired
????ClassImpl?classImpl;
?
????//localhost:8081/sendMsg
????@GetMapping("/sendMsg")
????public?String?sendMsg()?throws?Exception{
????????for?(int?i=0;i????????????Class?pluginClass=?Class.forName(classImpl.getClazz()[i]);
????????????MessagePlugin?messagePlugin?=?(MessagePlugin)?pluginClass.newInstance();
????????????messagePlugin.sendMsg(new?HashMap());
????????}
????????return?"success";
????}
?
}
2.2.4 啟動(dòng)類
@EnableConfigurationProperties({ClassImpl.class})
@SpringBootApplication
public?class?PluginApp?{
?
????public?static?void?main(String[]?args)?{
????????SpringApplication.run(PluginApp.class,args);
????}
?
}
localhost:8081/sendMsg,在控制臺(tái)中可以看到下面的輸出信息,即通過這種方式也可以實(shí)現(xiàn)類似serviceloader的方式,不過在實(shí)際使用時(shí),可以結(jié)合配置參數(shù)進(jìn)行靈活的控制;
2.3 自定義配置讀取依賴jar的方式
應(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)方法;
2.3.1 創(chuàng)建約定目錄

2.3.2 新增讀取jar的工具類
@Component
public?class?ServiceLoaderUtils?{
?
????@Autowired
????ClassImpl?classImpl;
?
?
????public?static?void?loadJarsFromAppFolder()?throws?Exception?{
????????String?path?=?"E:\\code-self\\bitzpp\\lib";
????????File?f?=?new?File(path);
????????if?(f.isDirectory())?{
????????????for?(File?subf?:?f.listFiles())?{
????????????????if?(subf.isFile())?{
????????????????????loadJarFile(subf);
????????????????}
????????????}
????????}?else?{
????????????loadJarFile(f);
????????}
????}
?
????public?static?void?loadJarFile(File?path)?throws?Exception?{
????????URL?url?=?path.toURI().toURL();
????????//?可以獲取到AppClassLoader,可以提到前面,不用每次都獲取一次
????????URLClassLoader?classLoader?=?(URLClassLoader)?ClassLoader.getSystemClassLoader();
????????//?加載
????????//Method?method?=?URLClassLoader.class.getDeclaredMethod("sendMsg",?Map.class);
????????Method?method?=?URLClassLoader.class.getMethod("sendMsg",?Map.class);
?
????????method.setAccessible(true);
????????method.invoke(classLoader,?url);
????}
?
????public??void?main(String[]?args)?throws?Exception{
????????System.out.println(invokeMethod("hello"));;
????}
?
????public?String?doExecuteMethod()?throws?Exception{
????????String?path?=?"E:\\code-self\\bitzpp\\lib";
????????File?f1?=?new?File(path);
????????Object?result?=?null;
????????if?(f1.isDirectory())?{
????????????for?(File?subf?:?f1.listFiles())?{
????????????????//獲取文件名稱
????????????????String?name?=?subf.getName();
????????????????String?fullPath?=?path?+?"\\"?+?name;
????????????????//執(zhí)行反射相關(guān)的方法
????????????????//ServiceLoaderUtils?serviceLoaderUtils?=?new?ServiceLoaderUtils();
????????????????//result?=?serviceLoaderUtils.loadMethod(fullPath);
????????????????File?f?=?new?File(fullPath);
????????????????URL?urlB?=?f.toURI().toURL();
????????????????URLClassLoader?classLoaderA?=?new?URLClassLoader(new?URL[]{urlB},?Thread.currentThread()
????????????????????????.getContextClassLoader());
????????????????String[]?clazz?=?classImpl.getClazz();
????????????????for(String?claName?:?clazz){
????????????????????if(name.equals("biz-pt-1.0-SNAPSHOT.jar")){
????????????????????????if(!claName.equals("com.congge.spi.BitptImpl")){
????????????????????????????continue;
????????????????????????}
????????????????????????Class>?loadClass?=?classLoaderA.loadClass(claName);
????????????????????????if(Objects.isNull(loadClass)){
????????????????????????????continue;
????????????????????????}
????????????????????????//獲取實(shí)例
????????????????????????Object?obj?=?loadClass.newInstance();
????????????????????????Map?map?=?new?HashMap();
????????????????????????//獲取方法
????????????????????????Method?method=loadClass.getDeclaredMethod("sendMsg",Map.class);
????????????????????????result?=?method.invoke(obj,map);
????????????????????????if(Objects.nonNull(result)){
????????????????????????????break;
????????????????????????}
????????????????????}else?if(name.equals("miz-pt-1.0-SNAPSHOT.jar")){
????????????????????????if(!claName.equals("com.congge.spi.MizptImpl")){
????????????????????????????continue;
????????????????????????}
????????????????????????Class>?loadClass?=?classLoaderA.loadClass(claName);
????????????????????????if(Objects.isNull(loadClass)){
????????????????????????????continue;
????????????????????????}
????????????????????????//獲取實(shí)例
????????????????????????Object?obj?=?loadClass.newInstance();
????????????????????????Map?map?=?new?HashMap();
????????????????????????//獲取方法
????????????????????????Method?method=loadClass.getDeclaredMethod("sendMsg",Map.class);
????????????????????????result?=?method.invoke(obj,map);
????????????????????????if(Objects.nonNull(result)){
????????????????????????????break;
????????????????????????}
????????????????????}
????????????????}
????????????????if(Objects.nonNull(result)){
????????????????????break;
????????????????}
????????????}
????????}
????????return?result.toString();
????}
?
????public?Object?loadMethod(String?fullPath)?throws?Exception{
????????File?f?=?new?File(fullPath);
????????URL?urlB?=?f.toURI().toURL();
????????URLClassLoader?classLoaderA?=?new?URLClassLoader(new?URL[]{urlB},?Thread.currentThread()
????????????????.getContextClassLoader());
????????Object?result?=?null;
????????String[]?clazz?=?classImpl.getClazz();
????????for(String?claName?:?clazz){
????????????Class>?loadClass?=?classLoaderA.loadClass(claName);
????????????if(Objects.isNull(loadClass)){
????????????????continue;
????????????}
????????????//獲取實(shí)例
????????????Object?obj?=?loadClass.newInstance();
????????????Map?map?=?new?HashMap();
????????????//獲取方法
????????????Method?method=loadClass.getDeclaredMethod("sendMsg",Map.class);
????????????result?=?method.invoke(obj,map);
????????????if(Objects.nonNull(result)){
????????????????break;
????????????}
????????}
????????return?result;
????}
?
?
????public?static?String?invokeMethod(String?text)?throws?Exception{
????????String?path?=?"E:\\code-self\\bitzpp\\lib\\miz-pt-1.0-SNAPSHOT.jar";
????????File?f?=?new?File(path);
????????URL?urlB?=?f.toURI().toURL();
????????URLClassLoader?classLoaderA?=?new?URLClassLoader(new?URL[]{urlB},?Thread.currentThread()
????????????????.getContextClassLoader());
????????Class>?product?=?classLoaderA.loadClass("com.congge.spi.MizptImpl");
????????//獲取實(shí)例
????????Object?obj?=?product.newInstance();
????????Map?map?=?new?HashMap();
????????//獲取方法
????????Method?method=product.getDeclaredMethod("sendMsg",Map.class);
????????//執(zhí)行方法
????????Object?result1?=?method.invoke(obj,map);
????????//?TODO?According?to?the?requirements?,?write?the?implementation?code.
????????return?result1.toString();
????}
?
????public?static?String?getApplicationFolder()?{
????????String?path?=?ServiceLoaderUtils.class.getProtectionDomain().getCodeSource().getLocation().getPath();
????????return?new?File(path).getParent();
????}
?
?
?
}
2.3.3 添加測(cè)試接口
@GetMapping("/sendMsgV2")
public?String?index()?throws?Exception?{
????String?result?=?serviceLoaderUtils.doExecuteMethod();
????return?result;
}

三、SpringBoot中的插件化實(shí)現(xiàn)
spring.factories的實(shí)現(xiàn);3.1 Spring Boot中的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)原理
SpringFactoriesLoader類,這個(gè)類實(shí)現(xiàn)了檢索META-INF/spring.factories文件,并獲取指定接口的配置的功能。在這個(gè)類中定義了兩個(gè)對(duì)外的方法:loadFactories 根據(jù)接口類獲取其實(shí)現(xiàn)類的實(shí)例,這個(gè)方法返回的是對(duì)象列表; loadFactoryNames 根據(jù)接口獲取其接口類的名稱,這個(gè)方法返回的是類名的列表;
spring.factories文件,并解析得到類名列表,具體代碼如下:public?static?List
?loadFactoryNames(Class>?factoryClass,?ClassLoader?classLoader)?{
????String?factoryClassName?=?factoryClass.getName();
????try?{
????????Enumeration?urls?=?(classLoader?!=?null???classLoader.getResources(FACTORIES_RESOURCE_LOCATION)?:
????????????????ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
????????List?result?=?new?ArrayList ();
????????while?(urls.hasMoreElements())?{
????????????URL?url?=?urls.nextElement();
????????????Properties?properties?=?PropertiesLoaderUtils.loadProperties(new?UrlResource(url));
????????????String?factoryClassNames?=?properties.getProperty(factoryClassName);
????????????result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
????????}
????????return?result;
????}
????catch?(IOException?ex)?{
????????throw?new?IllegalArgumentException("Unable?to?load?["?+?factoryClass.getName()?+
????????????????"]?factories?from?location?["?+?FACTORIES_RESOURCE_LOCATION?+?"]",?ex);
????}
}
spring.factories文件,就是說我們可以在自己的jar中配置spring.factories文件,不會(huì)影響到其它地方的配置,也不會(huì)被別人的配置覆蓋。spring.factories的是通過Properties解析得到的,所以我們?cè)趯懳募械膬?nèi)容都是安裝下面這種方式配置的:com.xxx.interface=com.xxx.classname
3.3 Spring Factories案例實(shí)現(xiàn)
3.3.1 定義一個(gè)服務(wù)接口
public?interface?SmsPlugin?{
?
????public?void?sendMessage(String?message);
?
}
3.3.2 定義2個(gè)服務(wù)實(shí)現(xiàn)
public?class?BizSmsImpl?implements?SmsPlugin?{
?
????@Override
????public?void?sendMessage(String?message)?{
????????System.out.println("this?is?BizSmsImpl?sendMessage..."?+?message);
????}
}
public?class?SystemSmsImpl?implements?SmsPlugin?{
?
????@Override
????public?void?sendMessage(String?message)?{
????????System.out.println("this?is?SystemSmsImpl?sendMessage..."?+?message);
????}
}
3.3.3 添加spring.factories文件
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 添加自定義接口
SpringFactoriesLoader去加載服務(wù);@GetMapping("/sendMsgV3")
public?String?sendMsgV3(String?msg)?throws?Exception{
????List?smsServices=?SpringFactoriesLoader.loadFactories(SmsPlugin.class,?null);
????for(SmsPlugin?smsService?:?smsServices){
????????smsService.sendMessage(msg);
????}
????return?"success";
}
localhost:8087/sendMsgV3?msg=hello,通過控制臺(tái),可以看到,這種方式能夠正確獲取到系統(tǒng)中可用的服務(wù)實(shí)現(xiàn);四、插件化機(jī)制案例實(shí)戰(zhàn)
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)
4.1.2 整體實(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包,或者安裝到倉(cāng)庫(kù)中; 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ù)接口
public?interface?MessagePlugin?{
?
????public?String?sendMsg(Map?msgMap);
?
}
4.2.2 打成jar包并安裝到倉(cāng)庫(kù)
4.2.3 自定義服務(wù)加載工具類
import?com.congge.plugin.spi.MessagePlugin;
import?com.congge.spi.BitptImpl;
import?com.congge.spi.MizptImpl;
?
import?java.util.*;
?
public?class?PluginFactory?{
?
????public?void?installPlugin(){
????????Map?context?=?new?LinkedHashMap();
????????context.put("_userId","");
????????context.put("_version","1.0");
????????context.put("_type","sms");
????????ServiceLoader?serviceLoader?=?ServiceLoader.load(MessagePlugin.class);
????????Iterator?iterator?=?serviceLoader.iterator();
????????while?(iterator.hasNext()){
????????????MessagePlugin?messagePlugin?=?iterator.next();
????????????messagePlugin.sendMsg(context);
????????}
????}
?
????public?static?MessagePlugin?getTargetPlugin(String?type){
????????ServiceLoader?serviceLoader?=?ServiceLoader.load(MessagePlugin.class);
????????Iterator?iterator?=?serviceLoader.iterator();
????????List?messagePlugins?=?new?ArrayList<>();
????????while?(iterator.hasNext()){
????????????MessagePlugin?messagePlugin?=?iterator.next();
????????????messagePlugins.add(messagePlugin);
????????}
????????MessagePlugin?targetPlugin?=?null;
????????for?(MessagePlugin?messagePlugin?:?messagePlugins)?{
????????????boolean?findTarget?=?false;
????????????switch?(type)?{
????????????????case?"aliyun":
????????????????????if?(messagePlugin?instanceof?BitptImpl){
????????????????????????targetPlugin?=?messagePlugin;
????????????????????????findTarget?=?true;
????????????????????????break;
????????????????????}
????????????????case?"tencent":
????????????????????if?(messagePlugin?instanceof?MizptImpl){
????????????????????????targetPlugin?=?messagePlugin;
????????????????????????findTarget?=?true;
????????????????????????break;
????????????????????}
????????????}
????????????if(findTarget)?break;
????????}
????????return?targetPlugin;
????}
?
????public?static?void?main(String[]?args)?{
????????new?PluginFactory().installPlugin();
????}
?
?
}
4.2.4 自定義接口
@RestController
public?class?SmsController?{
?
????@Autowired
????private?SmsService?smsService;
?
????@Autowired
????private?ServiceLoaderUtils?serviceLoaderUtils;
?
????//localhost:8087/sendMsg?msg=sendMsg
????@GetMapping("/sendMsg")
????public?String?sendMessage(String?msg){
????????return?smsService.sendMsg(msg);
????}
?
}
4.2.5 接口實(shí)現(xiàn)
@Service
public?class?SmsService?{
?
????@Value("${msg.type}")
????private?String?msgType;
?
????@Autowired
????private?DefaultSmsService?defaultSmsService;
?
????public?String?sendMsg(String?msg)?{
????????MessagePlugin?messagePlugin?=?PluginFactory.getTargetPlugin(msgType);
????????Map?paramMap?=?new?HashMap();
????????if(Objects.nonNull(messagePlugin)){
????????????return?messagePlugin.sendMsg(paramMap);
????????}
????????return?defaultSmsService.sendMsg(paramMap);
????}
}
4.2.6 添加服務(wù)依賴
????
????????org.springframework.boot
????????spring-boot-starter-web
????
????
????
????????com.congge
????????biz-pt
????????1.0-SNAPSHOT
????
????
????????com.congge
????????miz-pt
????????1.0-SNAPSHOT
????
????
????????org.projectlombok
????????lombok
????
4.3 bizpt 關(guān)鍵代碼實(shí)現(xiàn)過程

4.3.1 添加對(duì)biz-app的jar的依賴
????
????????com.congge
????????biz-app
????????1.0-SNAPSHOT
????
4.3.2 添加MessagePlugin接口的實(shí)現(xiàn)
public?class?BitptImpl?implements?MessagePlugin?{
?
????@Override
????public?String?sendMsg(Map?msgMap)?{
????????Object?userId?=?msgMap.get("userId");
????????Object?type?=?msgMap.get("_type");
????????//TODO?參數(shù)校驗(yàn)
????????System.out.println("?====?userId?:"?+?userId?+?",type?:"?+?type);
????????System.out.println("aliyun?send?message?success");
????????return?"aliyun?send?message?success";
????}
}
4.3.3 添加SPI配置文件
com.congge.spi.BitptImpl
4.3.4 將jar安裝到倉(cāng)庫(kù)中
4.4 效果演示
localhost:8087/sendMsg?msg=sendMsg,可以看到如下效果

五、寫在文末
推薦閱讀:
美團(tuán)一面:MyBatis 的 3 種分頁(yè),還有誰(shuí)不會(huì)?
互聯(lián)網(wǎng)初中高級(jí)大廠面試題(9個(gè)G) 內(nèi)容包含Java基礎(chǔ)、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬(wàn)并發(fā)、消息隊(duì)列、高性能緩存、反射、Spring全家桶原理、微服務(wù)、Zookeeper......等技術(shù)棧!
?戳閱讀原文領(lǐng)取!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??朕已閱?


