荒野中的魔法:Java 巨头如 Spring、Hibernate 和 Mockito 如何使用动态代理
Source: Dev.to
欢迎阅读系列 Java Proxies Unmasked 的第二篇。
在上一篇文章中,我们学习了经典的 Proxy 模式,构建了一个 static 代理,并了解了它为何难以扩展。随后我们介绍了 dynamic proxies,它们在运行时生成代理,解决了静态代理的 N + 1 问题。
@Transactional Onion (Spring)
在 Spring 框架中,你最常遇到代理的地方就是 @Transactional 注解。
- 对我们来说,这只是一行代码。
- 对 Spring 来说,这是一条信号,用来把 bean 包裹在一个复杂的 “代理洋葱” 中。
当你调用标有 @Transactional 的方法时,你并不是直接调用自己的代码;而是调用一个管理数据库连接生命周期的代理。
代理是如何创建的
-
Bean Post‑Processor – 在 Context Refresh 阶段,Spring 的
BeanPostProcessor会对你的类进行多步骤的转换。 -
InfrastructureAdvisorAutoProxyCreator – 当 Spring 扫描到带有
@Transactional的类时,这个组件会问:这个 bean 有 “Pointcut” 匹配吗?(例如,它是否被
@Transactional注解?)
我应该应用什么 “Advice”?(在本例中是TransactionInterceptor。)
创建阶段(包装器)
Spring 使用 ProxyFactory。根据你的配置和类结构,它会选择以下两条路径之一:
| 代理类型 | 使用场景 |
|---|---|
| JDK Dynamic Proxy | 服务实现了接口(例如 UserServiceImpl implements UserService)。Spring 使用 java.lang.reflect.Proxy 创建代理。 |
| CGLIB Proxy | 服务是没有接口的具体类。Spring 使用 CGLIB 在运行时生成子类。 |
注意: JDK 动态代理将在下一篇文章中展开讨论。CGLIB(较老的技术)在新版 Spring 中正被 ByteBuddy 取代。
当客户端调用你的服务时,会触发代理的 invoke()(JDK)或 intercept()(CGLIB)方法:
// Pseudo‑code for a JDK dynamic proxy
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. TransactionAspectSupport.invokeWithinTransaction(...)
// 2. Execute the target method
// 3. Commit / rollback as needed
}
秘密武器:TransactionAspectSupport
真正的“引擎舱”位于 TransactionAspectSupport.java,尤其是 invokeWithinTransaction 方法,其中包含了包装你代码的 try‑catch 代码块。
这也是为什么 自调用(self‑invocation)不起作用的原因。 如果 UserService 中的一个方法通过 this.otherMethod() 调用另一个 @Transactional 方法,调用永远不会经过代理,事务逻辑就会被跳过。
延迟加载 (Hibernate)
Hibernate 使用代理来解决一个巨大的性能问题:延迟加载。
想象一个 User 实体有一个 List<Order>。每次获取用户时都加载所有订单会让数据库瘫痪。Hibernate 默认对 @OneToMany 和 @ManyToMany 关联使用延迟加载,返回 空壳对象,只有在访问时才会获取数据。
工作原理
- Hibernate 返回一个扩展实体类的代理。
- 代理使用 ByteBuddy 生成;对于
Order类,你可能会看到一个名为Order$HibernateProxy$Q4X0SwcL的类。
生成的子类包含的内容
LazyInitializer字段 – 存储实体 ID、Session引用以及延迟初始化逻辑。- 重写的访问器方法,在首次访问时触发 SQL 查询,然后缓存结果。
当你在这个代理上调用 getter 时,ByteBuddy 会拦截调用,检查数据是否已加载;如果没有,它会通过执行 SQL 查询来 填充 对象。
秘密武器:ByteBuddyProxyFactory
Hibernate 源码中的工厂类 ByteBuddyProxyFactory.java 配置了 ByteBuddy 如何构建空壳子类,并将 getter/setter 与 LazyInitializer 关联起来。
Source: …
Mockito Spy
Mockito spy 会包装一个真实对象。与从无行为开始的 mock 不同,spy 会将方法调用委托给底层对象,除非你显式地对其进行存根(stub)。
List list = new ArrayList<>();
List spyList = spy(list); // 为真实列表创建一个代理
// 被代理拦截,但仍委托给真实方法
spyList.size(); // → 0
// 为代理存根返回自定义值
doReturn(100).when(spyList).size();
spyList.size(); // → 100
Mockito 如何创建 spy
Mockito.spy(myObject) 使用 ByteBuddy 在运行时生成一个子类(例如 PaymentService$MockitoMock$cG76HxJ8v)。生成的类会复制真实对象的状态,使代理能够在仍能拦截并存根的同时,将调用委托给真实对象。
TL;DR
| 框架 | 使用代理的原因 | 代理技术 |
|---|---|---|
Spring (@Transactional) | 透明地管理事务边界 | JDK 动态代理 或 CGLIB(由 ProxyFactory 选择) |
| Hibernate | 懒加载关联以避免不必要的数据库访问 | ByteBuddy 生成的子类($HibernateProxy$…) |
| Mockito | 提供可以委托或存根行为的测试替身 | ByteBuddy 生成的子类($MockitoMock$…) |
MockMethodInterceptor
Mockito 生成的子类中的每个方法都会被重写,以调用唯一的调度器:MockMethodInterceptor。该拦截器持有所有存根指令的注册表(例如 when(...).thenReturn(...))。
当你在 spy 上调用方法时,代理会决定:
“我应该执行真实代码,还是返回预设的答案?”
when() vs. doReturn()
-
使用
when(spy.get(0)).thenReturn("foo");会在存根之前实际调用一次
get(0)方法,这在空列表上会抛出IndexOutOfBoundsException。 -
使用
doReturn("foo").when(spy).get(0);直接与代理的设置交互;真实方法永不被调用。
秘密武器:InvocationContainer
InvocationContainerImpl.java 会记录对代理的每一次方法调用,从而支持后续的验证,例如:
verify(spy).add(anyString());
前瞻
我们已经揭示了 Spring、Hibernate 和 Mockito 代理背后的魔法。在下一篇文章中,我们将仅使用标准 JDK 构建我们的第一个动态代理——不依赖外部库,也不使用 Maven 依赖——纯粹的 Java。
Part 3: The Native Way 将演示如何编写 InvocationHandler。准备好你的 IDE 吧!