SpringBoot 插件化开发模式,强烈推荐!

SpringBoot 插件化开发尝试 这里使用Java SPI的方式来实现插件化,而开发的插件是一般的java Project。至于更丰富的SpringBoot插件,不一定适用。 在STS新建一个java project,然后,稍微改下.classpath的文件,将文件目录改成通常的形式: <classpathentry kind="src" path="src/main/java"/> <classpathentry kind="src" path="src/main/resources"/> 然后新建一个I. 阅读详情

点击上方“芋道源码”,选择“设为星标”

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:blog.youkuaiyun.com/zhangcongyi420

/article/details/131139599

30db2dfa4fcbc957e1a2e4110c36d82a.jpeg


一、前言

插件化开发模式正在很多编程语言或技术框架中得以广泛的应用实践,比如大家熟悉的jenkins,docker可视化管理平台rancher,以及日常编码使用的编辑器idea,vscode等,随处可见的带有热插拔功能的插件,让系统像插了翅膀一样,大大提升了系统的扩展性和伸缩性,也拓展了系统整体的使用价值,那么为什么要使用插件呢?

1.1 使用插件的好处
1.1.1 模块解耦

实现服务模块之间解耦的方式有很多,但是插件来说,其解耦的程度似乎更高,而且更灵活,可定制化、个性化更好。

举例来说,代码中可以使用设计模式来选择使用哪种方式发送短信给下单完成的客户,问题是各个短信服务商并不一定能保证在任何情况下都能发送成功,怎么办呢?这时候设计模式也没法帮你解决这个问题,如果使用定制化插件的方式,结合外部配置参数,假设系统中某种短信发送不出去了,这时候就可以利用插件动态植入,切换为不同的厂商发短信了。

1.1.2 提升扩展性和开放性

以spring来说,之所以具备如此广泛的生态,与其自身内置的各种可扩展的插件机制是分不开的,试想为什么使用了spring框架之后可以很方便的对接其他中间件,那就是spring框架提供了很多基于插件化的扩展点。

插件化机制让系统的扩展性得以提升,从而可以丰富系统的周边应用生态。

1.1.3 方便第三方接入

有了插件之后,第三方应用或系统如果要对接自身的系统,直接基于系统预留的插件接口完成一套适合自己业务的实现即可,而且对自身系统的侵入性很小,甚至可以实现基于配置参数的热加载,方便灵活,开箱即用。

1.2 插件化常用实现思路

以java为例,这里结合实际经验,整理一些常用的插件化实现思路:

  • spi机制;

  • 约定配置和目录,利用反射配合实现;

  • springboot中的Factories机制;

  • java agent(探针)技术;

  • spring内置扩展点;

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

  • spring aop技术;

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

二、Java常用插件实现方案

2.1 serviceloader方式

serviceloader是java提供的spi模式的实现。按照接口开发实现类,而后配置,java通过ServiceLoader来实现统一接口不同实现的依次调用。而java中最经典的serviceloader的使用就是Java的spi机制。

2.1.1 java spi

SPI全称 Service Provider Interface ,是JDK内置的一种服务发现机制,SPI是一种动态替换扩展机制,比如有个接口,你想在运行时动态给他添加实现,你只需按照规范给他添加一个实现类即可。比如大家熟悉的jdbc中的Driver接口,不同的厂商可以提供不同的实现,有mysql的,也有oracle的,而Java的SPI机制就可以为某个接口寻找服务的实现。

下面用一张简图说明下SPI机制的原理

4d789e55b178c87bea4754e876ef8023.png
2.1.2 java spi 简单案例

如下工程目录,在某个应用工程中定义一个插件接口,而其他应用工程为了实现这个接口,只需要引入当前工程的jar包依赖进行实现即可,这里为了演示我就将不同的实现直接放在同一个工程下;

f3b53d473b6f6f2051a816e24ab1dae6.png

定义接口

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

在resources目录按照规范要求创建文件目录,并填写实现类的全类名

bd69a738ee9e9a28f5521bca6930b6b3.png

自定义服务加载类

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

运行上面的程序后,可以看到下面的效果,这就是说,使用ServiceLoader的方式可以加载到不同接口的实现,业务中只需要根据自身的需求,结合配置参数的方式就可以灵活的控制具体使用哪一个实现。

d75691dfd7be27981c380db3a282bbc2.png
2.2 自定义配置约定方式

serviceloader其实是有缺陷的,在使用中必须在META-INF里定义接口名称的文件,在文件中才能写上实现类的类名,如果一个项目里插件化的东西比较多,那很可能会出现越来越多配置文件的情况。所以在结合实际项目使用时,可以考虑下面这种实现思路:

  • A应用定义接口;

  • B,C,D等其他应用定义服务实现;

  • B,C,D应用实现后达成SDK的jar;

  • A应用引用SDK或者将SDK放到某个可以读取到的目录下;

  • A应用读取并解析SDK中的实现类;

在上文中案例基础上,我们做如下调整;

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 自定义测试接口

使用上述的封装对象通过类加载的方式动态的在程序中引入

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 启动类
@EnableConfigurationProperties({ClassImpl.class})
@SpringBootApplication
public class PluginApp {

    public static void main(String[] args) {
        SpringApplication.run(PluginApp.class,args);
    }

}

启动工程代码后,调用接口:localhost:8081/sendMsg,在控制台中可以看到下面的输出信息,即通过这种方式也可以实现类似serviceloader的方式,不过在实际使用时,可以结合配置参数进行灵活的控制;

09c1d9fd30e1c2b6a8d5daa47a30f3f9.png
2.3 自定义配置读取依赖jar的方式

更进一步,在很多场景下,可能我们并不想直接在工程中引入接口实现的依赖包,这时候可以考虑通过读取指定目录下的依赖jar的方式,利用反射的方式进行动态加载,这也是生产中一种比较常用的实践经验。

具体实践来说,主要为下面的步骤:

  • 应用A定义服务接口;

  • 应用B,C,D等实现接口(或者在应用内部实现相同的接口);

  • 应用B,C,D打成jar,放到应用A约定的读取目录下;

  • 应用A加载约定目录下的jar,通过反射加载目标方法;

在上述的基础上,按照上面的实现思路来实现一下;

2.3.1 创建约定目录

在当前工程下创建一个lib目录,并将依赖的jar放进去

5e1b918e35cd8ade66d415deeab6b534.png
2.3.2 新增读取jar的工具类

添加一个工具类,用于读取指定目录下的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;
                //执行反射相关的方法
                //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;
                        }
                        //获取实例
                        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;
                        }
                        //获取实例
                        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;
            }
            //获取实例
            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");
        //获取实例
        Object obj = product.newInstance();
        Map map = new HashMap();
        //获取方法
        Method method=product.getDeclaredMethod("sendMsg",Map.class);
        //执行方法
        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 添加测试接口

添加如下测试接口

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

以上全部完成之后,启动工程,测试一下该接口,仍然可以得到预期结果;

ed320310b31b2f1e3fc18af898448d6f.png

在上述的实现中还是比较粗糙的,实际运用时,还需要做较多的优化改进以满足实际的业务需要,比如接口传入类型参数用于控制具体使用哪个依赖包的方法进行执行等;

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

三、SpringBoot中的插件化实现

在大家使用较多的springboot框架中,其实框架自身提供了非常多的扩展点,其中最适合做插件扩展的莫过于spring.factories的实现;

3.1 Spring Boot中的SPI机制

在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化,这种自定义的SPI机制是Spring Boot Starter实现的基础。

3.2 Spring Factories实现原理

spring-core包里定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:

  • loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表;

  • loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表;

上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下:

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

从代码中我们可以知道,在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件,就是说我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖。

spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:

com.xxx.interface=com.xxx.classname

如果一个接口希望配置多个实现类,可以使用’,’进行分割

3.3 Spring Factories案例实现

接下来看一个具体的案例实现来体验下Spring Factories的使用;

3.3.1 定义一个服务接口

自定义一个接口,里面添加一个方法;

public interface SmsPlugin {

    public void sendMessage(String message);

}
3.3.2 定义2个服务实现

实现类1

public class BizSmsImpl implements SmsPlugin {

    @Override
    public void sendMessage(String message) {
        System.out.println("this is BizSmsImpl sendMessage..." + message);
    }
}

实现类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目录下,创建一个名叫:META-INF的目录,然后在该目录下定义一个spring.factories的配置文件,内容如下,其实就是配置了服务接口,以及两个实现类的全类名的路径;

com.congge.plugin.spi.SmsPlugin=\
        com.congge.plugin.impl.SystemSmsImpl,\
        com.congge.plugin.impl.BizSmsImpl
3.3.4 添加自定义接口

添加一个自定义的接口,有没有发现,这里和java 的spi有点类似,只不过是这里换成了SpringFactoriesLoader去加载服务;

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

启动工程之后,调用一下该接口进行测试,localhost:8087/sendMsgV3?msg=hello,通过控制台,可以看到,这种方式能够正确获取到系统中可用的服务实现;

15d43cd33376e5c8f325620b8a668656.png

利用spring的这种机制,可以很好的对系统中的某些业务逻辑通过插件化接口的方式进行扩展实现;

四、插件化机制案例实战

结合上面掌握的理论知识,下面基于Java SPI机制进行一个接近真实使用场景的完整的操作步骤;

4.1 案例背景
  • 3个微服务模块,在A模块中有个插件化的接口;

  • 在A模块中的某个接口,需要调用插件化的服务实现进行短信发送;

  • 可以通过配置文件配置参数指定具体的哪一种方式发送短信;

  • 如果没有加载到任何插件,将走A模块在默认的发短信实现;

4.1.1 模块结构

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

2、bitpt,aliyun短信发送实现;

3、miz-pt,tencent短信发送实现;

4.1.2 整体实现思路

本案例完整的实现思路参考如下:

  • biz-pp定义服务接口,并提供出去jar被其他实现工程依赖;

  • bitpt与miz-pt依赖biz-pp的jar并实现SPI中的方法;

  • bitpt与miz-pt按照API规范实现完成后,打成jar包,或者安装到仓库中;

  • biz-pp在pom中依赖bitpt与miz-pt的jar,或者通过启动加载的方式即可得到具体某个实现;

4.2 biz-pp 关键代码实现过程
4.2.1 添加服务接口
public interface MessagePlugin {

    public String sendMsg(Map msgMap);

}
4.2.2 打成jar包并安装到仓库

这一步比较简单就不展开了

4.2.3 自定义服务加载工具类

这个类,可以理解为在真实的业务编码中,可以根据业务定义的规则,具体加载哪个插件的实现类进行发送短信的操作;

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 接口实现
@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 添加服务依赖

在该模块中,需要引入对具体实现的两个工程的jar依赖(也可以通过启动加载的命令方式)

<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--依赖具体的实现-->
<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的核心代码实现就到此结束了,后面再具体测试的时候再继续;

4.3 bizpt 关键代码实现过程

接下来就是插件化机制中具体的SPI实现过程,两个模块的实现步骤完全一致,挑选其中一个说明,工程目录结构如下:

db29e4df123d6a0f77c5df070746934c.png
4.3.1 添加对biz-app的jar的依赖

将上面biz-app工程打出来的jar依赖过来

<dependencies>
<dependency>
<groupId>com.congge</groupId>
<artifactId>biz-app</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
4.3.2 添加MessagePlugin接口的实现
public class BitptImpl implements MessagePlugin {

    @Override
    public String sendMsg(Map msgMap) {
        Object userId = msgMap.get("userId");
        Object type = msgMap.get("_type");
        //TODO 参数校验
        System.out.println(" ==== userId :" + userId + ",type :" + type);
        System.out.println("aliyun send message success");
        return "aliyun send message success";
    }
}
4.3.3 添加SPI配置文件

按照前文的方式,在resources目录下创建一个文件,注意文件名称为SPI中的接口全名,文件内容为实现类的全类名

com.congge.spi.BitptImpl
4.3.4 将jar安装到仓库中

完成实现类的编码后,通过maven命令将jar安装到仓库中,然后再在上一步的biz-app中引入即可;

4.4 效果演示

启动biz-app服务,调用接口:localhost:8087/sendMsg?msg=sendMsg,可以看到如下效果

3a758d47c453c01007730a46dd1234f0.png

为什么会出现这个效果呢?因为我们在实现类配置了具体使用哪一种方式进行短信的发送,而加载插件的时候正好能够找到对应的服务实现,这样的话就给当前的业务提供了一个较好的扩展点。

27c192b61ab50467ea8d0cbfd6effa86.png

五、写在文末

从当前的趋势来看,插件化机制的思想已经遍布各种编程语言,框架,中间件,开源工具等领域,因此掌握插件化的实现机制对于当下做程序实现,或架构设计方面都有着很重要的意义,值得深入研究,本篇到此结束,感谢观看!



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:

85ed7ebdf485d8265b17885ca07a06e6.png

已在知识星球更新源码解析如下:

1093505f8e2550202fefdb2390694cb4.jpeg

7a0b7da7b0083983a860abc1db7ed41c.jpeg

93d0532a0fb72dd71cde6797fc5b66f6.jpeg

1968ebef352d7ac32c8b17eb106f1678.jpeg

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
Kunpeng框架插件开发实战:从零构建Spring Boot Actuator漏洞检测插件 在网络安全领域,插件化框架是实现自动化漏洞检测的核心技术。其原理是通过模块化设计,将安全专家的经验固化为可复用的检测脚本,从而提升安全评估效率。这种技术价值在于能够将复杂的手工验证过程转化为标准化、可批量执行的自动化流程,广泛应用于企业内部安全巡检、红队评估等场景。以Kunpeng框架为例,其插件机制围绕POCBase基类展开,开发者通过实现_verify方法,将针对特定漏洞的检测逻辑,如Spring Boot Actuator未授权访问,封装成独立的插件。本文聚焦于Kunpeng框架的二次开发,详细拆解了 阅读详情

相关推荐

SpringBoot 插件化开发详细总结

SpringBoot 插件化开发方案总结

congge 1万+

Spring Boot 如何实现插件化开发模式

程序员的成长之路互联网/程序员/技术/资料共享关注阅读本文大概需要 20分钟。来自:blog.youkuaiyun.com/zhangcongyi420/article/details/131139599一、前言插件化开发模式正在很多编程语言或技术框架中得以广泛的应用实践,比如大家熟悉的jenkins,docker可视化管理平台rancher,以及日常编码使用的编辑器idea,vscode等,随处可见的带...

良月柒 1561

spring-plugin 插件化开发

Spring Plugin 是一个允许你为 Spring 应用程序定义插件接口并在运行时动态加载插件实现的库。supports 方法是 Plugin 接口提供的,用于寻找执行类。定义 PayServicePlugin 继承 Plugin 接口。实现 PayServicePlugin 接口​​​​​​​。可以看到传入不同的 name 就可以动态切换支付方式。PayContext 上下文​​​​​​​。

qq_30895747的博客 822

Spring Boot插件化开发概念原理及实现

插件化开发是Spring Boot中强大而灵活的开发模式,可以将应用程序的功能模块解耦、实现动态扩展。通过Spring框架提供的特性和机制,可以在Spring Boot项目中实现插件化开发。插件化开发的好处包括模块解耦、动态扩展和可维护性。创建插件项目,作为独立的模块,实现特定的功能。定义插件接口或抽象类,规范插件的功能和提供必要的方法。在插件项目中实现插件,包括功能的具体实现和所需的配置。将插件打包为可执行的Jar文件,并定义插件的配置文件。创建主应用程序,用于加载和管理插件。

各类程序设计成品、文档、学习资料 3200

【AI智能体】Claude Code +Superpowers 项目实战操作详解

Claude Code +Superpowers 项目实战操作详解

congge 4055

高效IT运维报告编写指南:实战经验与模板分享

本文提供了一份全面的IT运维报告编写指南,涵盖实战经验与实用模板分享。文章详细解析了高效运维报告的核心要素,包括结构设计、数据收集自动化和可视化技巧,并提供了月度报告和故障分析报告模板。通过自动化工具和写作技巧,帮助团队提升报告质量,支持决策和问题追踪。

u5v6w7的博客 930

Spring Boot 插件化开发模式

前两天在办公室喝咖啡的时候,我跟隔壁组那个老王瞎扯...他突然问我一个事儿,说他最近在搞一个“插件化”的Spring Boot项目,问我有没有用过。后来就决定搞成插件化模式——每个业务模块做成一个插件,想部署哪个就挂哪个,不用打一个大包,还能热插拔,贼爽。举个例子,比如支付这块,是我们“支付组”写的,它就是个插件包,核心框架只负责启动和注册插件,其他啥也不管,典型的“宿主-插件”结构。宿主项目在启动的时候会遍历插件目录,把所有插件JAR加载进来,然后扫描里面的Spring配置。我们是每个插件一个独立的。

m0_73735578的博客 340

高等数学/概率论/数理统计/线性代数/离散数学面试-核心概念-问题理解

特征值与特征向量是线性代数中的概念,它们与线性变换有关。在线性变换中,如果存在非零向量 v ,使得当该向量通过变换后,其方向不变,只是长度发生了变化,那么这个向量 v 就是特征向量。而这个长度变化的比例因子,即变换前后向量长度的比值,称为特征值。数学公式表示:通俗易懂的解释:想象一下,你有一个可以伸缩的橡皮筋,这个橡皮筋可以代表一个向量。现在,你有一个特殊的机器,它可以改变橡皮筋的形状,但不会改变它延伸的方向。当你把橡皮筋放入机器中,机器会拉伸或压缩橡皮筋,但橡皮筋的方向保持不变。

m0_64198455的博客 2294

炸裂!强烈推荐一个 RuoYi-Plus 增强版

RuoYi-Vue-Plus 是面向分布式集群与多租户场景的企业级后台框架,在 RuoYi-Vue 基础上实现了技术栈升级、架构解耦和功能扩展,适合需要处理复杂权限、多终端接入、分布式部署的业务场景,且开源免费、文档完善,是学习和商用的优质选择。

m0_73735578的博客 1173

ComfyUI 的 LoRA 训练:秋叶 LoRA-scripts 参数设置与测试方法详解

秋叶 LoRA-scripts 的参数设置需结合理论(如模型原理、优化器机制)和硬件条件(显存、算力),新手可先按推荐值起步,再逐步调整。测试时,loss 值帮你判断训练是否正常,XYZ 图表法帮你找到最佳使用参数。掌握这些知识,你就能避开 LoRA 训练的常见坑,从 “盲目调参” 变为 “精准训练”。

2301_78150483的博客 2403

第四章 自定义组件、动画

第三章 自定义组件、动画 (一)自定义View 1.1) onMeasure:对当前View的尺寸进行测量 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) widthMeasureSpec和heightMeasureSpec包含测量模式和尺寸大小 int widthMode = MeasureSpec....

Stay Hungry Stay Foolish 求知若饥 虚心若愚 8134

公司新来了个架构师:我的代码不需要做注释,所谓的注释,都是给垃圾代码用的!...

往期热门文章:1、微服务的血与泪,从拆分到合并2、SpringBoot 插件化开发模式,强烈推荐!3、京东二面:线程池中的线程抛出了异常,该如何处理?大部分人都会答错!4、利用 Function 接口告别冗余(屎山)代码5、搭建静态网站竟然有这么多方案,而且还如此简单部门新来了个架构师,BAT背景,住在三环,开宝马上班,有车位。小伙话不多,但一旦说话斩钉截铁,带着无法撼动的自信。原因就是,有他着数...

Java后端技术 179

腾讯股票接口API(1)——根据股票代码获取详情

获取股票简要信息,找来找去也没有找到官方的文档,都是复制粘贴的,这里先简单记录一下。 1.简要信息 以贵州茅台为例: http://qt.gtimg.cn/q=s_sh600519 // sh600519是贵州茅台的股票代码,根据自己的关键字拼接URL 可以直接在Android项目中请求,会得到如下一串字符串: v_s_sh600519="1~贵州茅台~600519~2075.95~42.95~2.11~38930~794942~~26078.04~GP-A"; 这里v_s代表简要信息,sh600519

GeofferySun的专栏 6万+

SpringBoot 插件化开发

serviceloader其实是有缺陷的,在使用中必须在META-INF里定义接口名称的文件,在文件中才能写上实现类的类名,如果一个项目里插件化的东西比较多,那很可能会出现越来越多配置文件的情况。所以在结合实际项目使用时,可以考虑下面这种实现思路:A应用定义接口;B,C,D等其他应用定义服务实现;B,C,D应用实现后达成SDK的jar;A应用引用SDK或者将SDK放到某个可以读取到的目录下;A应用读取并解析SDK中的实现类;在上文中案例基础上,我们做如下调整;

tangjieqing的博客 1314

【点云鸟瞰图BEV】点云鸟瞰图BEV原理与可视化

本节将介绍激光雷达等点云的鸟瞰图生成原理、代码及效果图。 1 BEV视图原理 点云BEV(Bird's Eye View)视图是指点云在垂直于高度方向的平面上的投影。通常,在获得bev视图前,会将空间分割成体素,利用体素对点云进行下采样,然后将每个体素作为一个点进行投影。体素是指将三维空间按照固定尺寸长方体进行划分时的长方体(Δl*Δw*Δh)。 体素投影时可以得到BEV视图的像素点坐标。每个像素点的特征取值则可以通过多种方式得到。 ...

叶子的博客 2万+

融合Retinex理论与Transformer的YOLOv8低光照目标检测增强方法【YOLOv8】

图像分解:将输入图像分解为光照成分和反射成分。光照估计:估计光照图以消除暗光区域。图像重建:将光照增强后的反射图像重建成最终图像。Retinexformer通过在Transformer中引入Retinex的图像增强思路,使得低光照图像的细节能够得到充分保留。本文提出了一种基于Retinexformer的低光照增强方法,用于改进YOLOv8模型在夜间和低光照环境下的目标检测性能。

博客 1931
上一篇: 腾讯捐赠Java编译器基础软件
下一篇: Spring 赌上未来的一击,响应式的 WebFlux 框架更优雅,性能更强!
公众号-芋道源码
博客等级 码龄10年 1636粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值