原文来自于:zha-ge.cn/java/134
代理选错,性能和功能全翻车!Spring AOP 的默认技术别再搞混
上周一个同事跑来找我:
"哥,我这个 @Transactional 不生效啊?AOP 不拦我?"
我一看代码,主类都没实现接口,结果还非手动关掉了 CGLIB。 那一刻我只想说:Spring AOP 的代理机制,你要是没搞明白,功能翻车只是时间问题。
一、故事开场:两个代理的"身份危机"
Spring AOP 本质上就是靠 代理对象(Proxy) 实现的。 代理就像个"假我",负责在目标对象方法前后插入增强逻辑(比如事务、日志、权限校验)。
但 Spring 并不只有一种代理方式,而是:
- JDK 动态代理(基于接口)
- CGLIB 动态代理(基于类继承)
这俩名字看着像兄弟,其实完全不同血统------ 选错了,不仅性能掉帧,还可能导致功能彻底失效。
二、JDK 动态代理:官方自带、接口限定
JDK 动态代理是 Java 原生支持的代理机制。 它的底层原理是 反射 + InvocationHandler。
特点总结👇
特点 | 描述 |
---|---|
实现方式 | 创建目标类的"接口代理对象" |
依赖条件 | 必须有接口(interface) |
底层原理 | Proxy.newProxyInstance() |
性能 | 反射调用,略慢 |
限制 | 只能代理接口方法 |
简单例子:
java
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(p, method, args) -> {
System.out.println("前置逻辑");
Object result = method.invoke(target, args);
System.out.println("后置逻辑");
return result;
}
);
核心特征:
你必须通过接口调用,才能触发代理逻辑。 调用实现类本身 = 直接绕过 AOP。
三、CGLIB 代理:生成子类,无接口也能搞定
当目标类没有实现接口时,Spring 就会使用 CGLIB(Code Generation Library) 。 它通过 继承目标类并重写方法 的方式完成代理。
特点如下👇
特点 | 描述 |
---|---|
实现方式 | 为目标类生成子类 |
依赖条件 | 目标类不能是 final |
底层原理 | 字节码增强(ASM) |
性能 | 创建略慢,调用略快 |
限制 | final 方法无法代理 |
示例:
java
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("前置逻辑");
Object result = proxy.invokeSuper(obj, args);
System.out.println("后置逻辑");
return result;
});
UserService proxy = (UserService) enhancer.create();
CGLIB 是真正意义上的"类代理",不需要接口,也不靠反射。 这也是为什么 Spring Boot 默认优先使用它。
四、Spring 的默认策略:JDK?还是 CGLIB?
很多人以为 Spring 默认用 CGLIB,其实要分版本看👇
版本 | 默认代理机制 | 开启条件 |
---|---|---|
Spring 3.x ~ 4.x | JDK 动态代理 | 只有实现接口时才生效 |
Spring 5.x+(含 Boot) | 自动判断(有接口走 JDK,无接口走 CGLIB) | 可强制配置 |
所以这句代码的意义就关键了:
java
@EnableAspectJAutoProxy(proxyTargetClass = true)
proxyTargetClass = false
(默认) → 优先用 JDK 动态代理proxyTargetClass = true
→ 强制使用 CGLIB
换句话说:
你加了
proxyTargetClass = true
,Spring 就算看到接口也不走 JDK,全都走 CGLIB。
五、性能差异:别迷信 CGLIB "更快"
我们测试一下(以 Spring 6.x 为例):
操作 | JDK Proxy | CGLIB Proxy |
---|---|---|
代理类生成速度 | 快 | 慢(ASM 字节码) |
方法调用性能 | 反射调用稍慢 | 直接调用稍快 |
内存开销 | 小 | 略大(继承链) |
调试难度 | 易懂 | 类层次复杂 |
实际结论:
- 生成阶段:JDK 更快;
- 调用阶段:CGLIB 稍优;
- 差距在 5% 左右,肉眼无感。
所以性能不是选型关键。关键是适配性与代理范围。
六、功能层面:选错代理直接"失效"
❌ 1. 自调用失效(JDK、CGLIB 通病)
AOP 是代理拦截外部调用,如果在同类内部互调方法------绕过代理!
java
public void outer() {
inner(); // 不走代理
}
✅ 解决:从 AopContext.currentProxy()
获取当前代理再调。
❌ 2. final 类或 final 方法无法被 CGLIB 代理
因为 CGLIB 是通过继承实现的,final 就是死路。
✅ 解决:避免 final 修饰目标类或方法。
❌ 3. JDK 代理下直接调用实现类方法无效
比如:
java
((UserServiceImpl) proxy).doSomething(); // 不触发增强
✅ 解决:通过接口类型调用。
七、手动切换:选对代理,功能才能稳
在 Spring Boot 项目中,想控制全局代理策略,只需:
java
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class MyApp { }
或者在配置文件中设置:
yaml
spring:
aop:
proxy-target-class: true # 强制使用 CGLIB
👉 推荐策略:
- 如果项目以接口为主:保持默认(JDK)。
- 如果以类为主(尤其是没有接口的 Service):统一切 CGLIB。
八、面试强化:JDK vs CGLIB 三连问
Q1:Spring 默认使用哪种代理? ✅ 默认使用 JDK 动态代理(目标类有接口时),否则使用 CGLIB。
Q2:如何强制使用 CGLIB? ✅ @EnableAspectJAutoProxy(proxyTargetClass = true)
或配置文件中设置 spring.aop.proxy-target-class=true
。
Q3:两者主要区别? ✅ JDK 基于接口、反射实现;CGLIB 基于继承、字节码生成。 JDK 调用快生成快但受限多,CGLIB 无需接口但无法代理 final。
九、总结:选代理,不是性能问题,而是适配问题
"AOP 是手术刀,代理是手。" 用错手,刀再锋利也切不准。
- 接口类 → JDK Proxy,简单高效;
- 无接口类 → CGLIB,通用灵活;
- 混用项目 → proxyTargetClass=true 一劳永逸;
- 永远别忘:自调用绕过代理、final 无法增强。
一句话收尾:
"懂得代理之别,才算真正入门 Spring AOP 的底层世界。"