代理机制
Spring AOP 使用 JDK 动态代理或 CGLIB 来为目标对象创建代理。JDK 动态代理内置于 JDK 中,而 CGLIB 是一个常用的开源类定义库(已重新打包到 spring-core 中)。
如果要被代理的目标对象至少实现了一个接口,则使用JDK动态代理,并且目标类型所实现的所有接口都会被代理。 如果目标对象没有实现任何接口,则会创建一个CGLIB代理,该代理是目标类型的运行时生成的子类。
如果你想强制使用 CGLIB 代理(例如,代理目标对象定义的所有方法,而不仅仅是其接口中实现的方法),你可以这样做。但是,你应该考虑以下问题:
-
final类无法被代理,因为它们不能被继承。 -
final方法无法被通知(advised),因为它们不能被重写(overridden)。 -
private方法无法被通知(advised),因为它们不能被重写(overridden)。 -
无法对不可见的方法(例如,位于不同包中的父类中的包私有方法)进行增强,因为它们实际上相当于私有方法。
-
由于 CGLIB 代理实例是通过 Objenesis 创建的,因此您被代理对象的构造函数不会被调用两次。然而,如果您的 JVM 不允许绕过构造函数,您可能会看到构造函数被调用两次,并在 Spring 的 AOP 支持中看到相应的调试日志条目。
-
您使用 CGLIB 代理可能会受到 Java 模块系统的限制。典型的情况是,当在模块路径(module path)上部署时,您无法为
java.lang包中的类创建 CGLIB 代理。这类情况需要使用 JVM 启动参数--add-opens=java.base/java.lang=ALL-UNNAMED,而该参数对模块不可用。
强制指定 AOP 代理类型
要强制使用 CGLIB 代理,请将 proxy-target-class 元素的 <aop:config> 属性值设置为 true,如下所示:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
当你使用 @AspectJ 自动代理支持时,若要强制使用 CGLIB 代理,请将 proxy-target-class 元素的 <aop:aspectj-autoproxy> 属性设置为 true,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
|
多个 需要明确的是,在 |
@EnableAspectJAutoProxy、@EnableTransactionManagement 及相关配置注解提供对应的 proxyTargetClass 属性。这些也被合并为一个统一的自动代理创建器,从而在运行时有效应用最强的代理设置。自 7.0 版本起,这也适用于各个独立的代理处理器,例如 @EnableAsync,使其一致地参与给定应用中所有自动代理尝试的统一全局默认设置。
全局默认的代理类型在不同设置中可能有所不同。虽然核心框架默认建议使用基于接口的代理,但 Spring Boot 可能会根据配置属性,默认启用基于类的代理。
从 7.0 版本开始,可以通过在特定的 @Proxyable 方法或 @Bean 类上使用 @Component 注解来为单个 Bean 强制指定代理类型,其中 @Proxyable(INTERFACES) 或 @Proxyable(TARGET_CLASS) 会覆盖任何全局配置的默认值。出于非常特定的目的,您甚至可以通过 @Proxyable(interfaces=…) 明确指定要使用的代理接口,从而将暴露范围限制为所选接口,而不是目标 Bean 实现的所有接口。
理解 AOP 代理
Spring AOP 基于代理。在编写自己的切面或使用 Spring 框架所提供的任何基于 Spring AOP 的切面之前,你必须充分理解上述语句的实际含义,这一点至关重要。
首先考虑这样一种场景:你持有一个普通的、未经代理的对象引用,如下列代码片段所示:
-
Java
-
Kotlin
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...
}
}
class SimplePojo : Pojo {
fun foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar()
}
fun bar() {
// some logic...
}
}
如果你在一个对象引用上调用一个方法,该方法会直接在该对象引用上被调用,如下图和代码清单所示:
-
Java
-
Kotlin
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();
}
}
fun main() {
val pojo = SimplePojo()
// this is a direct method call on the 'pojo' reference
pojo.foo()
}
当客户端代码持有的引用是一个代理时,情况会略有不同。请看下面的图示和代码片段:
-
Java
-
Kotlin
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();
}
}
fun main() {
val factory = ProxyFactory(SimplePojo())
factory.addInterface(Pojo::class.java)
factory.addAdvice(RetryAdvice())
val pojo = factory.proxy as Pojo
// this is a method call on the proxy!
pojo.foo()
}
这里需要理解的关键点是:main(..) 类的 Main 方法中的客户端代码持有对代理对象的引用。这意味着对该对象引用的方法调用实际上是调用代理对象。因此,代理可以将调用委派给与该特定方法调用相关的所有拦截器(通知)。
然而,一旦调用最终到达目标对象(在本例中是 SimplePojo 的引用),该目标对象对其自身发起的任何方法调用(例如 this.bar() 或 this.foo())都将通过 this 引用直接调用,而不会经过代理。
这一点具有重要的影响:这意味着自调用不会触发与该方法调用相关联的通知逻辑。换句话说,通过显式或隐式的 this 引用进行的自调用会绕过通知。
为了解决这个问题,您有以下几种选择。
- 避免自我调用
-
最佳的方法(此处“最佳”一词的使用较为宽松)是重构您的代码,以避免发生自调用。这确实需要您做一些工作,但这是最佳且侵入性最小的方式。
- 注入一个自引用
-
另一种方法是利用自注入,并通过自引用(而非通过
this)来调用代理上的方法。 - 使用
AopContext.currentProxy() -
最后这种方法极不推荐使用,我们之所以犹豫地指出这一点,是因为更倾向于前面提到的选项。然而,作为万不得已的最后手段,你可以选择将类中的逻辑与 Spring AOP 绑定,如下例所示。
-
Java
-
Kotlin
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...
}
}
class SimplePojo : Pojo {
fun foo() {
// This works, but it should be avoided if possible.
(AopContext.currentProxy() as Pojo).bar()
}
fun bar() {
// some logic...
}
}
使用 AopContext.currentProxy() 会将您的代码完全耦合到 Spring AOP,并使类自身意识到它正处于 AOP 上下文中,从而削弱了 AOP 的某些优势。此外,它还要求将 ProxyFactory 配置为暴露代理,如下例所示:
-
Java
-
Kotlin
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();
}
}
fun main() {
val factory = ProxyFactory(SimplePojo())
factory.addInterface(Pojo::class.java)
factory.addAdvice(RetryAdvice())
factory.isExposeProxy = true
val pojo = factory.proxy as Pojo
// this is a method call on the proxy!
pojo.foo()
}
| AspectJ 的编译时织入和加载时织入不存在这种自调用问题,因为它们是直接在字节码中应用通知(advice),而不是通过代理实现的。 |