SpringBoot 插件化開(kāi)發(fā)模式,強(qiáng)烈推薦!
一、前言
插件化開(kāi)發(fā)模式正在很多編程語(yǔ)言或技術(shù)框架中得以廣泛的應(yīng)用實(shí)踐,比如大家熟悉的jenkins,docker可視化管理平臺(tái)rancher,以及日常編碼使用的編輯器idea,vscode等,隨處可見(jiàn)的帶有熱插拔功能的插件,讓系統(tǒng)像插了翅膀一樣,大大提升了系統(tǒng)的擴(kuò)展性和伸縮性,也拓展了系統(tǒng)整體的使用價(jià)值,那么為什么要使用插件呢?
1.1 使用插件的好處
1.1.1 模塊解耦
實(shí)現(xiàn)服務(wù)模塊之間解耦的方式有很多,但是插件來(lái)說(shuō),其解耦的程度似乎更高,而且更靈活,可定制化、個(gè)性化更好。
舉例來(lái)說(shuō),代碼中可以使用設(shè)計(jì)模式來(lái)選擇使用哪種方式發(fā)送短信給下單完成的客戶(hù),問(wèn)題是各個(gè)短信服務(wù)商并不一定能保證在任何情況下都能發(fā)送成功,怎么辦呢?這時(shí)候設(shè)計(jì)模式也沒(méi)法幫你解決這個(gè)問(wèn)題,如果使用定制化插件的方式,結(jié)合外部配置參數(shù),假設(shè)系統(tǒng)中某種短信發(fā)送不出去了,這時(shí)候就可以利用插件動(dòng)態(tài)植入,切換為不同的廠商發(fā)短信了。
1.1.2 提升擴(kuò)展性和開(kāi)放性
以spring來(lái)說(shuō),之所以具備如此廣泛的生態(tài),與其自身內(nèi)置的各種可擴(kuò)展的插件機(jī)制是分不開(kāi)的,試想為什么使用了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ù)的熱加載,方便靈活,開(kāi)箱即用。
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)。按照接口開(kāi)發(fā)實(shí)現(xiàn)類(lèi),而后配置,java通過(guò)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全稱(chēng) 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)類(lèi)即可。比如大家熟悉的jdbc中的Driver接口,不同的廠商可以提供不同的實(shí)現(xiàn),有mysql的,也有oracle的,而Java的SPI機(jī)制就可以為某個(gè)接口尋找服務(wù)的實(shí)現(xiàn)。
下面用一張簡(jiǎn)圖說(shuō)明下SPI機(jī)制的原理
圖片
2.1.2 java spi 簡(jiǎn)單案例
如下工程目錄,在某個(gè)應(yīng)用工程中定義一個(gè)插件接口,而其他應(yīng)用工程為了實(shí)現(xiàn)這個(gè)接口,只需要引入當(dāng)前工程的jar包依賴(lài)進(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)建文件目錄,并填寫(xiě)實(shí)現(xiàn)類(lèi)的全類(lèi)名
圖片
自定義服務(wù)加載類(lèi)
public static void main(String[] args) {
        ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
        Iterator<MessagePlugin> iterator = serviceLoader.iterator();
        Map map = new HashMap();
        while (iterator.hasNext()){
            MessagePlugin messagePlugin = iterator.next();
            messagePlugin.sendMsg(map);
        }
    }運(yùn)行上面的程序后,可以看到下面的效果,這就是說(shuō),使用ServiceLoader的方式可以加載到不同接口的實(shí)現(xiàn),業(yè)務(wù)中只需要根據(jù)自身的需求,結(jié)合配置參數(shù)的方式就可以靈活的控制具體使用哪一個(gè)實(shí)現(xiàn)。
圖片
2.2 自定義配置約定方式
serviceloader其實(shí)是有缺陷的,在使用中必須在META-INF里定義接口名稱(chēng)的文件,在文件中才能寫(xiě)上實(shí)現(xiàn)類(lèi)的類(lèi)名,如果一個(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)類(lèi);
 
在上文中案例基礎(chǔ)上,我們做如下調(diào)整;
2.2.1 添加配置文件
在配置文件中,將具體的實(shí)現(xiàn)類(lèi)配置進(jìn)去
server :
  port : 8081
impl:
  name : com.congge.plugins.spi.MessagePlugin
  clazz :
    - com.congge.plugins.impl.TencentMsg
    - com.congge.plugins.impl.AliyunMsg2.2.2 自定義配置文件加載類(lèi)
通過(guò)這個(gè)類(lèi),將上述配置文件中的實(shí)現(xiàn)類(lèi)封裝到類(lèi)對(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ì)象通過(guò)類(lèi)加載的方式動(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<classImpl.getClazz().length;i++) {
            Class pluginClass= Class.forName(classImpl.getClazz()[i]);
            MessagePlugin messagePlugin = (MessagePlugin) pluginClass.newInstance();
            messagePlugin.sendMsg(new HashMap());
        }
        return "success";
    }
 
}2.2.4 啟動(dòng)類(lèi)
@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)中可以看到下面的輸出信息,即通過(guò)這種方式也可以實(shí)現(xiàn)類(lèi)似serviceloader的方式,不過(guò)在實(shí)際使用時(shí),可以結(jié)合配置參數(shù)進(jìn)行靈活的控制;
圖片
2.3 自定義配置讀取依賴(lài)jar的方式
更進(jìn)一步,在很多場(chǎng)景下,可能我們并不想直接在工程中引入接口實(shí)現(xiàn)的依賴(lài)包,這時(shí)候可以考慮通過(guò)讀取指定目錄下的依賴(lài)jar的方式,利用反射的方式進(jìn)行動(dòng)態(tài)加載,這也是生產(chǎn)中一種比較常用的實(shí)踐經(jīng)驗(yàn)。
具體實(shí)踐來(lái)說(shuō),主要為下面的步驟:
- 應(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,通過(guò)反射加載目標(biāo)方法;
 
在上述的基礎(chǔ)上,按照上面的實(shí)現(xiàn)思路來(lái)實(shí)現(xiàn)一下;
2.3.1 創(chuàng)建約定目錄
在當(dāng)前工程下創(chuàng)建一個(gè)lib目錄,并將依賴(lài)的jar放進(jìn)去
圖片
2.3.2 新增讀取jar的工具類(lèi)
添加一個(gè)工具類(lèi),用于讀取指定目錄下的jar,并通過(guò)反射的方式,結(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()) {
                //獲取文件名稱(chēng)
                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)以滿(mǎn)足實(shí)際的業(yè)務(wù)需要,比如接口傳入類(lèi)型參數(shù)用于控制具體使用哪個(gè)依賴(lài)包的方法進(jìn)行執(zhí)行等;
三、SpringBoot中的插件化實(shí)現(xiàn)
在大家使用較多的springboot框架中,其實(shí)框架自身提供了非常多的擴(kuò)展點(diǎn),其中最適合做插件擴(kuò)展的莫過(guò)于spring.factories的實(shí)現(xiàn);
3.1 Spring Boot中的SPI機(jī)制
在Spring中也有一種類(lèi)似與Java SPI的加載機(jī)制。它在META-INF/spring.factories文件中配置接口的實(shí)現(xiàn)類(lèi)名稱(chēng),然后在程序中讀取這些配置文件并實(shí)例化,這種自定義的SPI機(jī)制是Spring Boot Starter實(shí)現(xiàn)的基礎(chǔ)。
3.2 Spring Factories實(shí)現(xiàn)原理
spring-core包里定義了SpringFactoriesLoader類(lèi),這個(gè)類(lèi)實(shí)現(xiàn)了檢索META-INF/spring.factories文件,并獲取指定接口的配置的功能。在這個(gè)類(lèi)中定義了兩個(gè)對(duì)外的方法:
- loadFactories 根據(jù)接口類(lèi)獲取其實(shí)現(xiàn)類(lèi)的實(shí)例,這個(gè)方法返回的是對(duì)象列表;
 - loadFactoryNames 根據(jù)接口獲取其接口類(lèi)的名稱(chēng),這個(gè)方法返回的是類(lèi)名的列表;
 
上面的兩個(gè)方法的關(guān)鍵都是從指定的ClassLoader中獲取spring.factories文件,并解析得到類(lèi)名列表,具體代碼如下:
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    try {
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<String>();
        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文件,就是說(shuō)我們可以在自己的jar中配置spring.factories文件,不會(huì)影響到其它地方的配置,也不會(huì)被別人的配置覆蓋。
spring.factories的是通過(guò)Properties解析得到的,所以我們?cè)趯?xiě)文件中的內(nèi)容都是安裝下面這種方式配置的:
com.xxx.interface=com.xxx.classname如果一個(gè)接口希望配置多個(gè)實(shí)現(xiàn)類(lèi),可以使用’,’進(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)類(lèi)1
public class BizSmsImpl implements SmsPlugin {
 
    @Override
    public void sendMessage(String message) {
        System.out.println("this is BizSmsImpl sendMessage..." + message);
    }
}實(shí)現(xiàn)類(lèi)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)類(lèi)的全類(lèi)名的路徑;
com.congge.plugin.spi.SmsPlugin=\
com.congge.plugin.impl.SystemSmsImpl,\
com.congge.plugin.impl.BizSmsImpl3.3.4 添加自定義接口
添加一個(gè)自定義的接口,有沒(méi)有發(fā)現(xiàn),這里和java 的spi有點(diǎn)類(lèi)似,只不過(guò)是這里換成了SpringFactoriesLoader去加載服務(wù);
@GetMapping("/sendMsgV3")
public String sendMsgV3(String msg) throws Exception{
    List<SmsPlugin> 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,通過(guò)控制臺(tái),可以看到,這種方式能夠正確獲取到系統(tǒng)中可用的服務(wù)實(shí)現(xiàn);
圖片
利用spring的這種機(jī)制,可以很好的對(duì)系統(tǒng)中的某些業(yè)務(wù)邏輯通過(guò)插件化接口的方式進(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ā)送;
 - 可以通過(guò)配置文件配置參數(shù)指定具體的哪一種方式發(fā)送短信;
 - 如果沒(méi)有加載到任何插件,將走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)工程依賴(lài);
 - bitpt與miz-pt依賴(lài)biz-pp的jar并實(shí)現(xiàn)SPI中的方法;
 - bitpt與miz-pt按照API規(guī)范實(shí)現(xiàn)完成后,打成jar包,或者安裝到倉(cāng)庫(kù)中;
 - biz-pp在pom中依賴(lài)bitpt與miz-pt的jar,或者通過(guò)啟動(dòng)加載的方式即可得到具體某個(gè)實(shí)現(xiàn);
 
4.2 biz-pp 關(guān)鍵代碼實(shí)現(xiàn)過(guò)程
4.2.1 添加服務(wù)接口
public interface MessagePlugin {
 
    public String sendMsg(Map msgMap);
 
}4.2.2 打成jar包并安裝到倉(cāng)庫(kù)
這一步比較簡(jiǎn)單就不展開(kāi)了
4.2.3 自定義服務(wù)加載工具類(lèi)
這個(gè)類(lèi),可以理解為在真實(shí)的業(yè)務(wù)編碼中,可以根據(jù)業(yè)務(wù)定義的規(guī)則,具體加載哪個(gè)插件的實(shí)現(xiàn)類(lèi)進(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<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
        Iterator<MessagePlugin> iterator = serviceLoader.iterator();
        while (iterator.hasNext()){
            MessagePlugin messagePlugin = iterator.next();
            messagePlugin.sendMsg(context);
        }
    }
 
    public static MessagePlugin getTargetPlugin(String type){
        ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
        Iterator<MessagePlugin> iterator = serviceLoader.iterator();
        List<MessagePlugin> 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ù)依賴(lài)
在該模塊中,需要引入對(duì)具體實(shí)現(xiàn)的兩個(gè)工程的jar依賴(lài)(也可以通過(guò)啟動(dòng)加載的命令方式)
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--依賴(lài)具體的實(shí)現(xiàn)-->
    <dependency>
        <groupId>com.congge</groupId>
        <artifactId>biz-pt</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.congge</groupId>
        <artifactId>miz-pt</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>biz-pp的核心代碼實(shí)現(xiàn)就到此結(jié)束了,后面再具體測(cè)試的時(shí)候再繼續(xù);
4.3 bizpt 關(guān)鍵代碼實(shí)現(xiàn)過(guò)程
接下來(lái)就是插件化機(jī)制中具體的SPI實(shí)現(xiàn)過(guò)程,兩個(gè)模塊的實(shí)現(xiàn)步驟完全一致,挑選其中一個(gè)說(shuō)明,工程目錄結(jié)構(gòu)如下:
圖片
4.3.1 添加對(duì)biz-app的jar的依賴(lài)
將上面biz-app工程打出來(lái)的jar依賴(lài)過(guò)來(lái)
<dependencies>
    <dependency>
        <groupId>com.congge</groupId>
        <artifactId>biz-app</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>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è)文件,注意文件名稱(chēng)為SPI中的接口全名,文件內(nèi)容為實(shí)現(xiàn)類(lèi)的全類(lèi)名
com.congge.spi.BitptImpl4.3.4 將jar安裝到倉(cāng)庫(kù)中
完成實(shí)現(xiàn)類(lèi)的編碼后,通過(guò)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)類(lèi)配置了具體使用哪一種方式進(jìn)行短信的發(fā)送,而加載插件的時(shí)候正好能夠找到對(duì)應(yīng)的服務(wù)實(shí)現(xiàn),這樣的話(huà)就給當(dāng)前的業(yè)務(wù)提供了一個(gè)較好的擴(kuò)展點(diǎn)。
圖片
五、寫(xiě)在文末
從當(dāng)前的趨勢(shì)來(lái)看,插件化機(jī)制的思想已經(jīng)遍布各種編程語(yǔ)言,框架,中間件,開(kāi)源工具等領(lǐng)域,因此掌握插件化的實(shí)現(xiàn)機(jī)制對(duì)于當(dāng)下做程序?qū)崿F(xiàn),或架構(gòu)設(shè)計(jì)方面都有著很重要的意義,值得深入研究,本篇到此結(jié)束,感謝觀看!















 
 
 








 
 
 
 