代理选错,性能和功能全翻车!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 的底层世界。"

相关推荐
三小河22 分钟前
Agent Skill与Rules的区别——以Cursor为例
前端·javascript·后端
Hilaku29 分钟前
不要在简历上写精通 Vue3?来自面试官的真实劝退
前端·javascript·vue.js
三小河35 分钟前
前端视角详解 Agent Skill
前端·javascript·后端
牛奔44 分钟前
Go 是如何做抢占式调度的?
开发语言·后端·golang
颜酱1 小时前
二叉树遍历思维实战
javascript·后端·算法
鹏多多1 小时前
移动端H5项目,还需要react-fastclick解决300ms点击延迟吗?
前端·javascript·react.js
不想秃头的程序员1 小时前
Vue3 封装 Axios 实战:从基础到生产级,新手也能秒上手
前端·javascript·面试
爱装代码的小瓶子1 小时前
【C++与Linux基础】进程间通讯方式:匿名管道
android·c++·后端
你听得到111 小时前
我彻底搞懂了 SSE,原来流式响应效果还能这么玩的?(附 JS/Dart 双端实战)
前端·面试·github
奔跑的web.1 小时前
UniApp 路由导航守
前端·javascript·uni-app