找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

575

积分

0

好友

76

主题
发表于 昨天 07:08 | 查看: 7| 回复: 0

1.1 使用插件的好处

1.1.1 模块解耦

实现服务模块之间解耦的方式有很多,而插件化方案通常能达到更高的解耦程度,并且具备更好的灵活性与可定制性。

例如,在设计一个系统时,可以使用策略模式来选择使用哪家服务商发送短信。然而,若某家服务商出现故障导致短信无法发送,单纯的设计模式难以应对此动态变化。此时,若采用插件化设计并结合外部配置,则可以在运行时动态加载和切换不同的短信发送插件,实现服务的无缝切换。

1.1.2 提升扩展性和开放性

Spring 框架为例,其之所以能构建起繁荣的生态,与其内置的各种可扩展的插件机制密不可分。正因为 Spring 提供了丰富的扩展点,我们才能方便地集成各类中间件。

插件化机制极大地提升了系统的可扩展性,从而能够围绕核心系统构建起丰富的应用生态。

1.1.3 方便第三方接入

当系统具备插件化能力后,第三方应用若需对接,只需基于系统预留的插件接口实现符合自身业务逻辑的插件即可。这种方式对主系统侵入性极小,甚至可以实现基于配置的热加载,真正做到开箱即用,灵活便捷。

1.2 插件化常用实现思路

基于 Java 平台,结合实际经验,常见的插件化实现思路包括:

  • SPI(Service Provider Interface)机制;
  • 约定配置文件与目录结构,利用反射机制实现;
  • Spring Boot 中的 spring.factories 自动装配机制;
  • Java Agent(探针)技术;
  • Spring 框架内置的扩展点(如 BeanPostProcessor);
  • 第三方插件框架,例如 spring-plugin-core
  • Spring AOP 技术;

二、Java常用插件实现方案

2.1 ServiceLoader方式

ServiceLoader 是 Java 对 SPI 模式的标准实现。开发者只需按照接口规范开发实现类并进行配置,Java 即可通过 ServiceLoader 加载并调用所有实现。

2.1.1 Java SPI

SPI 全称 Service Provider Interface,是 JDK 内置的一种服务发现与动态替换扩展机制。当需要为某个接口在运行时动态添加实现时,只需按照规范提供实现类即可。例如,JDBC 中的 Driver 接口,不同的数据库厂商(MySQL、Oracle)提供不同的实现,Java 的 SPI 机制正是为此类场景服务。

其原理简图如下: 图片

2.1.2 Java SPI 简单案例

假设工程目录结构如下,我们在一个模块中定义插件接口,其他模块通过依赖引入并实现该接口。为方便演示,此处将不同实现放在同一工程内。 图片

定义接口

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 目录下,按照 SPI 规范创建 META-INF/services/ 目录,并在其中创建以接口全限定名命名的文件(如 com.example.MessagePlugin),文件内容为实现类的全限定名,每个类名占一行。 图片

自定义服务加载类

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 成功加载了所有配置的实现。在实际业务中,可结合配置参数灵活控制具体使用哪一个实现。 图片

2.2 自定义配置约定方式

ServiceLoader 要求必须在 META-INF/services 下放置配置文件,当项目插件较多时,配置文件会显得繁杂。因此,在实践中可考虑以下思路:

  • 应用A定义核心接口;
  • 应用B、C、D实现该接口并打包成 SDK JAR;
  • 应用A引用或将SDK JAR放入特定目录;
  • 应用A读取配置文件,并通过反射动态加载指定实现类;

基于上文案例进行调整:

2.2.1 添加配置文件 (application.yml)

在配置文件中声明具体的实现类。

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 的动态加载效果。 图片

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

更进一步,有时我们不希望将插件实现直接打包进主应用依赖,而是希望动态加载指定目录下的 JAR 文件。这在生产环境中是一种常见实践,主要步骤为:

  • 应用A定义服务接口;
  • 应用B、C、D实现该接口并打包成独立JAR;
  • 将JAR文件放入应用A约定的目录(如 lib/);
  • 应用A启动时加载该目录下的JAR,通过反射调用目标方法;
2.3.1 创建约定目录

在工程目录下创建 lib 文件夹,并放入实现类的JAR包。 图片

2.3.2 新增读取JAR的工具类

该工具类负责扫描目录、加载JAR,并根据配置反射执行方法。

@Component
public class ServiceLoaderUtils {
    @Autowired
    ClassImpl classImpl;
    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;
                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){
                    // 示例逻辑:根据JAR文件名决定加载哪个配置的实现类
                    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();
    }
}
2.3.3 添加测试接口
@GetMapping("/sendMsgV2")
public String index() throws Exception {
    String result = serviceLoaderUtils.doExecuteMethod();
    return result;
}

完成以上步骤后,启动工程并测试接口,可得到预期结果。此示例较为基础,实际应用中需优化类加载管理、错误处理及根据更复杂的规则(如接口参数)来选择插件。 图片

三、SpringBoot中的插件化实现

在 Spring Boot 中,框架自身提供了诸多扩展点,其中 spring.factories 自动装配机制非常适合用于实现插件化扩展。

3.1 Spring Boot 中的 SPI 机制

Spring 框架拥有一种类似于 Java SPI 的加载机制。它通过在 META-INF/spring.factories 文件中配置接口与实现类的映射关系,由框架在启动时读取并实例化。这种机制是 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);
    }
}

该方法会扫描所有 Jar 包中的 spring.factories 文件,这意味着我们可以在自己的模块中安全地配置,而不会与其他模块的配置冲突。文件内容以 Properties 格式解析,支持为一个接口配置多个实现类,用逗号分隔。

com.xxx.SmsPlugin=com.xxx.impl.AliyunSms,com.xxx.impl.TencentSms

3.3 Spring Factories 案例实现

3.3.1 定义一个服务接口
public interface SmsPlugin {
    public void sendMessage(String message);
}
3.3.2 定义两个服务实现
public class BizSmsImpl implements SmsPlugin {
    @Override
    public void sendMessage(String message) {
        System.out.println("this is BizSmsImpl sendMessage..." + message);
    }
}
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 添加测试接口
@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,控制台将输出两个实现类的信息,证明成功加载。 图片

四、插件化机制案例实战

下面将基于 Java SPI 机制,模拟一个接近真实微服务场景的完整案例。

4.1 案例背景

  • 存在三个微服务模块。
  • 模块A(biz-app)定义插件化接口,并在某个业务接口中需调用该插件发送短信。
  • 可通过配置文件指定使用哪种短信发送方式(阿里云或腾讯云)。
  • 若未加载到任何插件,则使用模块A内置的默认短信发送实现。
4.1.1 模块结构
  1. biz-app:主应用,定义插件接口与业务逻辑。
  2. biz-pt:插件实现一,模拟阿里云短信发送。
  3. miz-pt:插件实现二,模拟腾讯云短信发送。
4.1.2 整体实现思路
  • biz-app 定义服务接口 MessagePlugin,并打包发布。
  • biz-pt 和 miz-pt 依赖 biz-app 的接口JAR,并实现其方法。
  • 两个插件模块分别打包成独立JAR。
  • biz-app 通过 SPI 机制加载 classpath 下所有 MessagePlugin 实现(可通过依赖引入或外部JAR加载),再根据配置选择具体插件执行。

4.2 biz-app 关键代码实现

4.2.1 添加服务接口
public interface MessagePlugin {
    public String sendMsg(Map msgMap);
}
4.2.2 自定义插件工厂类

此类根据配置的 type 筛选出对应的插件实例。

import com.congge.plugin.spi.MessagePlugin;
import com.congge.spi.BitptImpl;
import com.congge.spi.MizptImpl;
import java.util.*;
public class PluginFactory {
    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;
    }
}
4.2.3 业务服务层
@Service
public class SmsService {
    @Value("${msg.type}") // 从配置读取插件类型,如 aliyun
    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.4 控制器
@RestController
public class SmsController {
    @Autowired
    private SmsService smsService;
    @GetMapping("/sendMsg")
    public String sendMessage(String msg){
        return smsService.sendMsg(msg);
    }
}
4.2.5 添加插件依赖

在 biz-app 的 pom.xml 中引入两个插件模块的JAR,这样它们的 SPI 配置文件才会被主 ClassLoader 读取到。

<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>

4.3 biz-pt (插件一) 关键实现

4.3.1 依赖与接口实现

pom.xml 依赖 biz-app 的接口JAR,并实现 MessagePlugin

public class BitptImpl implements MessagePlugin {
    @Override
    public String sendMsg(Map msgMap) {
        Object userId = msgMap.get("userId");
        Object type = msgMap.get("_type");
        System.out.println(" ==== userId :" + userId + ",type :" + type);
        System.out.println("aliyun send message success");
        return "aliyun send message success";
    }
}
4.3.2 添加 SPI 配置文件

resources/META-INF/services/ 下创建文件 com.congge.plugin.spi.MessagePlugin,内容为:

com.congge.spi.BitptImpl

4.4 效果演示

启动 biz-app 服务,假设配置 msg.type=aliyun,调用接口 localhost:8087/sendMsg?msg=test。 控制台将输出阿里云插件的执行结果,业务服务成功通过配置动态选择了指定的插件执行。 图片 配置与插件加载关系示意: 图片

五、写在文末

插件化机制作为一种重要的 架构设计 思想,已广泛应用于各种编程语言、框架及中间件中。深入理解并掌握 Java SPI、Spring Factories 等核心实现机制,对于构建高扩展、易维护的系统具有重要意义。本文通过理论结合实战,希望能为你在实现系统插件化扩展时提供清晰的思路和可行的方案。




上一篇:SpringBoot ResponseBodyEmitter异步流式推送实战:实现实时日志输出
下一篇:分布式缓存雪崩详解:从成因到四种核心解决方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-10 21:14 , Processed in 0.079451 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

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