SpringBoot 插件化开发

news2024/11/15 12:01:30

SpringBoot 插件化开发

    • 介绍
    • 使用插件的好处
      • 1 模块解耦
      • 2 提升扩展性和开放性
      • 3 方便第三方接入
    • 插件化常用实现思路
    • Java常用插件实现方案
      • ServiceLoader方式
        • 1 java spi
        • 2 java spi 简单案例
      • 自定义配置约定方式
        • 添加配置文件
        • 自定义配置文件加载类
        • 自定义测试接口
        • 启动类
        • 项目结构
      • 自定义配置读取依赖jar的方式
        • 创建约定目录
        • 新增读取jar的工具类
        • 自定义配置文件加载类
        • 添加测试接口
        • 代码结构
    • SpringBoot中的插件化实现
      • Spring Boot中的SPI机制
      • Spring Factories实现原理
      • Spring Factories案例实现
        • 定义一个服务接口
        • 定义2个服务实现
        • 添加spring.factories文件
        • 添加自定义接口
    • 插件化机制案例实战
      • 案例背景
      • 模块结构
      • 整体实现思路
      • springboot-mry-env-biz-pp 代码实现过程
        • 添加服务接口
        • 打成jar包并安装到仓库
        • 项目结果
      • springboot-mry-env-biz-pt 代码实现过程
        • 引入springboot-mry-env-biz-pp依赖
        • 添加MessagePlugin接口的实现
        • 添加SPI配置文件
        • 将jar安装到仓库中
        • 项目结构
      • springboot-mry-env-miz-pt 代码实现过程
        • 引入springboot-mry-env-biz-pp依赖
        • 添加MessagePlugin接口的实现
        • 添加SPI配置文件
        • 将jar安装到仓库中
        • 项目结构
      • springboot-mry-env-biz-pp-demo 代码实现过程
        • 添加服务依赖
        • 自定义服务加载工具类
        • 接口实现
        • 测试controller
        • 配置文件application.yml
        • 效果演示

介绍

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

使用插件的好处

1 模块解耦

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

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

2 提升扩展性和开放性

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

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

3 方便第三方接入

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

插件化常用实现思路

以java为例,整理一些常用的插件化实现思路:

  • spi机制;
  • 约定配置和目录,利用反射配合实现;
  • springboot中的Factories机制;
  • java agent(探针)技术;
  • spring内置扩展点;
  • 第三方插件包,例如:spring-plugin-core;
  • spring aop技术;

Java常用插件实现方案

ServiceLoader方式

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

1 java spi

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

下面用一张简图说明下SPI机制的原理
在这里插入图片描述

2 java spi 简单案例

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

import java.util.Map;
public interface MessagePlugin {

    public String sendMsg(Map msgMap);

}

2.定义两个不同的实现

import com.mry.plugins.spi.MessagePlugin;
import java.util.Map;

public class AliyunMsg implements MessagePlugin {

    @Override
    public String sendMsg(Map msgMap) {
        System.out.println("aliyun sendMsg");
        return "aliyun sendMsg";
    }

}
import com.mry.plugins.spi.MessagePlugin;
import java.util.Map;

public class TencentMsg implements MessagePlugin {

    @Override
    public String sendMsg(Map msgMap) {
        System.out.println("tencent sendMsg");
        return "tencent sendMsg";
    }

}

3.在resources目录按照规范要求创建文件目录,并填写实现类的全类名
(创建META-INF/services目录)
在这里插入图片描述
4.测试类

import com.mry.plugins.spi.MessagePlugin;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.ServiceLoader;

public class DemoController {

    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的方式可以加载到不同接口的实现,业务中只需要根据自身的需求,结合配置参数的方式就可以灵活的控制具体使用哪一个实现。
在这里插入图片描述

自定义配置约定方式

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

  • A应用定义接口;
  • B,C,D等其他应用定义服务实现;
  • B,C,D应用实现后达成SDK的jar;
  • A应用引用SDK或者将SDK放到某个可以读取到的目录下;
  • A应用读取并解析SDK中的实现类;
    在上文中案例基础上,我们做如下调整;

添加配置文件

在配置文件中,将具体的实现类配置进去

server:
  port: 8866

impl:
  name: com.mry.plugin.spi.MessagePlugin
  clazz:
    - com.mry.plugin.impl.AliyunMsg
    - com.mry.plugin.impl.TencentMsg

自定义配置文件加载类

通过这个类,将上述配置文件中的实现类封装到类对象中,方便后续使用;

package com.mry.plug.config;

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;

}

自定义测试接口

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

package com.mry.plug.controller;

import com.mry.plug.config.ClassImpl;
import com.mry.plugin.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;

    //http://localhost:8866/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";
    }

}

启动类

package com.mry.plug;

import com.mry.plug.config.ClassImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@EnableConfigurationProperties({ClassImpl.class})
@SpringBootApplication
public class SpringbootMrySpiPlugDemoApplication {

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

}

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

项目结构

在这里插入图片描述

自定义配置读取依赖jar的方式

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

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

  • 应用springboot-mry-spi-plug定义服务接口;
  • 应用springboot-mry-biz,springboot-mry-miz等实现接口(或者在应用内部实现相同的接口);
  • 应用springboot-mry-biz,springboot-mry-miz打成jar包,放到应用springboot-mry-iz-demo约定的读取目录下;
  • 应用springboot-mry-iz-demo加载约定目录下的jar,通过反射加载目标方法;

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

创建约定目录

在当前工程下创建一个lib目录,并将依赖的jar放进去
在这里插入图片描述

新增读取jar的工具类

添加一个工具类,用于读取指定目录下的jar,并通过反射的方式,结合配置文件中的约定配置进行反射方法的执行;

package com.mry.plug.utils;

import com.mry.plug.config.ClassImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@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 static void main(String[] args) throws Exception{
        System.out.println(invokeMethod("hello"));
    }

    public String doExecuteMethod() throws Exception{
        String path = "D:\\work\\springboot-mry-plug-develop-method\\springboot-mry-iz-demo\\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("springboot-mry-biz-0.0.1-SNAPSHOT.jar")){
                        /*if(!claName.equals("com.mry.plugins.impl.AliyunSendMsg")){
                            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("springboot-mry-miz-0.0.1-SNAPSHOT.jar")){
                        /*if(!claName.equals("com.mry.plugins.impl.TencentSendMsg")){
                            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 = "D:\\work\\springboot-mry-plug-develop-method\\springboot-mry-iz-demo\\lib\\springboot-mry-miz-0.0.1-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.mry.plugin.impl.TencentSendMsg");
        //获取实例
        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();
    }

}

自定义配置文件加载类

通过这个类,将上述配置文件中的实现类封装到类对象中,方便后续使用;

package com.mry.plug.config;

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;

}

添加测试接口

package com.mry.plug.controller;

import com.mry.plug.utils.ServiceLoaderUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Autowired
    ServiceLoaderUtils serviceLoaderUtils;

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

以上全部完成之后,启动工程,测试一下该接口,仍然可以得到预期结果;
在这里插入图片描述

代码结构

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

SpringBoot中的插件化实现

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

Spring Boot中的SPI机制

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

Spring Factories实现原理

spring-core包定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
(1) loadFactories根据接口类获取其实现类的实例,这个方法返回的是对象列表;
(2) 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

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

Spring Factories案例实现

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

定义一个服务接口

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

package com.mry.factories.spi;

public interface SmsPlugin {

    public void sendMessage(String message);

}

定义2个服务实现

实现类1

package com.mry.factories.impl;

import com.mry.factories.spi.SmsPlugin;

public class BizSmsImpl implements SmsPlugin {

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

}

实现类2

package com.mry.factories.impl;

import com.mry.factories.spi.SmsPlugin;

public class SystemSmsImpl implements SmsPlugin {

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

}

添加spring.factories文件

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

com.mry.factories.spi.SmsPlugin=\
com.mry.factories.impl.BizSmsImpl,\
com.mry.factories.impl.SystemSmsImpl

添加自定义接口

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

package com.mry.factories.controller;

import com.mry.factories.spi.SmsPlugin;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequestMapping("/api")
public class DemoController {

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

}

启动工程之后,调用一下该接口进行测试,http://localhost:8811/api/sendMsgV3?msg=mry,通过控制台,可以看到,这种方式能够正确获取到系统中可用的服务实现;
在这里插入图片描述
利用spring的这种机制,可以很好的对系统中的某些业务逻辑通过插件化接口的方式进行扩展实现;

插件化机制案例实战

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

案例背景

  • 3个微服务模块,在A模块中有个插件化的接口;
  • 在A模块中的某个接口,需要调用插件化的服务实现进行短信发送;
  • 可以通过配置文件配置参数指定具体的哪一种方式发送短信;
  • 如果没有加载到任何插件,将走A模块在默认的发短信实现;

模块结构

1、springboot-mry-env-biz-pp,插件化接口工程;
2、springboot-mry-env-biz-pt,aliyun短信发送实现;
3、springboot-mry-env-miz-pt,tencent短信发送实现;
4、springboot-mry-env-biz-pp-demo,验证工程;

整体实现思路

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

  • springboot-mry-env-biz-pp定义服务接口,并提供出去jar被其他实现工程依赖;
  • springboot-mry-env-biz-pt与springboot-mry-env-miz-pt依赖springboot-mry-env-biz-pp的jar并实现SPI中的方法;
  • springboot-mry-env-biz-pt与springboot-mry-env-miz-pt按照API规范实现完成后,打成jar包,或者安装到仓库中;
  • springboot-mry-env-biz-pp-demo在pom中依赖springboot-mry-env-biz-pt与springboot-mry-env-miz-pt的jar,或者通过启动加载的方式即可得到具体某个实现;

springboot-mry-env-biz-pp 代码实现过程

添加服务接口

package com.mry.plugin.spi;

import java.util.Map;

public interface MessagePlugin {

    public String sendMsg(Map msgMap);

}

打成jar包并安装到仓库

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

项目结果

在这里插入图片描述

springboot-mry-env-biz-pt 代码实现过程

接下来就是插件化机制中具体的SPI实现过程,两个模块的实现步骤完全一致,下面对 springboot-mry-env-biz-pt 进行说明:

引入springboot-mry-env-biz-pp依赖

<dependencies>
	<dependency>
		<groupId>com.mry</groupId>
		<artifactId>springboot-mry-env-biz-pp</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</dependency>
</dependencies>

添加MessagePlugin接口的实现

package com.mry.spi;
import com.mry.plugin.spi.MessagePlugin;
import java.util.Map;

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

}

添加SPI配置文件

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

com.mry.spi.BitptImpl

将jar安装到仓库中

完成实现类的编码后,通过maven命令将jar安装到仓库中,然后再引入springboot-mry-env-biz-pp-demo中即可;

项目结构

在这里插入图片描述

springboot-mry-env-miz-pt 代码实现过程

接下来就是插件化机制中具体的SPI实现过程,两个模块的实现步骤完全一致,下面对 springboot-mry-env-miz-pt 进行说明:

引入springboot-mry-env-biz-pp依赖

<dependencies>
	<dependency>
		<groupId>com.mry</groupId>
		<artifactId>springboot-mry-env-biz-pp</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</dependency>
</dependencies>

添加MessagePlugin接口的实现

package com.mry.spi;
import com.mry.plugin.spi.MessagePlugin;
import java.util.Map;

public class MizptImpl 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("tencent send message success");
        return "tencent send message success";
    }

}

添加SPI配置文件

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

com.mry.spi.MizptImpl

将jar安装到仓库中

完成实现类的编码后,通过maven命令将jar安装到仓库中,然后再引入springboot-mry-env-biz-pp-demo中即可;

项目结构

在这里插入图片描述

springboot-mry-env-biz-pp-demo 代码实现过程

添加服务依赖

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

	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
	</dependency>

	<dependency>
		<groupId>com.mry</groupId>
		<artifactId>springboot-mry-env-biz-pt</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</dependency>

	<dependency>
		<groupId>com.mry</groupId>
		<artifactId>springboot-mry-env-miz-pt</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</dependency>

</dependencies>

自定义服务加载工具类

package com.mry.utils;

import com.mry.plugin.spi.MessagePlugin;
import com.mry.spi.BitptImpl;
import com.mry.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();
    }

}

接口实现

package com.mry.service;

import com.mry.plugin.spi.MessagePlugin;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class DefaultSmsService implements MessagePlugin {
    @Override
    public String sendMsg(Map msgMap) {
        return "DefaultSmsService--------";
    }
}

package com.mry.service;
import com.mry.plugin.spi.MessagePlugin;
import com.mry.utils.PluginFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

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

}

测试controller

package com.mry.controller;

import com.mry.service.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class SmsController {

    @Autowired
    private SmsService smsService;

    //http://localhost:8822/api/sendMsg?msg=sendMsg
    @GetMapping("/sendMsg")
    public String sendMessage(String msg){
        return smsService.sendMsg(msg);
    }

}

配置文件application.yml

通过修改配置application.yml中msg.type的值切换不同实现

server.port=8822
# msg.type=tencent
msg.type=aliyun

效果演示

启动springboot-mry-env-biz-pp-demo服务,调用接口:http://localhost:8822/api/sendMsg?msg=sendMsg,可以看到如下效果
在这里插入图片描述

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/902058.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【python地理信息绘制入门】cartopy学习

cartopy学习 cartopy简介cartopy绘制中国行政地图&#xff08;cartopy的版本为0.20.0&#xff09;cartopy绘制中国行政地图&#xff0c;单一省细分画出市区cartopy库的一些问题 cartopy简介 cartopy是一个用于绘制地图投影和地理数据可视化的 Python 库。它是建立在 matplotli…

约瑟夫占位问题

面试题中的考查&#xff1a; 其实这个就是一个约瑟夫问题&#xff0c;下面通过讲解你会很好的解决这道题目。 故事背景&#xff1a; 据说在罗马帝国时期&#xff0c;一群犹太士兵被罗马人包围&#xff0c;为了不当罗马人的俘虏&#xff0c;犹太士兵们决定集体自杀&#xff0c;…

java实现人物关系抽取

java实现人物关系抽取 人物关系抽取是实体关系抽取的一种情况。实际上是两个过程&#xff1a;命名实体识别和关系抽取。 Java人物关系抽取是指从文本中提取出与Java相关的人物之间的关系。这个过程可以通过自然语言处理和文本分析的方法来实现。具体的步骤包括&#xff1a; 文本…

PCTA 认证考试高分通过经验分享

作者&#xff1a; msx-yzu 原文来源&#xff1a; https://tidb.net/blog/0b343c9f 序言 我在2023年8月10日&#xff0c;参加了 PingCAP 认证 TiDB 数据库专员 V6 考试 &#xff0c;并以 90分 的成绩通过考试。 考试总分是100分&#xff0c;超过60分就算通过考试。试卷…

深入理解Linux内核--访问文件

访问文件模式 访问文件的模式有多种。我们在本章考虑如下几种情况&#xff1a;规范模式规范模式下文件打开后&#xff0c;标志O_SYNC与0_DIRECT清0,而且它的内容是由系统调用read()和write()来存取。系统调用read()将阻塞调用进程&#xff0c;直到数据被拷贝进用户态地址空间(…

C#与西门子PLC1500的ModbusTcp服务器通信2--ModbusTcp协议

Modbus TCP是近年来越来越流行的工业控制系统通信协议之一&#xff0c;与其他通信协议相比&#xff0c;Modbus TCP通信速度快、可靠性高、兼容性强、适用于模拟或数字量信号的传输&#xff0c;阅读本文前你必须比较熟悉Modbus协议&#xff0c;了解tcp网络。 一、什么是Modbus …

win10 wsl ubuntu 更换版本为18.04 apt换国内源Python换国内源;默认root; Ubuntu18.04 下载与安装(阿里云官方镜像站)

控制面板里面应用模块找到Ubuntu&#xff0c;可以卸载或者移动到其他盘。 Microsoft 应用程序 - ubuntu https://apps.microsoft.com/store/search/ubuntu?hlzh-cn&glcn&rtc1 选择想要的版本安装。 cp /etc/apt/sources.list /etc/apt/sources.list.bak nano /etc/ap…

Java程序设计——拼图游戏

建立四个类 LeftjPanel、LoginPintu、MainJFranme、RightJPanel 在准备一个图片 LeftjPane类 import java.awt.Image; import java.net.MalformedURLException; import java.net.URL;import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JPanel;…

中国电信秋招攻略,考试内容分析

电信秋招简介 每年的毕业生人数都在逐年递增&#xff0c;逐年递增就意味着竞争会越来越大&#xff0c;最好比别人做更充足的准备。要确定好就业方向以及就业的岗位&#xff0c;要了解各种各样的流程&#xff0c;做好一切自己能做到的准备。而对于有想法进入电信公司工作的人来…

第 7 章 排序算法(1)(介绍,分类,时间复杂度,空间复杂度)

7.1排序算法的介绍 排序也称排序算法(Sort Algorithm)&#xff0c;排序是将一组数据&#xff0c;依指定的顺序进行排列的过程。 7.2排序的分类&#xff1a; 内部排序: 指将需要处理的所有数据都加载到**内部存储器(内存)**中进行排序。外部排序法&#xff1a; 数据量过大&am…

【Unity每日一记】SceneManager场景资源动态加载

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

Java程序设计——编写国际象棋棋盘(5*6的黑白方格)

重点代码&#xff1a; 调整黑白方格 if(i%20) { if(k%20) j[i].setBackground(color[0]); else j[i].setBackground(color[1]); add(j[i]); } else …

spring boot分装通用的查询+分页接口

背景 在用spring bootmybatis plus实现增删改查的时候&#xff0c;总是免不了各种模糊查询和分页的查询。每个数据表设计一个模糊分页&#xff0c;这样代码就造成了冗余&#xff0c;且对自身的技能提升没有帮助。那么有没有办法实现一个通用的增删改查的方法呢&#xff1f;今天…

分享8个新鲜的 VSCode 插件,提高你的开发生产效率

Visual Studio Code通常被称为VSCode&#xff0c;是一款开源、轻量但功能强大的源代码编辑器。被全球开发者广泛使用&#xff0c;它提供了丰富的扩展生态系统&#xff0c;适用于各种类型的开发者&#xff0c;增强了用户在多种语言中编码、高效调试甚至在编码过程中引入一些乐趣…

【C语言】字符分类函数、字符转换函数、内存函数

前言 之前我们用两篇文章介绍了strlen、strcpy、stract、strcmp、strncpy、strncat、strncmp、strstr、strtok、streeror这些函数 第一篇文章strlen、strcpy、stract 第二篇文章strcmp、strncpy、strncat、strncmp 第三篇文章strstr、strtok、streeror 今天我们就来学习字…

【GeoDa实用技巧100例】019:制作统计地图(气泡地图)

严重声明:本文为CSDN博主刘一哥GIS原创,原文地址为:https://blog.csdn.net/lucky51222/article/details/132379144,拒绝转载。 文章目录 一、统计地图介绍二、统计地图制作1. 加载实验数据2. 制作统计地图三、重新定义统计地图一、统计地图介绍 统计地图是显示地图中极端值…

C++ Qt 待机弹球游戏

以前的电视机待机时&#xff0c;都有一个球在界面弹来弹去&#xff0c;碰到边界则改变颜色和方向。 设计算法实现该效果&#xff0c;qt实现界面&#xff0c;C实现运动轨迹&#xff0c;及颜色变化。 详细注释 效果如图 运动轨迹控制类头文件 #ifndef CMOTIONCONTROL_H #defi…

系统架构设计师之网络安全-各个层次的网络安全保障

系统架构设计师之网络安全-各个层次的网络安全保障

java面试基础 -- ArrayList 和 LinkedList有什么区别, ArrayList和Vector呢?

目录 基本介绍 有什么不同?? ArrayList的扩容机制 ArrayLIst的基本使用 ArrayList和Vector 基本介绍 还记得我们的java集合框架吗, 我们来复习一下, 如图: 可以看出来 ArrayList和LinkedList 都是具体类, 他们都是接口List的实现类. 但是他们底层的逻辑是不同的, 相信…

什么是条件get方法?

条件GET方法通常指的是HTTP协议中的"GET"请求&#xff0c;但它带有一些条件&#xff0c;这些条件用于控制服务器是否应该返回请求的资源。这些条件通常使用HTTP标头字段来指定&#xff0c;以便客户端可以告诉服务器在某些条件下是否需要新的或更新的资源。 条件GET方…