此版本仍在开发中,尚未被视为稳定版。如需最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

代理机制

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

如果要被代理的目标对象至少实现了一个接口,则使用JDK动态代理,并且目标类型所实现的所有接口都会被代理。 如果目标对象没有实现任何接口,则会创建一个CGLIB代理,该代理是目标类型的运行时生成的子类。spring-doc.cadn.net.cn

如果你想强制使用 CGLIB 代理(例如,代理目标对象定义的所有方法,而不仅仅是其接口中实现的方法),你可以这样做。但是,你应该考虑以下问题:spring-doc.cadn.net.cn

  • final 类无法被代理,因为它们不能被继承。spring-doc.cadn.net.cn

  • final 方法无法被通知(advised),因为它们不能被重写(overridden)。spring-doc.cadn.net.cn

  • private 方法无法被通知(advised),因为它们不能被重写(overridden)。spring-doc.cadn.net.cn

  • 无法对不可见的方法(例如,位于不同包中的父类中的包私有方法)进行增强,因为它们实际上相当于私有方法。spring-doc.cadn.net.cn

  • 由于 CGLIB 代理实例是通过 Objenesis 创建的,因此您被代理对象的构造函数不会被调用两次。然而,如果您的 JVM 不允许绕过构造函数,您可能会看到构造函数被调用两次,并在 Spring 的 AOP 支持中看到相应的调试日志条目。spring-doc.cadn.net.cn

  • 您使用 CGLIB 代理可能会受到 Java 模块系统的限制。典型的情况是,当在模块路径(module path)上部署时,您无法为 java.lang 包中的类创建 CGLIB 代理。这类情况需要使用 JVM 启动参数 --add-opens=java.base/java.lang=ALL-UNNAMED,而该参数对模块不可用。spring-doc.cadn.net.cn

强制指定 AOP 代理类型

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

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

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

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

多个 <aop:config/> 配置段在运行时会被合并为一个统一的自动代理创建器,该创建器会应用所有 <aop:config/> 配置段(通常来自不同的 XML Bean 定义文件)中指定的最强代理设置。 这一点同样适用于 <tx:annotation-driven/><aop:aspectj-autoproxy/> 元素。spring-doc.cadn.net.cn

需要明确的是,在 <tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/> 元素上使用 proxy-target-class="true" 会强制对所有三者使用 CGLIB 代理。spring-doc.cadn.net.cn

@EnableAspectJAutoProxy@EnableTransactionManagement 及相关配置注解提供对应的 proxyTargetClass 属性。这些也被合并为一个统一的自动代理创建器,从而在运行时有效应用最强的代理设置。自 7.0 版本起,这也适用于各个独立的代理处理器,例如 @EnableAsync,使其一致地参与给定应用中所有自动代理尝试的统一全局默认设置。spring-doc.cadn.net.cn

全局默认的代理类型在不同设置中可能有所不同。虽然核心框架默认建议使用基于接口的代理,但 Spring Boot 可能会根据配置属性,默认启用基于类的代理。spring-doc.cadn.net.cn

从 7.0 版本开始,可以通过在特定的 @Proxyable 方法或 @Bean 类上使用 @Component 注解来为单个 Bean 强制指定代理类型,其中 @Proxyable(INTERFACES)@Proxyable(TARGET_CLASS) 会覆盖任何全局配置的默认值。出于非常特定的目的,您甚至可以通过 @Proxyable(interfaces=…​) 明确指定要使用的代理接口,从而将暴露范围限制为所选接口,而不是目标 Bean 实现的所有接口。spring-doc.cadn.net.cn

理解 AOP 代理

Spring AOP 基于代理。在编写自己的切面或使用 Spring 框架所提供的任何基于 Spring AOP 的切面之前,你必须充分理解上述语句的实际含义,这一点至关重要。spring-doc.cadn.net.cn

首先考虑这样一种场景:你持有一个普通的、未经代理的对象引用,如下列代码片段所示:spring-doc.cadn.net.cn

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...
	}
}

如果你在一个对象引用上调用一个方法,该方法会直接在该对象引用上被调用,如下图和代码清单所示:spring-doc.cadn.net.cn

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();
	}
}
fun main() {
	val pojo = SimplePojo()
	// this is a direct method call on the 'pojo' reference
	pojo.foo()
}

当客户端代码持有的引用是一个代理时,情况会略有不同。请看下面的图示和代码片段:spring-doc.cadn.net.cn

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();
	}
}
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 引用进行的自调用会绕过通知。spring-doc.cadn.net.cn

为了解决这个问题,您有以下几种选择。spring-doc.cadn.net.cn

避免自我调用

最佳的方法(此处“最佳”一词的使用较为宽松)是重构您的代码,以避免发生自调用。这确实需要您做一些工作,但这是最佳且侵入性最小的方式。spring-doc.cadn.net.cn

注入一个自引用

另一种方法是利用自注入,并通过自引用(而非通过 this)来调用代理上的方法。spring-doc.cadn.net.cn

使用 AopContext.currentProxy()

最后这种方法极不推荐使用,我们之所以犹豫地指出这一点,是因为更倾向于前面提到的选项。然而,作为万不得已的最后手段,你可以选择将类中的逻辑与 Spring AOP 绑定,如下例所示。spring-doc.cadn.net.cn

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 配置为暴露代理,如下例所示:spring-doc.cadn.net.cn

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),而不是通过代理实现的。