使用 Java 反射,你可以在运行时动态地创建接口的实现。这是通过 java.lang.reflect.Proxy 类完成的。正因如此,我将这些动态接口实现称为动态代理(dynamic proxies)。
动态代理可用于多种用途,例如:
- 数据库连接与事务管理
- 单元测试中的动态 Mock 对象
- 将依赖注入容器适配到自定义工厂接口
- AOP 风格的方法拦截等
创建代理
你可以使用 Proxy.newProxyInstance() 方法来创建动态代理。该方法接收以下 3 个参数:
- 用于“加载”动态代理类的
ClassLoader - 要实现的接口数组
- 一个
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数组:包含调用接口方法时传入的参数值。注意:接口中的基本类型(如int、long等)会被自动装箱为其对应的包装类型(如Integer、Long等)。
常见应用场景
动态代理已知至少用于以下几种场景:
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 之前、代替它、或之后调用其他对象的方法。