Java基础知识梳理(六):动态代理、SPI与ToolRegistry插件化设计

动态代理和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创建代理对象。当外部代码调用代理对象的方法时,代理逻辑会:

  1. 读取事务配置;

  2. 开启数据库事务;

  3. 调用目标方法;

  4. 正常完成时提交事务;

  5. 发生指定异常时回滚事务。

调用者看到的仍然是普通业务接口,但实际获得的可能是代理对象。

这也是为什么一些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首先定位类路径或模块路径中的服务配置。

当程序遍历加载结果时,它会:

  1. 查找指定服务接口对应的配置;

  2. 读取实现类名称;

  3. 使用类加载器加载实现类;

  4. 检查实现类是否符合服务接口要求;

  5. 创建服务实现实例;

  6. 将实例返回给调用者。

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

这样,主程序不需要了解具体工具类。

新增工具时只需要:

  1. 实现Tool接口;

  2. 在SPI配置中声明实现类;

  3. 将插件JAR加入类路径或插件目录;

  4. 重启或触发插件重新加载。

工具就可以被注册到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负责保存和查找插件,动态代理负责增强工具调用过程。

这几种机制组合后,就可以形成一个职责清晰、易于扩展的插件架构。

相关推荐
2601_957787582 个月前
星链引擎矩阵系统:插件化多平台 API 网关与账号级隔离技术实践
java·矩阵·插件化架构
__土块__4 个月前
一次 Spring Boot 自动装配机制源码走读:从误用 @Component 到理解 Bean 生命周期
spring boot·源码分析·自动装配·bean生命周期·@configuration·configurationclasspostprocessor·cglib代理
€81110 个月前
Java入门级教程17——利用Java SPI机制制作验证码、利用Java RMI机制实现分布式登录验证系统
java·开发语言·java spi机制·远程传输数据
doglc1 年前
Java 动态代理教程(JDK 动态代理)(以RPC 过程为例)
java·rpc·动态代理·jdk动态代理
无心水1 年前
【Java面试笔记:基础】6.动态代理是基于什么原理?
java·笔记·面试·动态代理·cglib·jdk动态代理
geminigoth2 年前
Spring Boot 学习第八天:AOP代理机制对性能的影响
spring aop·jdk动态代理·cglib动态代理·aop性能分析
小沈同学呀2 年前
Java编程JDK动态代理概述与实战演示
java·开发语言·jdk·jdk动态代理
yiridancan2 年前
设计模式-代理模式
设计模式·代理模式·jdk动态代理·cglib动态代理
tyeerth3 年前
【无标题】
jdk·jdk动态代理