跳到主要内容

代理机制

ChatGPT-4o-mini 中英对照 Proxying Mechanisms

Spring AOP 使用 JDK 动态代理或 CGLIB 来为给定的目标对象创建代理。JDK 动态代理内置于 JDK 中,而 CGLIB 是一个常见的开源类定义库(重新打包到 spring-core 中)。

如果要代理的目标对象实现了至少一个接口,则使用 JDK 动态代理,并且目标类型实现的所有接口都被代理。如果目标对象不实现任何接口,则创建一个 CGLIB 代理,它是目标类型的运行时生成的子类。

如果您想强制使用 CGLIB 代理(例如,代理目标对象定义的每个方法,而不仅仅是其接口实现的方法),您可以这样做。然而,您应该考虑以下问题:

  • final 类不能被代理,因为它们不能被扩展。

  • final 方法不能被增强,因为它们不能被重写。

  • private 方法不能被增强,因为它们不能被重写。

  • 不可见的方法 – 例如,来自不同包的父类中的包私有方法 – 不能被增强,因为它们实际上是私有的。

  • 你的代理对象的构造函数不会被调用两次,因为 CGLIB 代理实例是通过 Objenesis 创建的。然而,如果你的 JVM 不允许构造函数绕过,你可能会看到双重调用和来自 Spring AOP 支持的相应调试日志条目。

  • 你的 CGLIB 代理使用可能会面临 Java 模块系统的限制。作为一个典型案例,当在模块路径上部署时,你不能为来自 java.lang 包的类创建 CGLIB 代理。这种情况需要一个 JVM 启动标志 --add-opens=java.base/java.lang=ALL-UNNAMED,而该标志在模块中不可用。

要强制使用 CGLIB 代理,请将 <aop:config> 元素的 proxy-target-class 属性的值设置为 true,如下所示:

<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
xml

要在使用 @AspectJ 自动代理支持时强制使用 CGLIB 代理,请将 <aop:aspectj-autoproxy> 元素的 proxy-target-class 属性设置为 true,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true"/>
xml
备注

多个 <aop:config/> 部分在运行时被合并为一个统一的自动代理创建器,该创建器应用任何 <aop:config/> 部分(通常来自不同的 XML bean 定义文件)指定的 最强 代理设置。这同样适用于 <tx:annotation-driven/><aop:aspectj-autoproxy/> 元素。

为了明确,在 <tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/> 元素上使用 proxy-target-class="true" 强制对 所有三个 使用 CGLIB 代理。

理解 AOP 代理

Spring AOP 是基于代理的。在您编写自己的切面或使用 Spring 框架提供的任何基于 Spring AOP 的切面之前,理解最后一句话的语义是至关重要的。

考虑首先一种场景,其中你有一个普通的、未代理的对象引用,如以下代码片段所示:

public class SimplePojo implements Pojo {

public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}

public void bar() {
// some logic...
}
}
java

如果您在对象引用上调用一个方法,该方法将直接在该对象引用上被调用,如下图和列表所示:

aop proxy plain pojo call

public class Main {

public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
java

当客户端代码所引用的是一个代理时,情况会稍有不同。考虑以下图表和代码片段:

aop proxy call

public class Main {

public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());

Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
java

这里需要理解的关键是,Main 类的 main(..) 方法中的客户端代码持有对代理的引用。这意味着对该对象引用的调用实际上是对代理的调用。因此,代理可以委托所有与该特定方法调用相关的拦截器(建议)。然而,一旦调用最终到达目标对象(在这种情况下是 SimplePojo 引用),它可能对自身进行的任何方法调用,例如 this.bar()this.foo(),将会针对 this 引用进行调用,而不是代理。这具有重要的意义。这意味着自我调用不会导致与方法调用相关的建议有机会运行。换句话说,通过显式或隐式的 this 引用进行的自我调用将绕过建议。

为了解决这个问题,您有以下选项。

避免自我调用

最佳的方法(这里“最佳”一词使用得比较宽泛)是重构你的代码,使得自我调用不再发生。这确实需要你付出一些努力,但这是最好的、侵入性最小的方法。

注入自我引用

一种替代方法是利用 自我注入,通过自我引用而不是通过 this 调用代理上的方法。

使用 AopContext.currentProxy()

这种最后的方法是非常不推荐的,我们不愿意指出它,而是更倾向于之前的选项。然而,作为最后的手段,您可以选择将您的类中的逻辑与 Spring AOP 绑定,如下例所示。

public class SimplePojo implements Pojo {

public void foo() {
// This works, but it should be avoided if possible.
((Pojo) AopContext.currentProxy()).bar();
}

public void bar() {
// some logic...
}
}
java

使用 AopContext.currentProxy() 完全将你的代码与 Spring AOP 绑定在一起,并使类本身意识到它是在 AOP 上下文中使用的,这减少了 AOP 的一些好处。它还要求 ProxyFactory 被配置为暴露代理,如下例所示:

public class Main {

public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);

Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
java
备注

AspectJ 的编译时织入和加载时织入没有这个自我调用的问题,因为它们在字节码中应用通知,而不是通过代理。