代理选错,性能和功能全翻车!Spring AOP 的默认技术别再搞混

原文来自于: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 的底层世界。"

相关推荐
间彧3 小时前
Java泛型详解与项目实战
后端
WeilinerL3 小时前
泛前端代码覆盖率探索之路
前端·javascript·测试
间彧3 小时前
PECS原则在Java集合框架中的具体实现有哪些?举例说明
后端
间彧3 小时前
Java 泛型擦除详解和项目实战
后端
-睡到自然醒~4 小时前
[go 面试] 前端请求到后端API的中间件流程解析
前端·中间件·面试
间彧4 小时前
在自定义泛型类时,如何正确应用PECS原则来设计API?
后端
间彧4 小时前
能否详细解释PECS原则及其在项目中的实际应用场景?
后端
JNU freshman4 小时前
vue 之 import 的语法
前端·javascript·vue.js
武子康4 小时前
大数据-132 Flink SQL 实战入门 | 3 分钟跑通 Table API + SQL 含 toChangelogStream 新写法
大数据·后端·flink