<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Spring Boot 如何實(shí)現(xiàn)插件化開發(fā)模式

          共 14935字,需瀏覽 30分鐘

           ·

          2023-06-20 09:42

          程序員的成長(zhǎng)之路
          互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享?
          關(guān)注


          閱讀本文大概需要 20?分鐘。

          來(lái)自:blog.csdn.net/zhangcongyi420/article/details/131139599

          一、前言

          插件化開發(fā)模式正在很多編程語(yǔ)言或技術(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ù)模塊之間解耦的方式有很多,但是插件來(lái)說,其解耦的程度似乎更高,而且更靈活,可定制化、個(gè)性化更好。
          舉例來(lái)說,代碼中可以使用設(shè)計(jì)模式來(lái)選擇使用哪種方式發(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來(lái)說,之所以具備如此廣泛的生態(tài),與其自身內(nèi)置的各種可擴(kuò)展的插件機(jī)制是分不開的,試想為什么使用了spring框架之后可以很方便的對(duì)接其他中間件,那就是spring框架提供了很多基于插件化的擴(kuò)展點(diǎn)。
          插件化機(jī)制讓系統(tǒng)的擴(kuò)展性得以提升,從而可以豐富系統(tǒng)的周邊應(yīng)用生態(tài)。

          1.1.3 方便第三方接入

          有了插件之后,第三方應(yīng)用或系統(tǒng)如果要對(duì)接自身的系統(tǒng),直接基于系統(tǒng)預(yù)留的插件接口完成一套適合自己業(yè)務(wù)的實(shí)現(xiàn)即可,而且對(duì)自身系統(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來(lái)實(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)。
          下面用一張簡(jiǎn)圖說明下SPI機(jī)制的原理
          圖片

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

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

          public?interface?MessagePlugin?{
          ?
          ????public?String?sendMsg(Map?msgMap);
          ?
          }

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

          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";
          ????}
          }

          在resources目錄按照規(guī)范要求創(chuàng)建文件目錄,并填寫實(shí)現(xiàn)類的全類名
          圖片
          自定義服務(wù)加載類

          ?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);
          ????????}
          ????}

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

          2.2 自定義配置約定方式

          serviceloader其實(shí)是有缺陷的,在使用中必須在META-INF里定義接口名稱的文件,在文件中才能寫上實(shí)現(xiàn)類的類名,如果一個(gè)項(xiàng)目里插件化的東西比較多,那很可能會(huì)出現(xiàn)越來(lái)越多配置文件的情況。所以在結(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)類封裝到類對(duì)象中,方便后續(xù)使用;

          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è)試接口

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

          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);
          ????}
          ?
          }

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

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

          更進(jìn)一步,在很多場(chǎng)景下,可能我們并不想直接在工程中引入接口實(shí)現(xiàn)的依賴包,這時(shí)候可以考慮通過讀取指定目錄下的依賴jar的方式,利用反射的方式進(jìn)行動(dòng)態(tài)加載,這也是生產(chǎn)中一種比較常用的實(shí)踐經(jīng)驗(yàn)。
          具體實(shí)踐來(lái)說,主要為下面的步驟:
          • 應(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)思路來(lái)實(shí)現(xiàn)一下;

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

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

          2.3.2 新增讀取jar的工具類

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

          @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è)試接口

          添加如下測(cè)試接口

          @GetMapping("/sendMsgV2")
          public?String?index()?throws?Exception?{
          ????String?result?=?serviceLoaderUtils.doExecuteMethod();
          ????return?result;
          }

          以上全部完成之后,啟動(dòng)工程,測(cè)試一下該接口,仍然可以得到預(yù)期結(jié)果;
          圖片
          在上述的實(shí)現(xiàn)中還是比較粗糙的,實(shí)際運(yùn)用時(shí),還需要做較多的優(yōu)化改進(jìn)以滿足實(shí)際的業(yè)務(wù)需要,比如接口傳入類型參數(shù)用于控制具體使用哪個(gè)依賴包的方法進(jìn)行執(zhí)行等;我們創(chuàng)建了一個(gè)高質(zhì)量的技術(shù)交流群,與優(yōu)秀的人在一起,自己也會(huì)優(yōu)秀起來(lái),趕緊點(diǎn)擊加群,享受一起成長(zhǎng)的快樂。

          三、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è)對(duì)外的方法:
          • loadFactories 根據(jù)接口類獲取其實(shí)現(xiàn)類的實(shí)例,這個(gè)方法返回的是對(duì)象列表;
          • loadFactoryNames 根據(jù)接口獲取其接口類的名稱,這個(gè)方法返回的是類名的列表;
          上面的兩個(gè)方法的關(guān)鍵都是從指定的ClassLoader中獲取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);
          ????}
          }

          從代碼中我們可以知道,在這個(gè)方法中會(huì)遍歷整個(gè)ClassLoader中所有jar包下的spring.factories文件,就是說我們可以在自己的jar中配置spring.factories文件,不會(huì)影響到其它地方的配置,也不會(huì)被別人的配置覆蓋。
          spring.factories的是通過Properties解析得到的,所以我們?cè)趯懳募械膬?nèi)容都是安裝下面這種方式配置的:

          com.xxx.interface=com.xxx.classname

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

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

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

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

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

          public?interface?SmsPlugin?{
          ?
          ????public?void?sendMessage(String?message);
          ?
          }

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

          實(shí)現(xiàn)類1
          public?class?BizSmsImpl?implements?SmsPlugin?{
          ?
          ????@Override
          ????public?void?sendMessage(String?message)?{
          ????????System.out.println("this?is?BizSmsImpl?sendMessage..."?+?message);
          ????}
          }
          實(shí)現(xiàn)類2

          public?class?SystemSmsImpl?implements?SmsPlugin?{
          ?
          ????@Override
          ????public?void?sendMessage(String?message)?{
          ????????System.out.println("this?is?SystemSmsImpl?sendMessage..."?+?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")
          public?String?sendMsgV3(String?msg)?throws?Exception{
          ????List?smsServices=?SpringFactoriesLoader.loadFactories(SmsPlugin.class,?null);
          ????for(SmsPlugin?smsService?:?smsServices){
          ????????smsService.sendMessage(msg);
          ????}
          ????return?"success";
          }

          啟動(dòng)工程之后,調(diào)用一下該接口進(jìn)行測(cè)試,localhost:8087/sendMsgV3?msg=hello,通過控制臺(tái),可以看到,這種方式能夠正確獲取到系統(tǒng)中可用的服務(wù)實(shí)現(xiàn);
          ![圖片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
          利用spring的這種機(jī)制,可以很好的對(duì)系統(tǒng)中的某些業(yè)務(wù)邏輯通過插件化接口的方式進(jìn)行擴(kuò)展實(shí)現(xiàn);

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

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

          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包,或者安裝到倉(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ù)

          這一步比較簡(jiǎn)單就不展開了

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

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

          在該模塊中,需要引入對(duì)具體實(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é)束了,后面再具體測(cè)試的時(shí)候再繼續(xù);

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

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

          4.3.1 添加對(duì)biz-app的jar的依賴

          將上面biz-app工程打出來(lái)的jar依賴過來(lái)


          ????
          ????????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配置文件

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

          com.congge.spi.BitptImpl

          4.3.4 將jar安裝到倉(cāng)庫(kù)中

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

          4.4 效果演示

          啟動(dòng)biz-app服務(wù),調(diào)用接口:localhost:8087/sendMsg?msg=sendMsg,可以看到如下效果
          圖片
          為什么會(huì)出現(xiàn)這個(gè)效果呢?因?yàn)槲覀冊(cè)趯?shí)現(xiàn)類配置了具體使用哪一種方式進(jìn)行短信的發(fā)送,而加載插件的時(shí)候正好能夠找到對(duì)應(yīng)的服務(wù)實(shí)現(xiàn),這樣的話就給當(dāng)前的業(yè)務(wù)提供了一個(gè)較好的擴(kuò)展點(diǎn)。
          圖片

          五、寫在文末

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

          推薦閱讀:

          蘋果宣布最新操作系統(tǒng):visionOS

          美團(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)取!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??朕已閱?

          瀏覽 36
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色的艹逼视频网站 | 黄色成人网站在线免费看 | 国产看真人毛片爱做A片 | c逼视频网站| 在线日本a∨|