Spring 源码系列(14): JDK 动态代理 vs CGLIB,以及拦截器责任链

📌 阅读前提示 :AOP 阶段的收官篇。前两篇我们走通了「BPP 在初始化后调用 wrapIfNecessary → createProxy」。本篇回答最后两个硬核问题:(1)代理类型怎么选?(2)一次方法调用时,多个通知是怎么「排着队」依次执行的?并彻底讲清「同类自调用 AOP 失效」的底层原因。


一、引子:为什么面试必问 JDK 还是 CGLIB

因为这背后藏着 Spring 对「性能 / 兼容性 / 能力边界」的取舍:

  • JDK 动态代理 :基于接口,生成实现类,要求目标必须实现接口 ;调用快、但不能代理类本身的方法(非接口方法切不到)。
  • CGLIB :基于继承,生成子类重写方法,能代理普通类;无需接口,但不能代理 final 类 / final 方法,且生成字节码略慢。

Spring 的 DefaultAopProxyFactory 就是在这两者间做裁决。


二、源码追踪(一):代理类型裁决逻辑

java 复制代码
// DefaultAopProxyFactory.java
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    // optimize:激进优化;proxyTargetClass:强制 CGLIB;isProxyTargetClass
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) throw new AopConfigException("TargetSource 无法确定目标类");
        // 目标类是接口,或已经是 JDK 代理类 → 只能用 JDK 动态代理
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        // 其余 → CGLIB
        return new ObjenesisCglibAopProxy(config);
    } else {
        // 没强制 CGLIB,且有用户提供的接口 → JDK 动态代理
        return new JdkDynamicAopProxy(config);
    }
}

2.1 裁决规则一览表

条件 结果
proxyTargetClass=true(或 Boot 默认) CGLIB
optimize=true CGLIB
目标类无接口、且非强制 JDK CGLIB
目标类实现了接口、且 proxyTargetClass=false JDK 动态代理
目标类是接口本身 / 已是 JDK 代理 强制 JDK 动态代理

⚠️ 易错点 :「有接口就一定用 JDK 代理」------ 。只要 proxyTargetClass=true(Boot 默认就是),即使有接口也走 CGLIB。规则优先级是:强制 CGLIB 配置 > 接口存在


三、源码追踪(二):调用时的拦截器责任链

无论哪种代理,最终调用都会进入「责任链」。以 JDK 代理为例:

3.1 JdkDynamicAopProxy.invoke 织入责任链

java 复制代码
// JdkDynamicAopProxy.java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // ① 取出该方法的拦截器链(已按 @Order 排序)
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
    if (chain.isEmpty()) {
        // ② 无通知 → 直接反射调用目标方法,不走代理逻辑
        return AopUtils.invokeJoinpointUsingReflection(target, method, args);
    }
    // ③ 包装成 MethodInvocation,递归推进责任链
    MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
    return invocation.proceed();   // ← 责任链启动
}

3.2 ReflectiveMethodInvocation.proceed:递归式「逐个执行」

java 复制代码
// ReflectiveMethodInvocation.java
@Override
public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        // 链走完 → 调用真实目标方法
        return invokeJoinpoint();
    }
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    if (interceptorOrInterceptionAdvice instanceof MethodInterceptor) {
        MethodInterceptor mi = (MethodInterceptor) interceptorOrInterceptionAdvice;
        return mi.invoke(this);  // ← 每个拦截器处理完后,再调 proceed() 推进下一个
    }
    // 动态匹配拦截器
    return proceed();
}

📌 责任链本质 :这是经典的「递归 + 索引推进」。@Around 对应的 MethodInterceptor.invoke(this) 内部会调用 invocation.proceed() 进入下一个拦截器;当所有拦截器走完,才执行真实目标方法。出栈顺序天然形成「环绕」效果。

3.3 通知执行顺序(单切面前)

通知 在链路中的位置 执行特点
@Around(proceed 之前) 最外 最先执行,包住一切
@Before 靠前 proceed 前打印/校验
目标方法 链底 invokeJoinpoint()
@AfterReturning 返回后 拿到返回值
@After finally 必执行
@AfterThrowing 异常分支 仅异常时
@Around(proceed 之后) 最外返回 最后收尾

四、终极问题:同类自调用为什么 AOP 失效

java 复制代码
@Service
public class OrderService {
    public void create() {
        this.validate();   // ← this 指向【原始对象】,不是代理!
    }
    @Transactional
    public void validate() { /* ... */ }
}

原因create() 是由代理对象 调入的,但方法体内的 this原始目标对象 (Spring 注入的是原始 Bean,代理只是外层壳)。this.validate() 绕过了代理,责任链根本没机会执行,@Transactional / 切面全部失效。

两种解法

java 复制代码
// 解法一:exposeProxy=true 后从 AopContext 取代理再调
@EnableAspectJAutoProxy(exposeProxy = true)
public class AopConfig {}

public void create() {
    ((OrderService) AopContext.currentProxy()).validate(); // 走代理,AOP 生效
}

// 解法二:拆到另一个 Bean(推荐,解耦更清晰)
@Autowired OrderValidator validator;
public void create() { validator.validate(); } // 跨 Bean 调用,代理正常介入

💡 结论 :自调用失效的根因是「代理是包装,不是替换」------this 永远是原始对象。理解这点,所有「AOP 没生效」的诡异问题都能定位。


五、常见误区

误区 正解
有接口就一定 JDK 代理 错,proxyTargetClass=true 时强制 CGLIB
CGLIB 不能代理任何类 只能代理非 final 类、非 final 方法
@Around 不调 proceed 也能返回 不调 proceed 目标不执行,需自己定返回值
自调用时 AOP 也生效 失效,因 this 指向原始对象(见第四节)
无匹配通知的 Bean 走代理也慢 空链直接反射调目标,几乎无开销

🧪 面试题自测

  1. DefaultAopProxyFactory 选 JDK 还是 CGLIB 的完整规则?
  2. 为什么 proxyTargetClass=true 时即使有接口也走 CGLIB?
  3. ReflectiveMethodInvocation.proceed() 的递归推进原理?
  4. 同类自调用 AOP 失效的根因?两种解法?
  5. CGLIB 代理有什么限制(final / 构造器等)?
  6. 为什么 @Around 能完全控制目标方法是否执行?

🔧 Debug 小技巧

JdkDynamicAopProxy.invoke 断点,观察 chain 列表的元素顺序(对应通知执行顺序);再单步进入 ReflectiveMethodInvocation.proceed,用「Step Over 走到 mi.invoke(this)、再 Step Into」的方式,逐拦截器感受责任链的递归推进。最后在 create() 里加 this == AopContext.currentProxy() 的条件断点,亲眼看到「自调用时两边不等」。


下一篇预告

AOP 阶段完结。下一篇进入 Spring MVC 阶段(第 15 篇)DispatcherServlet 如何初始化------onRefresh → initStrategies 九大组件,以及一次 HTTP 请求在 Spring 里的「总调度」是如何搭起来的。


如果这篇对你有帮助,欢迎 点赞 · 收藏 · 关注 三连支持。

Spring 源码系列共 30 篇,由浅入深持续更新中。有疑问或想深挖的源码点,评论区告诉我,下篇见。

相关推荐
mifengxing1 小时前
计算机组成原理——存储器系统
开发语言·考研·计算机组成原理·复习笔记·计算机408
流云鹤1 小时前
05Java学习day(5)
java·学习
用户3126874877201 小时前
Spring 事件机制到底怎么传播的?ApplicationEvent 源码拆解
java·spring
2501_933923252 小时前
设计网页的时候加载不出来怎么办?认识常见的状态码
前端·spring
默辨2 小时前
Spring AI Alibaba 核心知识点
java·ai·spring ai·spring alibaba
乐观的Terry2 小时前
10、发布系统-路由管理与灰度发布
java
唐青枫2 小时前
Java WebLogic 实战指南:从 Domain、数据源到 WAR 部署和集群管理
java
Yolanda_20222 小时前
Python学习-第九部分-错误处理与异常处理
开发语言·python·学习
郝学胜-神的一滴2 小时前
Qt 高级编程 037:QSS按钮样式封神指南
开发语言·c++·qt·软件工程·用户界面