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

247 阅读4分钟

原文来自于: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()
性能反射调用,略慢
限制只能代理接口方法

简单例子:

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 方法无法代理

示例:

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.xJDK 动态代理只有实现接口时才生效
Spring 5.x+(含 Boot)自动判断(有接口走 JDK,无接口走 CGLIB)可强制配置

所以这句代码的意义就关键了:

@EnableAspectJAutoProxy(proxyTargetClass = true)
  • proxyTargetClass = false(默认) → 优先用 JDK 动态代理
  • proxyTargetClass = true → 强制使用 CGLIB

换句话说:

你加了 proxyTargetClass = true,Spring 就算看到接口也不走 JDK,全都走 CGLIB。


五、性能差异:别迷信 CGLIB “更快”

我们测试一下(以 Spring 6.x 为例):

操作JDK ProxyCGLIB Proxy
代理类生成速度慢(ASM 字节码)
方法调用性能反射调用稍慢直接调用稍快
内存开销略大(继承链)
调试难度易懂类层次复杂

实际结论:

  • 生成阶段:JDK 更快;
  • 调用阶段:CGLIB 稍优;
  • 差距在 5% 左右,肉眼无感。

所以性能不是选型关键。关键是适配性与代理范围。


六、功能层面:选错代理直接“失效”

❌ 1. 自调用失效(JDK、CGLIB 通病)

AOP 是代理拦截外部调用,如果在同类内部互调方法——绕过代理!

public void outer() {
    inner(); // 不走代理
}

✅ 解决:从 AopContext.currentProxy() 获取当前代理再调。


❌ 2. final 类或 final 方法无法被 CGLIB 代理

因为 CGLIB 是通过继承实现的,final 就是死路。

✅ 解决:避免 final 修饰目标类或方法。


❌ 3. JDK 代理下直接调用实现类方法无效

比如:

((UserServiceImpl) proxy).doSomething(); // 不触发增强

✅ 解决:通过接口类型调用。


七、手动切换:选对代理,功能才能稳

在 Spring Boot 项目中,想控制全局代理策略,只需:

@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class MyApp { }

或者在配置文件中设置:

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 的底层世界。”