← 返回题目列表

Spring AOP 的实现原理是什么?JDK 动态代理和 CGLIB 有什么区别?

高频 中等 第 11 / 30 题 更新于 2026/07/26
Spring AOPJDK 动态代理CGLIB自调用

简化版

Spring AOP 主要通过运行时代理,在目标方法调用前后执行拦截器链;Spring Framework 的基础规则是有接口时可使用 JDK 动态代理,没有接口时使用 CGLIB,也可通过配置强制类代理。代理只能拦截经过代理对象的调用,因此同类 this 自调用会绕过通知,CGLIB 也无法覆盖 final 类或 finalprivate 方法。

详细版

JDK 动态代理基于接口生成代理,代理类型实现目标接口;CGLIB 通过生成目标类子类覆盖可拦截方法,可通过 proxyTargetClass=true 强制使用。两者都是代理模式,真正差异主要是类型边界和可代理方法限制,不应简单断言谁在所有场景下性能更高。

Spring AOP 是基于代理的、方法执行级别的 AOP。外部调用先进入代理,由 Advisor 匹配 Pointcut,再按顺序执行 Advice/Interceptor,最后调用目标对象;对象内部的 this.otherMethod() 已经进入目标对象,不会再次经过代理。

完整版教学

一、一次 AOP 调用发生了什么

容器发现 Bean 匹配某个 Advisor 后,自动代理创建器会为它暴露代理对象。调用方拿到代理并调用方法时,代理大致执行以下流程:

  1. 根据当前方法和目标类型找到匹配的拦截器链;
  2. 依次执行事务、日志、权限等拦截器的前置逻辑;
  3. 调用目标方法;
  4. 按调用栈反向执行返回、异常和清理逻辑。

@Transactional@Cacheable@Async 等常见能力都依赖这种代理拦截,但各自还需要对应功能被启用并由相应基础设施处理。Spring Boot 的 AOP 自动配置通常默认启用 proxyTargetClass,因此 Boot 项目即使实现了接口,也常见 CGLIB 类代理;最终要以项目配置为准。

二、JDK 动态代理与 CGLIB

public interface OrderService {
    void create();
}

@Service
class OrderServiceImpl implements OrderService {
    public void create() {}
}

使用 JDK 代理时,代理保证实现 OrderService,调用方应面向接口注入,不能假设它可以强转为 OrderServiceImpl。CGLIB 代理是目标类的子类,因此可以按具体类类型暴露,但受 Java 继承规则限制。

若目标没有接口,Spring AOP 使用 CGLIB;设置 proxyTargetClass=true 也会统一选择类代理。CGLIB 无法代理 final 类,final 方法不能被覆盖,private 方法对子类不可见,因此这些方法不能依靠子类代理获得通知。不能只看目标类有没有接口就断定运行时代理类型,还要检查 Boot 自动配置和显式 AOP 配置。

三、为什么自调用会失效

@Service
class OrderService {
    public void create() {
        this.save();
    }

    @Transactional
    public void save() {}
}

外部调用 proxy.create() 时只在 create 入口经过代理;目标对象内部执行 this.save() 是普通 Java 调用,不会回到外层代理,所以 save 上单独配置的事务拦截不会触发。

首选修复方式是调整职责,把 save 移到另一个 Bean,由外部 Bean 间调用自然经过代理。自注入代理或 AopContext.currentProxy() 虽能绕开问题,却增加容器耦合,还需要额外配置,不宜作为默认设计。

四、Spring AOP 与 AspectJ 不是一回事

Spring AOP 在容器管理的 Bean 周围创建代理,主要支持方法执行连接点,不会直接改写目标类字节码。AspectJ 可以通过编译期或加载期织入支持更丰富的连接点,也没有同样的代理自调用限制,但配置与构建复杂度更高。

讨论“切面是否生效”时,要先确认使用的是 Spring 代理还是 AspectJ weaving。大多数常规 Spring 应用使用的是前者。

五、常见边界

  • 手动 new 出来的对象不是容器 Bean,通常没有 Spring AOP 代理。
  • 构造器执行发生在代理正常拦截业务方法之前,不能把方法代理当构造器拦截器。
  • 方法是否可见、切点是否匹配、代理类型是否符合注入类型,都会影响最终行为。
  • 一个 Bean 可能同时叠加多个 Advisor,顺序可通过 @OrderOrdered 协调,但顺序值相同不应依赖偶然结果。

排查时先确认注入对象是不是代理,再看调用是否穿过代理、目标方法是否可覆盖、切点表达式是否命中,比只盯着注解更有效。

六、用类型与调用次数推演代理边界

假设代理上依次有权限、事务、日志 3 个拦截器,一次外部调用会先后进入 3 层,再到目标方法,返回时按栈逆序退出;这不是把目标对象复制 3 份,而是一条责任链包围一次方法调用。若目标方法内部连续执行两次 this.save(),这两次都只发生在目标对象内部,不会额外进入 save 对应的代理拦截链。

client
  → permission
    → transaction
      → logging
        → target.method()
      ← logging
    ← transaction
  ← permission
对比维度JDK 动态代理CGLIB 类代理
代理基础目标接口目标类的运行时子类
调用方类型应面向代理接口可按可代理的具体类暴露
关键限制只能代理暴露的接口方法final 类/方法、private 方法不可覆盖
自调用绕过代理同样绕过代理
选择依据核心框架默认规则与配置无接口或显式强制类代理等配置

性能不能只凭“反射慢、子类快”下结论。现代 JVM、拦截器链长度、切点匹配与目标方法成本都会影响结果,代理类型通常应优先按类型契约和可代理性选择,再用基准测试验证热点。

排查心法:先看拿到的是不是代理,再看调用有没有穿过代理,最后看方法能不能被当前代理机制拦截。

七、常见误区与追问

  • 误区:只要类实现接口,运行时就一定是 JDK 代理。 核心框架默认倾向接口代理,但 Spring Boot 或显式配置可能强制类代理,应查看实际配置和代理类型。
  • 误区:CGLIB 能代理类里的所有方法。 final 方法不能覆盖,private 方法对子类不可见,final 类也无法被继承代理。
  • 误区:换成 CGLIB 就能解决 this 自调用。 自调用发生在目标对象内部,两种代理都不会自动重新经过外层代理。
  • 追问:为什么手动 new 的对象没有切面? 它没有经过 Spring 容器的自动代理创建流程,调用方拿到的只是原始对象。
  • 追问:多个切面的顺序怎样控制? 可使用 @OrderOrdered 定义优先级;进入与退出顺序呈调用栈关系,且相同优先级不应依赖偶然顺序。
  • 追问:Spring AOP 和 AspectJ 的核心区别是什么? Spring AOP 主要代理容器 Bean 的方法执行,AspectJ 通过织入支持更丰富连接点,构建和运行配置也更复杂。

八、加强记忆

Spring AOP 的主线是“代理对象 + 方法拦截器链”:JDK 代理面向接口,CGLIB 通过子类覆盖方法。只有从代理入口经过的可拦截方法才有通知,this 自调用、手动 new、final/private 方法都是高频失效边界。