Skip to content

代理机制 (Proxying Mechanisms)

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

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

如果你想强制使用 CGLIB 代理(例如,为了代理目标对象中定义的所有方法,而不仅仅是其接口实现的方法),可以进行配置。但是,你需要考虑以下问题:

  • final 类不能被代理,因为它们不能被扩展(继承)。
  • final 方法不能被通知,因为它们不能被重写。
  • private 方法不能被通知,因为它们不能被重写。
  • 不可见的方法(例如:来自不同包的父类中的包私有方法)不能被通知,因为它们在效果上是私有的。
  • 被代理对象的构造函数不会被调用两次,因为 CGLIB 代理实例是通过 Objenesis 创建的。但是,如果你的 JVM 不允许绕过构造函数,你可能会看到两次调用以及来自 Spring AOP 支持的相应调试日志条目。
  • CGLIB 代理的使用可能会面临 Java 模块系统(Java Module System)的限制。典型的案例是,当部署在模块路径上时,你不能为 java.lang 包中的类创建 CGLIB 代理。这种情况需要 JVM 启动标志 --add-opens=java.base/java.lang=ALL-UNNAMED,但模块并不支持该标志。

强制指定特定 AOP 代理类型 (Forcing Specific AOP Proxy Types)

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

xml
<aop:config proxy-target-class="true">
	<!-- 此处定义其他 Bean... -->
</aop:config>

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

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

TIP

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

明确地说,如果在 <tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/> 元素中的任何一个使用了 proxy-target-class="true",都会强制这三者全部使用 CGLIB 代理。

@EnableAspectJAutoProxy@EnableTransactionManagement 及其相关的配置注解也提供了相应的 proxyTargetClass 属性。它们同样会被合并为一个统一的自动代理创建器,在运行时应用最强的代理设置。从 Spring 7.0 开始,这同样适用于单个代理处理器,例如 @EnableAsync,它们会一致地参与给定应用中所有自动代理尝试的统一全局默认设置。

全局默认代理类型在不同设置下可能有所不同。核心框架默认建议使用基于接口的代理,但 Spring Boot 可能会根据配置属性默认启用基于类的代理(CGLIB)。

从 Spring 7.0 开始,可以通过在给定的 @Bean 方法或 @Component 类上使用 @Proxyable 注解来为单个 Bean 强制指定特定的代理类型。@Proxyable(INTERFACES)@Proxyable(TARGET_CLASS) 会覆盖任何全局配置的默认值。出于非常特殊的用途,你甚至可以通过 @Proxyable(interfaces=…) 指定要使用的代理接口,从而限制只暴露选定的接口,而不是目标 Bean 实现的所有接口。

理解 AOP 代理 (Understanding AOP Proxies)

Spring AOP 是基于代理的。在你编写自己的切面或使用 Spring 框架随附的任何基于 Spring AOP 的切面之前,掌握这句话的确切语义至关重要。

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

java
public class SimplePojo implements Pojo {

	public void foo() {
		// 下一个方法调用是对 'this' 引用的直接调用
		this.bar();
	}

	public void bar() {
		// 某些逻辑...
	}
}
kotlin
class SimplePojo : Pojo {

	fun foo() {
		// 下一个方法调用是对 'this' 引用的直接调用
		this.bar()
	}

	fun bar() {
		// 某些逻辑...
	}
}

如果你直接在一个对象引用上调用方法,该方法将直接在该对象引用上运行,如下图和清单所示:

POJO 直接调用

java
public class Main {

	public static void main(String[] args) {
		Pojo pojo = new SimplePojo();
		// 这是在 'pojo' 引用上的直接方法调用
		pojo.foo();
	}
}
kotlin
fun main() {
	val pojo = SimplePojo()
	// 这是在 'pojo' 引用上的直接方法调用
	pojo.foo()
}

当客户端代码持有的引用是一个代理时,情况会发生微妙的变化。请看下图和代码片段:

代理调用

java
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();
		// 这是在代理上的方法调用!
		pojo.foo();
	}
}
kotlin
fun main() {
	val factory = ProxyFactory(SimplePojo())
	factory.addInterface(Pojo::class.java)
	factory.addAdvice(RetryAdvice())

	val pojo = factory.proxy as Pojo
	// 这是在代理上的方法调用!
	pojo.foo()
}

这里需要理解的关键点是:Main 类的 main 方法中的客户端代码持有的是代理的引用。这意味着在该对象引用上的方法调用就是对代理的调用。因此,代理可以委派给与该特定方法调用相关的所有拦截器(通知)。

然而,一旦调用最终到达了目标对象(在本例中是 SimplePojo 的引用),它内部可能会进行的任何自调用(例如 this.bar()this.foo())都将针对 this 引用(即原始对象)运行,而不是针对代理

这具有重要的影响:自调用(Self-invocation)不会导致关联的通知运行。 换句话说,通过显式或隐式的 this 引用进行的自调用将绕过代理,从而绕过通知。

为了解决这个问题,你有以下选择:

  1. 避免自调用:最好的方法(这里的“最好”是宽泛指代的)是重构你的代码,使自调用不再发生。虽然这需要你做一些工作,但它是侵入性最小、也是最好的方法。
  2. 注入自我引用:另一种方法是利用自我注入,通过自我引用(代理)而不是通过 this 来调用方法。
  3. 使用 AopContext.currentProxy():这种方法是高度不推荐的,我们在指出它时有些犹豫,因为前两种选择更好。但作为最后手段,你可以选择将类内部的逻辑与 Spring AOP 挂钩。
java
public class SimplePojo implements Pojo {

	public void foo() {
		// 这样做有效,但应尽可能避免。
		((Pojo) AopContext.currentProxy()).bar();
	}

	public void bar() {
		// 某些逻辑...
	}
}
kotlin
class SimplePojo : Pojo {

	fun foo() {
		// 这样做有效,但应尽可能避免。
		(AopContext.currentProxy() as Pojo).bar()
	}

	fun bar() {
		// 某些逻辑...
	}
}

使用 AopContext.currentProxy() 会使你的代码完全耦合到 Spring AOP,并使类本身意识到自己正处于 AOP 上下文中,这削弱了 AOP 的解耦优势。它还要求 ProxyFactory 配置为暴露代理(Expose Proxy):

java
ProxyFactory factory = new ProxyFactory(new SimplePojo());
// ...
factory.setExposeProxy(true);
kotlin
val factory = ProxyFactory(SimplePojo())
// ...
factory.isExposeProxy = true

TIP

AspectJ 的编译时织入(Compile-time weaving)和加载时织入(Load-time weaving)不存在这种“自调用”问题,因为它们是在字节码内部直接应用通知,而不是通过代理。


补充教学

1. JDK 动态代理 vs CGLIB 深度对比

特性JDK 动态代理 (Default for Interfaces)CGLIB (Default for Classes)
底层原理利用 java.lang.reflect.Proxy 动态生成接口实现类利用 ASM 字节码库动态生成目标类的子类
性能早期版本较慢,JDK 8+ 之后性能已与 CGLIB 基本持平首次生成类时稍慢,运行期调用效率极高
局限性目标对象必须实现接口类和方法不能是 final (因为无法继承/重写)
调用链Client -> Proxy -> Interceptor -> TargetClient -> Proxy -> Interceptor -> Target

2. 春秋笔法:Spring Boot 的默认选择

虽然 Spring Framework 核心默认倾向于 JDK 代理,但 Spring Boot (自 2.x 起) 默认将 spring.aop.proxy-target-class 设置为 true。 这意味着在 Spring Boot 应用中,即使你的 Bean 实现了接口,默认也会使用 CGLIB 代理。这样做的目的是为了减少由于开发者不小心将 Bean 强转为实现类而导致的 ClassCastException

3. “自调用”大坑:为什么 @Transactional 会失效?

这是面试中最常问的 Spring 问题。

java
@Service
public class OrderService {
    public void placeOrder() {
        this.pay(); // 直接调用,不走代理!
    }

    @Transactional
    public void pay() {
        // 更新数据库操作
    }
}

现象:直接调用 placeOrder() 里面的 pay() 不会开启事务。 根本原因@Transactional 是基于 AOP 代理的。当你调用 this.pay() 时,跳过了代理对象,直接在原始对象上执行,拦截器根本没机会介入。

4. 优雅解决自调用的现代方案:Self-Injection

比起侵入性极强的 AopContext,现在更推荐利用 Spring 的自注入功能:

java
@Service
public class OrderService {
    @Autowired
    @Lazy // 处理循环引用
    private OrderService self;

    public void placeOrder() {
        self.pay(); // 通过代理调用,事务生效!
    }

    @Transactional
    public void pay() { ... }
}

5. Spring 7.0 之后的新武器:@Proxyable

在 Spring 7.0 之前,强制代理类型通常是“全局一刀切”的配置。 现在的 @Proxyable 允许你在** Bean 级别**进行精细化控制。如果你有一个特殊的 Bean 必须要用 JDK 代理(为了轻量、或是为了规避 CGLIB 对模块化的限制),而其他 Bean 用 CGLIB,这个注解将非常管用。

Based on Spring Framework.