动态代理和SPI都属于Java中的动态扩展机制。
动态代理允许程序在不修改目标类代码的情况下,为方法调用增加日志、事务、权限等功能;SPI允许主程序在不直接依赖具体实现类的情况下,发现并加载扩展实现。
将动态代理、SPI和注册表模式结合起来,可以进一步设计出支持动态扩展的插件系统。
一、什么是代理模式
代理模式是指不直接调用目标对象,而是通过一个代理对象间接访问目标对象。
例如,原本直接调用:
orderService.createOrder();
加入代理后,调用流程可以变为:
调用者
↓
代理对象
↓
权限检查
↓
事务开启
↓
目标方法
↓
事务提交
↓
日志记录
代理对象可以在目标方法执行前后增加额外逻辑,而目标类本身不需要重复编写这些代码。
常见应用包括:
-
日志记录;
-
事务管理;
-
权限校验;
-
性能统计;
-
缓存;
-
重试和限流;
-
远程调用封装。
代理可以分为静态代理和动态代理。
静态代理需要开发者提前编写代理类;动态代理则可以在程序运行期间生成代理对象。

二、JDK动态代理如何使用
JDK提供了基于接口的动态代理机制。
首先定义接口:
public interface OrderService {
void createOrder();
}
再提供实现类:
public class OrderServiceImpl
implements OrderService {
@Override
public void createOrder() {
System.out.println("创建订单");
}
}
通过InvocationHandler定义代理逻辑:
public class LogInvocationHandler
implements InvocationHandler {
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(
Object proxy,
Method method,
Object[] args) throws Throwable {
System.out.println(
"开始执行:" + method.getName()
);
try {
return method.invoke(target, args);
} finally {
System.out.println(
"执行结束:" + method.getName()
);
}
}
}
创建代理对象:
OrderService target =
new OrderServiceImpl();
OrderService proxy =
(OrderService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogInvocationHandler(target)
);
proxy.createOrder();
调用proxy.createOrder()时,实际会进入InvocationHandler.invoke(),再由处理器决定是否调用目标对象。
三、JDK动态代理为什么要求接口
JDK动态代理生成的代理类,本质上会实现目标对象所实现的接口。
假设目标类实现了:
OrderService
运行期间生成的代理类可以近似理解为:
public final class GeneratedProxy
extends Proxy
implements OrderService {
private final InvocationHandler handler;
@Override
public void createOrder() {
handler.invoke(
this,
createOrderMethod,
null
);
}
}
代理类并不是目标实现类的子类,而是接口的另一个实现类。
因此,调用者需要通过接口类型持有代理对象:
OrderService service = proxy;
JDK选择基于接口实现代理,主要是因为接口已经明确规定了可代理的方法集合。代理类只需要实现这些接口,并把方法调用转交给InvocationHandler。
Java类只支持单继承,而JDK生成的代理类本身已经继承了Proxy类,不能再继承目标普通类。因此,传统JDK动态代理无法直接通过继承方式代理一个没有接口的普通类。

四、CGLIB如何代理普通类
CGLIB采用生成目标类子类的方式实现代理。
假设目标类为:
public class OrderService {
public void createOrder() {
System.out.println("创建订单");
}
}
CGLIB会在运行期间生成一个类似的子类:
public class OrderServiceProxy
extends OrderService {
@Override
public void createOrder() {
// 执行增强逻辑
super.createOrder();
// 执行后置逻辑
}
}
通过重写目标方法,代理子类可以在调用原方法前后增加额外逻辑。
因此,CGLIB不要求目标类必须实现接口。
但是,基于继承的代理方式也存在限制:
-
final类不能被继承,因此不能被代理; -
final方法不能被重写,因此不能被增强; -
private方法对子类不可见,不能通过重写进行代理; -
构造和对象创建过程比普通实例化更复杂;
-
类和方法的可见性可能影响代理结果。
现代Spring通常会根据目标对象情况选择代理方式:
-
目标类实现了接口时,可以使用JDK动态代理;
-
目标类没有合适接口时,可以使用基于子类的代理方式;
-
也可以通过配置强制使用类代理。
五、动态代理和反射是什么关系

JDK动态代理需要通过InvocationHandler接收方法调用信息。
处理器中的Method对象就是反射API的一部分:
method.invoke(target, args);
因此,JDK动态代理在调用转发过程中通常会使用反射。
不过,动态代理和反射不是同一个概念。
反射解决的是:
如何在运行时获取类的信息并操作对象。
动态代理解决的是:
如何在运行时创建一个中间代理对象,统一拦截方法调用。
动态代理通常以反射作为实现工具之一,但它的目标是方法增强和调用控制。
六、Spring AOP如何使用动态代理
Spring AOP主要通过代理对象实现。
例如:
@Transactional
public void createOrder() {
// 创建订单
}
业务代码中没有直接编写开启事务、提交事务和回滚事务的代码。
Spring会为目标Bean创建代理对象。当外部代码调用代理对象的方法时,代理逻辑会:
-
读取事务配置;
-
开启数据库事务;
-
调用目标方法;
-
正常完成时提交事务;
-
发生指定异常时回滚事务。
调用者看到的仍然是普通业务接口,但实际获得的可能是代理对象。
这也是为什么一些Spring功能在同一个类内部自调用时可能失效:
public void methodA() {
methodB();
}
@Transactional
public void methodB() {
}
methodA()直接通过当前对象调用methodB(),可能没有经过Spring代理对象,因此事务拦截器没有机会执行。
解决思路通常包括:
-
把事务方法拆分到另一个Bean;
-
调整方法调用边界;
-
通过代理对象调用;
-
使用更适合的事务设计。
七、什么是Java SPI

SPI是Service Provider Interface的缩写,可以理解为服务提供者接口。
它允许框架或主程序先定义接口,再由不同的提供者实现接口。主程序不需要直接写死具体实现类,而是在运行期间发现并加载实现。
SPI的核心思想是:
接口由框架定义
实现由扩展方提供
实现关系通过配置声明
框架在运行时加载实现
例如,主程序定义一个通知接口:
public interface MessageSender {
void send(String message);
}
不同插件可以提供不同实现:
public class EmailMessageSender
implements MessageSender {
@Override
public void send(String message) {
System.out.println(
"发送邮件:" + message
);
}
}
public class SmsMessageSender
implements MessageSender {
@Override
public void send(String message) {
System.out.println(
"发送短信:" + message
);
}
}
八、JDK SPI如何配置
JDK原生SPI通常使用ServiceLoader加载实现。
扩展实现所在的JAR包中,需要创建配置文件:
META-INF/services/接口的全限定类名
假设接口完整名称为:
com.example.MessageSender
配置文件路径为:
META-INF/services/com.example.MessageSender
文件内容填写实现类的全限定名称:
com.example.EmailMessageSender
com.example.SmsMessageSender
主程序通过ServiceLoader加载:
ServiceLoader<MessageSender> loader =
ServiceLoader.load(MessageSender.class);
for (MessageSender sender : loader) {
sender.send("系统启动成功");
}
主程序不需要直接编写:
new EmailMessageSender();
new SmsMessageSender();
只要对应实现类和配置文件位于类路径中,ServiceLoader就可以发现它们。
九、ServiceLoader大致如何工作
ServiceLoader首先定位类路径或模块路径中的服务配置。
当程序遍历加载结果时,它会:
-
查找指定服务接口对应的配置;
-
读取实现类名称;
-
使用类加载器加载实现类;
-
检查实现类是否符合服务接口要求;
-
创建服务实现实例;
-
将实例返回给调用者。
JDK SPI通常具有延迟加载特点。创建ServiceLoader时不一定立即实例化全部实现,遍历时才逐步加载。
使用SPI时需要注意:
-
实现类必须能够被对应类加载器找到;
-
传统用法通常要求实现类具有可访问的无参构造能力;
-
配置文件路径和类名必须正确;
-
某个实现初始化失败可能影响加载过程;
-
多个实现的顺序不宜被当作稳定业务规则;
-
不应在实现类构造方法中执行过重操作。
十、JDBC驱动如何体现SPI思想
JDBC定义了一套统一接口,例如:
Connection
Statement
ResultSet
Driver
不同数据库厂商提供自己的驱动实现。
应用代码主要面向JDBC接口编程:
Connection connection =
DriverManager.getConnection(
url,
username,
password
);
应用不需要了解MySQL、PostgreSQL等数据库驱动内部如何建立网络连接。
驱动JAR包可以通过服务提供者配置声明自己的java.sql.Driver实现。驱动被发现和注册后,DriverManager会根据数据库URL选择能够处理该URL的驱动。
早期代码中经常显式编写:
Class.forName("com.mysql.cj.jdbc.Driver");
现代JDBC驱动通常可以通过SPI机制自动发现,很多场景下不再需要手动加载驱动类。
十一、日志框架和插件系统如何使用SPI
日志系统通常需要解决一个问题:
业务代码依赖统一日志接口,
实际运行时选择具体日志实现。
应用可以面向统一API编程,而具体实现由运行环境决定。
插件系统也可以通过SPI扩展:
-
文件解析器;
-
数据导入器;
-
消息发送器;
-
数据库驱动;
-
序列化组件;
-
加密算法;
-
任务执行器;
-
AI工具或函数调用组件。
主程序只依赖扩展接口,插件JAR包负责提供实现和配置。将插件加入类路径或指定插件目录后,主程序即可发现新的能力。
十二、原生SPI有哪些不足

原生ServiceLoader比较简单,但不一定适合复杂插件系统。
常见不足包括:
1. 通常需要遍历才能寻找指定实现
如果存在多个实现,原生SPI没有直接提供按业务名称快速查询的注册表。
程序通常需要:
for (MessageSender sender : loader) {
if (...) {
return sender;
}
}
2. 缺少完整的生命周期管理
原生SPI主要负责发现和创建实例,但不会自动处理:
-
初始化;
-
停止;
-
健康检查;
-
依赖注入;
-
配置绑定;
-
资源释放。
3. 缺少版本和依赖管理
复杂插件可能需要声明:
-
插件编号;
-
插件版本;
-
主程序最低版本;
-
依赖的其他插件;
-
冲突关系;
-
优先级。
这些能力需要额外设计。
4. 类加载隔离能力有限
所有插件都放在同一个类路径中时,可能出现依赖版本冲突。
真正复杂的插件系统往往需要为不同插件创建独立类加载器。
十三、如何设计Tool接口

假设项目中需要管理多种工具,可以先定义统一接口:
public interface Tool {
String name();
ToolResult execute(ToolRequest request);
}
每个工具提供唯一名称和执行方法:
public class WeatherTool implements Tool {
@Override
public String name() {
return "weather";
}
@Override
public ToolResult execute(
ToolRequest request) {
return ToolResult.success(
"天气查询结果"
);
}
}
再定义另一个工具:
public class CalculatorTool implements Tool {
@Override
public String name() {
return "calculator";
}
@Override
public ToolResult execute(
ToolRequest request) {
return ToolResult.success(
"计算结果"
);
}
}
主程序不应在业务代码中不断编写:
if ("weather".equals(name)) {
// 调用天气工具
} else if ("calculator".equals(name)) {
// 调用计算工具
}
更适合使用注册表统一管理。
十四、ToolRegistry如何设计
一个基础版ToolRegistry可以维护工具名称和工具对象之间的映射:
public class ToolRegistry {
private final Map<String, Tool> tools =
new ConcurrentHashMap<>();
public void register(Tool tool) {
Objects.requireNonNull(
tool,
"tool不能为null"
);
String name = normalize(tool.name());
Tool previous = tools.putIfAbsent(
name,
tool
);
if (previous != null) {
throw new IllegalStateException(
"工具名称重复:" + name
);
}
}
public Tool getRequired(String name) {
Tool tool = tools.get(normalize(name));
if (tool == null) {
throw new IllegalArgumentException(
"工具不存在:" + name
);
}
return tool;
}
public Optional<Tool> find(String name) {
return Optional.ofNullable(
tools.get(normalize(name))
);
}
public Collection<Tool> getAll() {
return List.copyOf(tools.values());
}
public void unregister(String name) {
tools.remove(normalize(name));
}
private String normalize(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException(
"工具名称不能为空"
);
}
return name.trim()
.toLowerCase(Locale.ROOT);
}
}
注册表应重点解决以下问题:
-
工具名称唯一;
-
查找过程高效;
-
多线程访问安全;
-
不向外暴露内部可变集合;
-
注册失败时给出明确错误;
-
支持按名称获取工具;
-
根据需求支持取消注册。
十五、如何结合SPI自动注册工具
可以让每个工具实现Tool接口,并通过JDK SPI声明。
插件JAR包中增加:
META-INF/services/com.example.tool.Tool
配置文件中填写:
com.example.tool.WeatherTool
com.example.tool.CalculatorTool
程序启动时加载并注册:
public class ToolBootstrap {
public static ToolRegistry load(
ClassLoader classLoader) {
ToolRegistry registry =
new ToolRegistry();
ServiceLoader<Tool> loader =
ServiceLoader.load(
Tool.class,
classLoader
);
for (Tool tool : loader) {
registry.register(tool);
}
return registry;
}
}
这样,主程序不需要了解具体工具类。
新增工具时只需要:
-
实现
Tool接口; -
在SPI配置中声明实现类;
-
将插件JAR加入类路径或插件目录;
-
重启或触发插件重新加载。
工具就可以被注册到ToolRegistry中。
十六、如何加入注解描述工具信息
仅依赖name()方法时,工具元数据比较有限。
可以定义注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToolDefinition {
String name();
String description();
String version() default "1.0.0";
int order() default 0;
}
工具类使用注解:
@ToolDefinition(
name = "weather",
description = "查询指定城市天气",
version = "1.0.0"
)
public class WeatherTool implements Tool {
@Override
public String name() {
return "weather";
}
@Override
public ToolResult execute(
ToolRequest request) {
return ToolResult.success(
"天气查询结果"
);
}
}
注册时读取注解:
ToolDefinition definition =
tool.getClass().getAnnotation(
ToolDefinition.class
);
if (definition == null) {
throw new IllegalStateException(
"工具缺少@ToolDefinition:"
+ tool.getClass().getName()
);
}
注册表可以保存更完整的描述对象:
public record ToolDescriptor(
String name,
String description,
String version,
int order,
Tool tool) {
}
这样既能根据名称执行工具,也能向前端或AI模型提供工具描述。
十七、如何支持插件生命周期
复杂工具可能需要初始化和关闭资源,例如建立连接池、启动线程或加载模型。
可以定义生命周期接口:
public interface ToolLifecycle {
default void initialize(
ToolContext context) {
}
default void shutdown() {
}
}
注册时进行初始化:
if (tool instanceof ToolLifecycle lifecycle) {
lifecycle.initialize(context);
}
卸载时释放资源:
if (tool instanceof ToolLifecycle lifecycle) {
lifecycle.shutdown();
}
需要特别注意:
-
初始化失败时不能把半初始化工具放入注册表;
-
卸载前应阻止新的调用进入;
-
正在执行的任务需要等待完成或安全取消;
-
关闭异常不能影响其他插件释放资源。
十八、如何支持外部插件JAR
如果插件不在应用的主类路径中,可以扫描指定目录中的JAR文件:
application/
├── app.jar
└── plugins/
├── weather-plugin.jar
└── calculator-plugin.jar
程序可以为插件创建类加载器:
URLClassLoader pluginClassLoader =
new URLClassLoader(
new URL[]{pluginJarUrl},
Tool.class.getClassLoader()
);
再使用该类加载器执行SPI发现:
ServiceLoader<Tool> loader =
ServiceLoader.load(
Tool.class,
pluginClassLoader
);
这里必须保证Tool接口由主程序和插件共同认可。
如果插件自己又加载了一份不同的Tool接口类,即使类名相同,JVM也可能认为它们是两个不同类型,导致类型转换失败。
因此,插件API通常应放在独立的公共模块中:
tool-api
tool-core
tool-plugin-weather
tool-plugin-calculator
其中:
-
tool-api只放接口、请求对象、返回对象和注解; -
tool-core负责注册、加载、执行和生命周期; -
各插件只依赖
tool-api; -
主程序通过
tool-core管理插件。
十九、动态插件系统需要注意什么
1. 名称冲突
两个插件可能注册同名工具。
可以选择:
-
直接拒绝重复注册;
-
按优先级覆盖;
-
使用命名空间,例如
weather.current; -
使用插件编号和工具名称组成联合键。
一般不建议无提示地覆盖旧实现。
2. 版本兼容
插件应声明:
-
插件版本;
-
API版本;
-
最低主程序版本;
-
最高兼容版本。
加载前应先进行兼容性检查。
3. 安全风险
插件代码与普通Java代码一样,可能:
-
读取本地文件;
-
访问网络;
-
创建线程;
-
消耗大量CPU和内存;
-
调用危险系统命令;
-
泄露敏感信息。
因此,不能把不可信插件直接加载到核心服务进程中。
高风险场景应考虑:
-
插件签名校验;
-
来源白名单;
-
权限控制;
-
独立进程运行;
-
容器隔离;
-
请求超时;
-
资源配额;
-
审计日志。
4. 热加载和类卸载
从注册表中删除工具对象,并不代表对应类已经从JVM中卸载。
只要还有对象、线程、缓存或静态字段引用插件类加载器,类加载器就无法被垃圾回收。
因此,插件卸载时需要清理:
-
注册表中的工具引用;
-
插件创建的线程;
-
静态缓存;
-
线程上下文类加载器;
-
JDBC驱动注册;
-
日志和监听器;
-
定时任务和资源连接。
二十、ToolRegistry插件机制的推荐结构
一个相对完整的插件调用流程可以设计为:
扫描插件目录
↓
为插件创建类加载器
↓
通过ServiceLoader发现Tool实现
↓
读取ToolDefinition注解
↓
检查名称、版本和依赖
↓
执行初始化
↓
注册到ToolRegistry
↓
根据名称查找工具
↓
执行参数校验和权限检查
↓
调用工具
↓
统一封装执行结果
在此基础上,还可以加入动态代理,为全部工具统一增加:
-
执行日志;
-
超时控制;
-
权限检查;
-
调用次数统计;
-
异常转换;
-
链路追踪;
-
重试机制。
这样,SPI负责发现插件,反射和注解负责读取插件信息,ToolRegistry负责保存和查找插件,动态代理负责增强工具调用过程。
这几种机制组合后,就可以形成一个职责清晰、易于扩展的插件架构。
