荒野中的魔法:Java 巨头如 Spring、Hibernate 和 Mockito 如何使用动态代理

发布: (2026年1月1日 GMT+8 22:59)
7 分钟阅读
原文: Dev.to

Source: Dev.to

欢迎阅读系列 Java Proxies Unmasked 的第二篇。
在上一篇文章中,我们学习了经典的 Proxy 模式,构建了一个 static 代理,并了解了它为何难以扩展。随后我们介绍了 dynamic proxies,它们在运行时生成代理,解决了静态代理的 N + 1 问题。

@Transactional Onion (Spring)

在 Spring 框架中,你最常遇到代理的地方就是 @Transactional 注解。

  • 对我们来说,这只是一行代码。
  • 对 Spring 来说,这是一条信号,用来把 bean 包裹在一个复杂的 “代理洋葱” 中。

当你调用标有 @Transactional 的方法时,你并不是直接调用自己的代码;而是调用一个管理数据库连接生命周期的代理。

代理是如何创建的

  1. Bean Post‑Processor – 在 Context Refresh 阶段,Spring 的 BeanPostProcessor 会对你的类进行多步骤的转换。

  2. 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 吧!

Back to Blog

相关文章

阅读更多 »

Spring Cloud Gateway:基础示例

Spring Cloud Gateway:基础示例的封面图片 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fd...

Java 中的变量

变量 - 变量是内存位置的名称。 - 变量用于存储值。 - 变量的值可以在程序执行期间改变。 R...