Java 反射 - 动态代理

更新于 2025-12-25

使用 Java 反射,你可以在运行时动态地创建接口的实现。这是通过 java.lang.reflect.Proxy 类完成的。正因如此,我将这些动态接口实现称为动态代理(dynamic proxies)

动态代理可用于多种用途,例如:

  • 数据库连接与事务管理
  • 单元测试中的动态 Mock 对象
  • 将依赖注入容器适配到自定义工厂接口
  • AOP 风格的方法拦截等

创建代理

你可以使用 Proxy.newProxyInstance() 方法来创建动态代理。该方法接收以下 3 个参数:

  1. 用于“加载”动态代理类的 ClassLoader
  2. 要实现的接口数组
  3. 一个 InvocationHandler,所有对代理的方法调用都将转发给它

示例代码如下:

InvocationHandler handler = new MyInvocationHandler();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
    MyInterface.class.getClassLoader(),
    new Class[] { MyInterface.class },
    handler
);

执行上述代码后,变量 proxy 就包含了一个 MyInterface 接口的动态实现。对该代理的所有方法调用都会被转发到 handler 所实现的通用 InvocationHandler 接口中。下一节将详细介绍 InvocationHandler


InvocationHandler

如前所述,你必须向 Proxy.newProxyInstance() 方法传入一个 InvocationHandler 的实现。所有对动态代理的方法调用都会被转发到这个 InvocationHandler 实现中。

InvocationHandler 接口定义如下:

public interface InvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

下面是一个具体的实现示例:

public class MyInvocationHandler implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在这里执行一些“动态”逻辑
    }
}
  • proxy 参数:传递给 invoke() 方法的 proxy 是实现了目标接口的动态代理对象。通常你并不需要使用这个对象。
  • Method 对象:表示在代理所实现的接口上调用的方法。你可以从中获取方法名、参数类型、返回类型等信息(详见 Methods 章节)。
  • Object[] args 数组:包含调用接口方法时传入的参数值。注意:接口中的基本类型(如 intlong 等)会被自动装箱为其对应的包装类型(如 IntegerLong 等)。

常见应用场景

动态代理已知至少用于以下几种场景:

1. 数据库连接与事务管理

Spring 框架提供了一个事务代理,可以自动为你开启、提交或回滚事务。简要流程如下:

Web 控制器 --> proxy.execute(...);
proxy --> connection.setAutoCommit(false);
proxy --> realAction.execute(); // realAction 执行数据库操作
proxy --> connection.commit();

2. 单元测试中的动态 Mock 对象

Butterfly Testing Tools 利用动态代理实现动态的 stub、mock 和代理,用于单元测试。

当你测试类 A(它依赖于另一个类 B,通常是接口)时,可以将 B 的 mock 实现传入 A,而不是真实的 B。所有对 B 的方法调用都会被记录下来,你还可以预设 mock 对象的返回值。

此外,Butterfly Testing Tools 还支持将真实的 B 包裹在一个 mock B 中:所有方法调用先被记录,然后转发给真实的 B。这样你就能验证真实对象是否被正确调用。

例如,在测试 DAO 时,你可以将数据库连接包装在一个 mock 中。DAO 不会察觉任何差异,仍可正常读写数据库,但你可以通过 mock 检查 DAO 是否正确使用了连接(比如是否调用了 connection.close(),或者是否未调用——这在常规返回值中是无法判断的)。

3. 将 DI 容器适配到自定义工厂接口

依赖注入容器有一个强大特性:它可以将整个容器注入到它所创建的 bean 中。但为了避免对容器接口产生依赖,该容器能将自身适配为你设计的自定义工厂接口——你只需定义接口,无需提供实现。

例如,你的工厂接口和使用类可能如下所示:

public interface IMyFactory {
    Bean bean1();
    Person person();
    // ...
}

public class MyAction {
    protected IMyFactory myFactory = null;

    public MyAction(IMyFactory factory) {
        this.myFactory = factory;
    }

    public void execute() {
        Bean bean = this.myFactory.bean();
        Person person = this.myFactory.person();
    }
}

MyAction 调用注入的 IMyFactory 实例上的方法时,这些调用会被动态代理转换为对容器 IContainer.instance() 方法的调用(这是从容器中获取实例的标准方式)。这样,对象就可以在运行时将 Butterfly Container 作为工厂使用,而不仅限于在创建时注入依赖,且完全不依赖任何 Butterfly Container 特定的接口。

4. AOP 风格的方法拦截

Spring 框架允许你拦截对某个 bean 的方法调用(前提是该 bean 实现了某个接口)。Spring 会将该 bean 包装在一个动态代理中,所有对该 bean 的调用都会先经过代理。代理可以选择在委托给原始 bean 之前代替它、或之后调用其他对象的方法。