SpringBoot 插件化开发模式

打印 上一主题 下一主题

主题 952|帖子 952|积分 2856

1、Java常用插件实现方案

1.2、serviceloader方式

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

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

1.2.2、java spi 简单案例

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

定义接口
  1. public interface MessagePlugin {
  2.     public String sendMsg(Map msgMap);
  3. }
复制代码
定义两个不同的实现
  1. public class AliyunMsg implements MessagePlugin {
  2.     @Override
  3.     public String sendMsg(Map msgMap) {
  4.         System.out.println("aliyun sendMsg");
  5.         return "aliyun sendMsg";
  6.     }
  7. }
复制代码
  1. public class TencentMsg implements MessagePlugin {
  2.     @Override
  3.     public String sendMsg(Map msgMap) {
  4.         System.out.println("tencent sendMsg");
  5.         return "tencent sendMsg";
  6.     }
  7. }
复制代码
在resources目录按照规范要求创建文件目录(META-INF/services),文件名为接口的全限定名,并填写实现类的全限定类名。

自定义服务加载类
  1. public static void main(String[] args) {
  2.         ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
  3.         Iterator<MessagePlugin> iterator = serviceLoader.iterator();
  4.         Map map = new HashMap();
  5.         while (iterator.hasNext()){
  6.             MessagePlugin messagePlugin = iterator.next();
  7.             messagePlugin.sendMsg(map);
  8.         }
  9.     }
复制代码
运行上面的程序后,可以看到下面的效果,这就是说,使用ServiceLoader的方式可以加载到不同接口的实现,业务中只需要根据自身的需求,结合配置参数的方式就可以灵活的控制具体使用哪一个实现。

1.2、自定义配置约定方式

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

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

在配置文件中,将具体的实现类配置进去
  1. server:
  2.   port: 8888
  3. impl:
  4.   name: com.wq.plugins.spi.MessagePlugin
  5.   clazz:
  6.     - com.wq.plugins.impl.AliyunMsg
  7.     - com.wq.plugins.impl.TencentMsg
复制代码
1.2.2、自定义配置文件加载类

通过这个类,将上述配置文件中的实现类封装到类对象中,方便后续使用;
  1. package com.wq.propertie;
  2. import lombok.ToString;
  3. import org.springframework.boot.context.properties.ConfigurationProperties;
  4. import java.util.Arrays;
  5. /**
  6. * @Description TODO
  7. * @Version 1.0.0
  8. * @Date 2023/7/1
  9. * @Author wandaren
  10. */
  11. // 启动类需要添加@EnableConfigurationProperties({ClassImpl.class})
  12. @ConfigurationProperties("impl")
  13. public class ClassImpl {
  14.    private String name;
  15.    private String[] clazz;
  16.    public String getName() {
  17.       return name;
  18.    }
  19.    public void setName(String name) {
  20.       this.name = name;
  21.    }
  22.    public String[] getClazz() {
  23.       return clazz;
  24.    }
  25.    public void setClazz(String[] clazz) {
  26.       this.clazz = clazz;
  27.    }
  28.    public ClassImpl(String name, String[] clazz) {
  29.       this.name = name;
  30.       this.clazz = clazz;
  31.    }
  32.    public ClassImpl() {
  33.    }
  34.    @Override
  35.    public String toString() {
  36.       return "ClassImpl{" +
  37.               "name='" + name + '\'' +
  38.               ", clazz=" + Arrays.toString(clazz) +
  39.               '}';
  40.    }
  41. }
复制代码
1.2.3、自定义测试接口

使用上述的封装对象通过类加载的方式动态的在程序中引入
  1. package com.wq.contorller;
  2. import com.wq.plugins.spi.MessagePlugin;
  3. import com.wq.propertie.ClassImpl;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.web.bind.annotation.GetMapping;
  6. import org.springframework.web.bind.annotation.RequestMapping;
  7. import org.springframework.web.bind.annotation.RestController;
  8. import java.util.HashMap;
  9. /**
  10. * @Description TODO
  11. * @Version 1.0.0
  12. * @Date 2023/7/1
  13. * @Author wandaren
  14. */
  15. @RestController
  16. public class HelloController {
  17.     @Autowired
  18.     private ClassImpl classImpl;
  19.     @GetMapping("/sendMsg")
  20.     public String sendMsg() throws Exception{
  21.         for (int i=0;i<classImpl.getClazz().length;i++) {
  22.             Class pluginClass= Class.forName(classImpl.getClazz()[i]);
  23.             MessagePlugin messagePlugin = (MessagePlugin) pluginClass.newInstance();
  24.             messagePlugin.sendMsg(new HashMap());
  25.         }
  26.         return "success";
  27.     }
  28. }
复制代码
1.3.3、添加测试接口
  1. package com.wq;
  2. import com.wq.propertie.ClassImpl;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.boot.context.properties.EnableConfigurationProperties;
  6. @EnableConfigurationProperties({ClassImpl.class})
  7. @SpringBootApplication
  8. public class DemoApplication {
  9.     public static void main(String[] args) {
  10.         SpringApplication.run(DemoApplication.class, args);
  11.     }
  12. }
复制代码
以上全部完成之后,启动工程,测试一下该接口,仍然可以得到预期结果;

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

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

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

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

  • loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表;
  • loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表;
上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下:
  1. public interface MessagePlugin {
  2.     public String sendMsg(Map msgMap);
  3. }
复制代码
从代码中我们可以知道,在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件,就是说我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖。
spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:
  1.         <dependency>
  2.             <groupId>com.wq</groupId>
  3.             <artifactId>spi-00</artifactId>
  4.             <version>1</version>
  5.         </dependency>
复制代码
如果一个接口希望配置多个实现类,可以使用’,’进行分割
2.3、Spring Factories案例实现

接下来看一个具体的案例实现来体验下Spring Factories的使用;
2.3.1、定义一个服务接口

自定义一个接口,里面添加一个方法;
  1. public class AliyunMsg implements MessagePlugin {
  2.     @Override
  3.     public String sendMsg(Map msgMap) {
  4.         System.out.println("aliyun sendMsg");
  5.         return "aliyun sendMsg";
  6.     }
  7. }
复制代码
2.3.2、 定义2个服务实现

实现类1
  1. public class TencentMsg implements MessagePlugin {
  2.     @Override
  3.     public String sendMsg(Map msgMap) {
  4.         System.out.println("tencent sendMsg");
  5.         return "tencent sendMsg";
  6.     }
  7. }
复制代码
实现类2
  1. package com.wq.utils;
  2. import com.wq.propertie.ClassImpl;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.stereotype.Component;
  5. import java.io.File;
  6. import java.lang.reflect.Method;
  7. import java.net.URL;
  8. import java.net.URLClassLoader;
  9. import java.util.HashMap;
  10. import java.util.Map;
  11. import java.util.Objects;
  12. @Component
  13. public class ServiceLoaderUtils {
  14.     @Autowired
  15.     ClassImpl classImpl;
  16.     public static void loadJarsFromAppFolder() throws Exception {
  17.         String path = "/Users/wandaren/develop/study/spi-00/lib";
  18.         File f = new File(path);
  19.         if (f.isDirectory()) {
  20.             for (File subf : f.listFiles()) {
  21.                 if (subf.isFile()) {
  22.                     loadJarFile(subf);
  23.                 }
  24.             }
  25.         } else {
  26.             loadJarFile(f);
  27.         }
  28.     }
  29.     public static void loadJarFile(File path) throws Exception {
  30.         URL url = path.toURI().toURL();
  31.         // 可以获取到AppClassLoader,可以提到前面,不用每次都获取一次
  32.         URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
  33.         // 加载
  34.         //Method method = URLClassLoader.class.getDeclaredMethod("sendMsg", Map.class);
  35.         Method method = URLClassLoader.class.getMethod("sendMsg", Map.class);
  36.         method.setAccessible(true);
  37.         method.invoke(classLoader, url);
  38.     }
  39.     public  void main(String[] args) throws Exception{
  40.         System.out.println(invokeMethod("hello"));;
  41.     }
  42.     public String doExecuteMethod() throws Exception{
  43.         String path = "/Users/wandaren/develop/study/spi-00/lib";
  44.         File f1 = new File(path);
  45.         Object result = null;
  46.         if (f1.isDirectory()) {
  47.             for (File subf : f1.listFiles()) {
  48.                 //获取文件名称
  49.                 String name = subf.getName();
  50.                 String fullPath = path + "/" + name;
  51.                 //执行反射相关的方法
  52.                 File f = new File(fullPath);
  53.                 URL urlB = f.toURI().toURL();
  54.                 URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread()
  55.                         .getContextClassLoader());
  56.                 String[] clazz = classImpl.getClazz();
  57.                 for(String claName : clazz){
  58.                     if(name.equals("spi-01-1.jar")){
  59.                         if(!claName.equals("com.wq.plugins.impl.AliyunMsg")){
  60.                             continue;
  61.                         }
  62.                         Class<?> loadClass = classLoaderA.loadClass(claName);
  63.                         if(Objects.isNull(loadClass)){
  64.                             continue;
  65.                         }
  66.                         //获取实例
  67.                         Object obj = loadClass.newInstance();
  68.                         Map map = new HashMap();
  69.                         //获取方法
  70.                         Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);
  71.                         result = method.invoke(obj,map);
  72.                         if(Objects.nonNull(result)){
  73.                             break;
  74.                         }
  75.                     }else if(name.equals("spi-02-1.jar")){
  76.                         if(!claName.equals("com.wq.plugins.impl.TencentMsg")){
  77.                             continue;
  78.                         }
  79.                         Class<?> loadClass = classLoaderA.loadClass(claName);
  80.                         if(Objects.isNull(loadClass)){
  81.                             continue;
  82.                         }
  83.                         //获取实例
  84.                         Object obj = loadClass.newInstance();
  85.                         Map map = new HashMap();
  86.                         //获取方法
  87.                         Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);
  88.                         result = method.invoke(obj,map);
  89.                         if(Objects.nonNull(result)){
  90.                             break;
  91.                         }
  92.                     }
  93.                 }
  94.                 if(Objects.nonNull(result)){
  95.                     break;
  96.                 }
  97.             }
  98.         }
  99.         return result.toString();
  100.     }
  101.     public Object loadMethod(String fullPath) throws Exception{
  102.         File f = new File(fullPath);
  103.         URL urlB = f.toURI().toURL();
  104.         URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread()
  105.                 .getContextClassLoader());
  106.         Object result = null;
  107.         String[] clazz = classImpl.getClazz();
  108.         for(String claName : clazz){
  109.             Class<?> loadClass = classLoaderA.loadClass(claName);
  110.             if(Objects.isNull(loadClass)){
  111.                 continue;
  112.             }
  113.             //获取实例
  114.             Object obj = loadClass.newInstance();
  115.             Map map = new HashMap();
  116.             //获取方法
  117.             Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);
  118.             result = method.invoke(obj,map);
  119.             if(Objects.nonNull(result)){
  120.                 break;
  121.             }
  122.         }
  123.         return result;
  124.     }
  125.     public static String invokeMethod(String text) throws Exception{
  126.         String path = "/Users/wandaren/develop/study/spi-00/lib/spi-01-1.jar";
  127.         File f = new File(path);
  128.         URL urlB = f.toURI().toURL();
  129.         URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread()
  130.                 .getContextClassLoader());
  131.         Class<?> product = classLoaderA.loadClass("com.wq.plugins.impl.AliyunMsg");
  132.         //获取实例
  133.         Object obj = product.newInstance();
  134.         Map map = new HashMap();
  135.         //获取方法
  136.         Method method=product.getDeclaredMethod("sendMsg",Map.class);
  137.         //执行方法
  138.         Object result1 = method.invoke(obj,map);
  139.         // TODO According to the requirements , write the implementation code.
  140.         return result1.toString();
  141.     }
  142.     public static String getApplicationFolder() {
  143.         String path = ServiceLoaderUtils.class.getProtectionDomain().getCodeSource().getLocation().getPath();
  144.         return new File(path).getParent();
  145.     }
  146. }
复制代码
2.3.3、 添加spring.factories文件

在resources目录下,创建一个名叫:META-INF的目录,然后在该目录下定义一个spring.factories的配置文件,内容如下,其实就是配置了服务接口,以及两个实现类的全类名的路径;
  1.     @Autowired
  2.     private ServiceLoaderUtils serviceLoaderUtils;
  3.     @GetMapping("/sendMsgV2")
  4.     public String index() throws Exception {
  5.         String result = serviceLoaderUtils.doExecuteMethod();
  6.         return result;
  7.     }
复制代码
2.3.4、 添加自定义接口

添加一个自定义的接口,有没有发现,这里和java 的spi有点类似,只不过是这里换成了SpringFactoriesLoader去加载服务;
  1.         public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  2.                 ClassLoader classLoaderToUse = classLoader;
  3.                 if (classLoaderToUse == null) {
  4.                         classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
  5.                 }
  6.                 String factoryTypeName = factoryType.getName();
  7.                 return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
  8.         }
  9.         private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
  10.                 Map<String, List<String>> result = cache.get(classLoader);
  11.                 if (result != null) {
  12.                         return result;
  13.                 }
  14.                 result = new HashMap<>();
  15.                 try {
  16.                         Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
  17.                         while (urls.hasMoreElements()) {
  18.                                 URL url = urls.nextElement();
  19.                                 UrlResource resource = new UrlResource(url);
  20.                                 Properties properties = PropertiesLoaderUtils.loadProperties(resource);
  21.                                 for (Map.Entry<?, ?> entry : properties.entrySet()) {
  22.                                         String factoryTypeName = ((String) entry.getKey()).trim();
  23.                                         String[] factoryImplementationNames =
  24.                                                         StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
  25.                                         for (String factoryImplementationName : factoryImplementationNames) {
  26.                                                 result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
  27.                                                                 .add(factoryImplementationName.trim());
  28.                                         }
  29.                                 }
  30.                         }
  31.                         // Replace all lists with unmodifiable lists containing unique elements
  32.                         result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
  33.                                         .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
  34.                         cache.put(classLoader, result);
  35.                 }
  36.                 catch (IOException ex) {
  37.                         throw new IllegalArgumentException("Unable to load factories from location [" +
  38.                                         FACTORIES_RESOURCE_LOCATION + "]", ex);
  39.                 }
  40.                 return result;
  41.         }
复制代码

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

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

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


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

1、spi-00,插件化接口工程;
2、spi-01,aliyun短信发送实现;
3、spi-02,tncent短信发送实现;
3.1.2、 整体实现思路

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

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

3.2.1、 添加服务接口
  1. public interface MessagePlugin {
  2.     public String sendMsg(Map msgMap);
  3. }
复制代码
3.2.2、 打成jar包并安装到仓库

idea执行install

3.3、spi-01与spi-02实现

maven引入spi-00依赖坐标
  1. public interface SmsPlugin {
  2.     public void sendMessage(String message);
  3. }
复制代码
3.3.1、spi-01
  1. public class AliyunMsg implements MessagePlugin {
  2.     @Override
  3.     public String sendMsg(Map msgMap) {
  4.         System.out.println("aliyun sendMsg");
  5.         return "aliyun sendMsg";
  6.     }
  7. }
复制代码
3.3.2、spi-02
  1. public class TencentMsg implements MessagePlugin {
  2.     @Override
  3.     public String sendMsg(Map msgMap) {
  4.         System.out.println("tencent sendMsg");
  5.         return "tencent sendMsg";
  6.     }
  7. }
复制代码

3.3.3、将spi-01与spi-02打成jar

idea执行install
3.4、spi-00添加服务依赖与实现

3.4.1、添加服务依赖
  1. com.wq.plugin.SmsPlugin=\
  2. com.wq.plugin.impl.BizSmsImpl,\
  3. com.wq.plugin.impl.SystemSmsImpl
复制代码
3.4.2、自定义服务加载工具类
  1. package com.wq.controller;
  2. import com.wq.plugin.SmsPlugin;
  3. import org.springframework.core.io.support.SpringFactoriesLoader;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import java.util.List;
  7. /**
  8. * @Description TODO
  9. * @Version 1.0.0
  10. * @Date 2023/7/2
  11. * @Author wandaren
  12. */
  13. @RestController
  14. public class SmsController {
  15.     @GetMapping("/sendMsgV3")
  16.     public String sendMsgV3(String msg) throws Exception{
  17.         List<SmsPlugin> smsServices= SpringFactoriesLoader.loadFactories(SmsPlugin.class, null);
  18.         for(SmsPlugin smsService : smsServices){
  19.             smsService.sendMessage(msg);
  20.         }
  21.         return "success";
  22.     }
  23. }
复制代码
3.4.3、接口实现
  1. public interface MessagePlugin {
  2.     public String sendMsg(Map msgMap);
  3. }
复制代码
  1. <dependencies>
  2.         <dependency>
  3.             <groupId>com.wq</groupId>
  4.             <artifactId>spi-00</artifactId>
  5.             <version>1</version>
  6.         </dependency>
  7.     </dependencies>
复制代码
3.4.4、测试controller
  1. public class AliyunMsg implements MessagePlugin {
  2.     @Override
  3.     public String sendMsg(Map msgMap) {
  4.         System.out.println("aliyun sendMsg");
  5.         return "aliyun sendMsg";
  6.     }
  7. }
复制代码
3.4.5、测试

通过修改配置application.yml中msg.type的值切换不同实现
  1. public class TencentMsg implements MessagePlugin {
  2.     @Override
  3.     public String sendMsg(Map msgMap) {
  4.         System.out.println("tencent sendMsg");
  5.         return "tencent sendMsg";
  6.     }
  7. }
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

尚未崩坏

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表