AOP 代理 (AOP Proxies)
Spring AOP 默认使用标准的 JDK 动态代理 作为 AOP 代理。这使得任何接口(或一组接口)都可以被代理。
Spring AOP 也可以使用 CGLIB 代理。当需要代理类而不是接口时,CGLIB 是必需的。默认情况下,如果业务对象没有实现任何接口,则会使用 CGLIB。由于“面向接口编程”而非“面向类编程”是良好的实践,因此业务类通常会实现一个或多个业务接口。但在某些(希望是少数)情况下,如果你需要通知(Advise)一个未在接口上声明的方法,或者需要将代理对象作为具体类型传递给方法,则可以强制使用 CGLIB。
理解“Spring AOP 是基于代理的”这一事实至关重要。请参阅理解 AOP 代理,以深入了解这一实现细节的具体含义。
补充教学
1. JDK 动态代理 vs. CGLIB 代理:双雄会
在 Spring AOP 的底层,代理模式是其灵魂。以下是两者的核心差异:
| 特性 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 原理 | 利用反射机制生成一个实现代理接口的匿名类。 | 利用 ASM 字节码操作框架,生成目标类的子类。 |
| 限制 | 必须实现接口。 | 不能是 final 类,且不能拦截 final 方法。 |
| Spring 默认行为 | 如果目标类实现了接口,默认用它。 | 如果目标类没实现接口,自动切换到它。 |
| 性能 | 在现代 JDK 中性能已经非常出色。 | 历史上性能略高于 JDK,但现在差异极小。 |
2. 为什么推荐“面向接口编程”?
Spring 默认倾向于 JDK 动态代理,是因为它更符合装饰器模式和依赖倒置原则。使用接口作为代理边界,可以确保你的代码与具体的实现类解耦,即便底层实现发生了变化(比如从 MyServiceImpl 换成 NewServiceImpl),调用方也不受影响。
3. 如何强制使用 CGLIB?
如果你明明有接口,但出于某些特殊原因(比如需要强转为实现类)非要用 CGLIB,可以进行如下配置:
- Java 配置 (Spring Boot):properties
# 在 application.properties 中 spring.aop.proxy-target-class=true - 注解配置:java
@EnableAspectJAutoProxy(proxyTargetClass = true)
4. 一个致命的陷阱:final 与 private
- CGLIB 的软肋:既然 CGLIB 是通过生成子类来重写父类方法实现的,那么
final方法是无法被子类重写的,因此 AOP 对final方法无效。 - 共同限制:无论是 JDK 还是 CGLIB,都无法代理
static方法。此外,Spring AOP 由于是通过代理调用的,因此private方法也无法被拦截。
5. 运行时如何确认代理类型?
你在调试时想知道某个 Bean 到解是哪种代理?可以通过代码查看:
java
// 使用 AopUtils 工具类
System.out.println("是否是 JDK 代理: " + AopUtils.isJdkDynamicProxy(myBean));
System.out.println("是否是 CGLIB 代理: " + AopUtils.isCglibProxy(myBean));或者在调试器(Debugger)中观察对象的类名:
- JDK 代理通常包含
$Proxy字样。 - CGLIB 代理通常包含
$$EnhancerBySpringCGLIB$$字样。