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 模块结构
- biz-app:主应用,定义插件接口与业务逻辑。
- biz-pt:插件实现一,模拟阿里云短信发送。
- 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 等核心实现机制,对于构建高扩展、易维护的系统具有重要意义。本文通过理论结合实战,希望能为你在实现系统插件化扩展时提供清晰的思路和可行的方案。