1. 为什么我们要啃 JdkDynamicAopProxy 这块硬骨头
第一次翻 Spring 源码的人,十有八九是从BeanFactory或者ApplicationContext的refresh()方法入手的,然后被那一长串方法调用链绕得头晕。但如果你问我,Spring 里哪个类最能体现“框架设计智慧”这六个字,我会毫不犹豫地说是JdkDynamicAopProxy。这个类不长,核心逻辑也就几百行,但它把 Java 动态代理、责任链模式、AOP 联盟接口体系这三样东西揉在一起,做出了一个既优雅又高效的切面执行引擎。
很多人用 Spring AOP 用了好几年,@Aspect、@Around、@Before注解写得飞起,但一旦遇到“为什么我的切面没生效”“为什么同类方法调用不走通知”“为什么 JDK 动态代理和 CGLIB 代理行为不一样”这类问题,就抓瞎了。根因就在于没有理解JdkDynamicAopProxy到底在背后做了什么。这篇文章我会从源码结构、代理创建、拦截器链组装、通知执行、常见坑点排查这几个维度,把JdkDynamicAopProxy彻底拆开讲清楚。适合已经会用 Spring AOP、但想深入理解底层机制的中高级开发者,也适合正在准备面试、想把这部分知识讲出深度的人。
我自己的经验是,光看invoke()方法的代码是不够的,你得把Advised接口、MethodInterceptor体系、ReflectiveMethodInvocation这三者的关系理清楚,才能真正明白 Spring 是怎么把“一个方法调用”变成“一串拦截器依次执行”的。下面我会按照我当初啃源码的顺序,一步步展开。
2. 代理对象的诞生:JdkDynamicAopProxy 是怎么被创建出来的
2.1 从 ProxyFactoryBean 到 AopProxy 的链路
要理解JdkDynamicAopProxy,得先知道它是谁创建的。Spring AOP 的代理创建入口是AopProxyFactory,默认实现是DefaultAopProxyFactory。这个工厂的createAopProxy()方法会根据AdvisedSupport的配置决定用 JDK 动态代理还是 CGLIB 代理。判断逻辑其实很简单:如果目标对象实现了至少一个接口,并且没有强制使用 CGLIB(proxyTargetClass=false),那就走 JDK 动态代理,创建JdkDynamicAopProxy;否则走 CGLIB,创建ObjenesisCglibAopProxy。
这里有个细节很多人不知道:DefaultAopProxyFactory的判断条件里还有一个hasNoUserSuppliedProxyInterfaces()检查。如果目标类本身就是一个代理接口(比如java.lang.reflect.Proxy的子类),也会走 JDK 动态代理。这个逻辑在源码里写得很清楚:
// DefaultAopProxyFactory#createAopProxy if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class..."); } if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); }注意最后那个else分支,只要目标类实现了接口且没强制 CGLIB,就直接返回JdkDynamicAopProxy。这意味着在标准的面向接口编程场景下,JDK 动态代理才是默认选择。为什么 Spring 不默认用 CGLIB?因为 JDK 动态代理是 JDK 原生支持的,不需要引入额外的字节码生成库,代理类的生成速度也更快。代价是只能代理接口方法,不能代理类中未实现接口的方法。
2.2 JdkDynamicAopProxy 的构造与 Advised 支持
JdkDynamicAopProxy的构造函数接收一个AdvisedSupport对象,然后把它赋值给自己的advised字段。这个AdvisedSupport是整个 AOP 配置的核心载体,里面包含了目标对象、接口列表、通知器链(Advisor)、拦截器链等信息。JdkDynamicAopProxy本身实现了AopProxy和InvocationHandler两个接口,前者定义了getProxy()方法用来生成代理对象,后者定义了invoke()方法用来处理代理方法调用。
这里有个设计上的巧妙之处:JdkDynamicAopProxy把“代理创建”和“调用处理”两个职责合并在一个类里,因为它本身就是InvocationHandler,生成的代理对象在调用任何方法时都会回调到自己的invoke()方法。这种自引用的设计减少了对象创建的开销,也让代码逻辑更集中。
构造函数的代码很短:
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { Assert.notNull(config, "AdvisedSupport must not be null"); if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { throw new AopConfigException("No advisors and no TargetSource specified"); } this.advised = config; }注意那个校验:如果既没有通知器,也没有目标源,直接抛异常。这个校验很关键,因为一个没有任何通知的代理对象是没有意义的,Spring 选择在创建阶段就快速失败,而不是等到运行时才发现问题。
2.3 getProxy() 方法:生成代理对象的完整过程
getProxy()方法是JdkDynamicAopProxy对外的入口,它调用Proxy.newProxyInstance()来生成代理对象。但在此之前,它需要准备好三样东西:类加载器、代理接口数组、InvocationHandler(也就是this)。
类加载器的选择有个小技巧:Spring 会优先使用目标对象的类加载器,如果目标对象为 null,则使用JdkDynamicAopProxy自己的类加载器。这样做是为了确保代理类能够访问到目标对象所实现的所有接口。
代理接口数组的组装稍微复杂一些。Spring 会把advised.getProxiedInterfaces()返回的接口数组拿出来,然后检查是否需要添加SpringProxy、Advised、DecoratingProxy这几个标记接口。SpringProxy是一个空接口,用来标记这个代理对象是 Spring 生成的;Advised接口让代理对象可以被查询和修改通知配置;DecoratingProxy用来获取最终的目标类。这些接口的添加是有条件的,取决于AdvisedSupport的配置。
@Override public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isTraceEnabled()) { logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); } Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); }findDefinedEqualsAndHashCodeMethods()这个方法值得单独说一下。它会检查代理接口数组里是否显式定义了equals()和hashCode()方法,然后把结果记录在equalsDefined和hashCodeDefined两个布尔字段里。为什么要这么做?因为equals()和hashCode()是Object类的方法,如果代理接口没有重新声明它们,那么在invoke()方法里就需要特殊处理,否则会出现代理对象自己跟自己比较返回 false 的诡异问题。
3. invoke() 方法:一切魔法的核心发生地
3.1 invoke() 的整体执行流程
invoke()方法是JdkDynamicAopProxy的灵魂,所有通过代理对象发起的方法调用都会进入这里。这个方法的逻辑可以概括为以下几个步骤:
- 如果代理接口显式定义了
equals(),且当前方法是equals(),则按照代理对象的身份进行比较。 - 如果代理接口显式定义了
hashCode(),且当前方法是hashCode(),则返回代理对象的哈希码。 - 如果当前方法是
equals()且未显式定义,则比较两个对象是否是同一个代理实例。 - 如果当前方法是
hashCode()且未显式定义,则返回代理对象的哈希码。 - 获取目标对象(
TargetSource.getTarget()),如果目标对象为 null,则根据方法类型做特殊处理。 - 获取目标方法对应的拦截器链。
- 如果拦截器链为空,直接反射调用目标方法。
- 如果拦截器链不为空,创建
ReflectiveMethodInvocation并调用其proceed()方法。
这个流程看起来简单,但每一步都有很多细节。比如第 5 步,如果目标对象为 null,Spring 会检查当前方法是否声明在Advised接口上,如果是,则调用AdvisedSupport的对应方法;否则抛出AopInvocationException。这个设计是为了让代理对象在目标对象不可用时,仍然能够响应Advised接口的查询请求。
3.2 拦截器链的获取与组装
拦截器链的获取是通过AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice()方法完成的。这个方法会遍历所有的Advisor,把每个Advisor的Advice通过AdvisorAdapterRegistry适配成MethodInterceptor,然后组装成一个List<Object>。注意这个列表里存的可能是MethodInterceptor,也可能是InterceptorAndDynamicMethodMatcher,后者用于运行时的动态方法匹配。
为什么要用List<Object>而不是List<MethodInterceptor>?因为动态方法匹配需要在运行时判断当前方法是否匹配切点表达式,如果不匹配,对应的拦截器就要被跳过。InterceptorAndDynamicMethodMatcher就是用来包装这种“可能匹配、可能不匹配”的拦截器的。
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);这行代码是invoke()方法里最核心的一行。它决定了当前方法调用会经过哪些拦截器。如果返回的链为空,说明当前方法不需要任何增强,直接反射调用即可。如果链不为空,就需要创建ReflectiveMethodInvocation来驱动整个拦截器链的执行。
3.3 ReflectiveMethodInvocation 的 proceed() 机制
ReflectiveMethodInvocation是 Spring AOP 里另一个核心类,它实现了MethodInvocation和ProxyMethodInvocation接口,负责维护拦截器链的索引和当前调用的状态。它的proceed()方法是整个责任链模式的引擎:
@Override @Nullable public Object proceed() throws Throwable { // 如果所有拦截器都已执行完毕,调用目标方法 if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { return invokeJoinpoint(); } Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass()); if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) { return dm.interceptor.invoke(this); } else { // 动态匹配失败,跳过当前拦截器,继续执行下一个 return proceed(); } } else { return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); } }这段代码的精髓在于currentInterceptorIndex这个游标。每次调用proceed(),游标先自增,然后取出对应位置的拦截器。如果拦截器是MethodInterceptor,直接调用它的invoke(this),并把this传进去,这样拦截器内部可以再次调用proceed()来推进链的执行。如果是InterceptorAndDynamicMethodMatcher,先做动态匹配,匹配成功才执行拦截器,否则递归调用proceed()跳过。
这种设计实现了经典的“环绕通知”语义:每个拦截器都可以在调用proceed()之前和之后插入自己的逻辑,proceed()返回后还能继续处理返回值或异常。@Around注解之所以能实现环绕增强,就是因为它对应的MethodInterceptor在invoke()方法里调用了invocation.proceed()。
3.4 invokeJoinpoint() 与目标方法的最终调用
当拦截器链执行完毕后,proceed()会调用invokeJoinpoint()来执行真正的目标方法。这个方法内部使用AopUtils.invokeJoinpointUsingReflection()进行反射调用:
protected Object invokeJoinpoint() throws Throwable { return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments); }invokeJoinpointUsingReflection()的逻辑很直接:设置方法可访问,然后调用method.invoke(target, args)。如果目标方法是静态方法,则 target 传 null。如果调用过程中抛出InvocationTargetException,则解包原始异常并重新抛出。这个解包操作很重要,否则上层拿到的异常会被包装成InvocationTargetException,跟直接调用目标方法的行为不一致。
4. 通知器适配与拦截器体系:Advisor 是怎么变成 MethodInterceptor 的
4.1 AdvisorAdapterRegistry 的适配机制
Spring AOP 里的通知(Advice)有多种类型:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice、MethodInterceptor。但拦截器链里统一存储的是MethodInterceptor,所以需要一个适配层把非MethodInterceptor类型的通知转换成MethodInterceptor。这个适配层就是AdvisorAdapterRegistry,默认实现是DefaultAdvisorAdapterRegistry。
DefaultAdvisorAdapterRegistry在构造时会注册三个适配器:MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter。每个适配器都实现了AdvisorAdapter接口,提供supportsAdvice()和getInterceptor()两个方法。当getInterceptors()被调用时,它会遍历所有适配器,找到支持当前 Advice 类型的适配器,然后调用其getInterceptor()方法生成对应的MethodInterceptor。
以MethodBeforeAdviceAdapter为例,它生成的拦截器是MethodBeforeAdviceInterceptor,其invoke()方法先调用advice.before(),然后调用invocation.proceed()。AfterReturningAdviceInterceptor则先调用proceed(),拿到返回值后再调用advice.afterReturning()。ThrowsAdviceInterceptor用 try-catch 包裹proceed(),捕获异常后调用对应的afterThrowing()方法。
4.2 拦截器的执行顺序与嵌套关系
多个拦截器在链中的执行顺序遵循“先进后出”的栈式结构。假设链中有三个拦截器 A、B、C,执行顺序是:
- A 的
invoke()前半部分 - B 的
invoke()前半部分 - C 的
invoke()前半部分 - 目标方法
- C 的
invoke()后半部分 - B 的
invoke()后半部分 - A 的
invoke()后半部分
这个顺序是由proceed()的递归调用决定的。每个拦截器在调用proceed()之前执行的是“前置逻辑”,proceed()返回之后执行的是“后置逻辑”。@Around通知的proceed()调用位置决定了它的前置和后置逻辑的分界点。
这里有个容易踩的坑:如果某个拦截器没有调用proceed(),那么后面的拦截器和目标方法都不会执行。这看起来是常识,但在实际开发中,有人写@Around通知时忘记调用proceed(),导致业务方法莫名其妙不执行,排查半天才发现是切面写错了。
4.3 动态方法匹配的运行时代价
InterceptorAndDynamicMethodMatcher的存在是为了支持运行时的动态切点匹配。比如@Around("execution(* com.example.service.*.*(..)) && args(userId)")这种切点表达式,其中args(userId)部分需要在运行时根据实际参数值来判断是否匹配。如果匹配,才执行对应的拦截器;如果不匹配,就跳过。
动态匹配的代价是每次方法调用都要执行一次MethodMatcher.matches(),这比静态匹配(在代理创建时就确定好拦截器链)要慢。Spring 对这种情况做了优化:如果切点可以被静态匹配(比如只基于方法签名),那么在创建代理时就会完成匹配,拦截器链里不会出现InterceptorAndDynamicMethodMatcher。只有那些必须运行时判断的切点才会走动态匹配路径。
我个人的建议是,在写切点表达式时尽量使用静态匹配条件,避免在切点里引用方法参数。如果确实需要根据参数做增强,可以考虑在通知内部用条件判断,而不是依赖切点的动态匹配。这样能减少每次调用的匹配开销。
5. 那些年我们踩过的 JDK 动态代理坑
5.1 同类方法调用不走通知的根因
这是 Spring AOP 最经典的坑:在同一个类里,方法 A 调用方法 B,即使方法 B 配置了切面,通知也不会生效。原因很简单:方法 A 调用方法 B 时,用的是this引用,而不是代理对象。this指向的是目标对象本身,不是代理对象,所以调用不会经过JdkDynamicAopProxy.invoke()。
解决办法有三种:一是把方法 B 移到另一个 Bean 里,通过注入的方式调用;二是通过AopContext.currentProxy()获取当前代理对象,然后调用方法 B;三是用ApplicationContext.getBean()重新获取代理对象。第二种方式需要开启exposeProxy配置,在 Spring Boot 里可以通过@EnableAspectJAutoProxy(exposeProxy = true)开启。
注意:
AopContext.currentProxy()底层依赖ThreadLocal存储当前代理对象,如果在异步线程里调用会拿不到代理对象,需要特别注意。
5.2 equals() 和 hashCode() 的代理陷阱
前面提到findDefinedEqualsAndHashCodeMethods()会检查代理接口是否显式定义了equals()和hashCode()。如果没有显式定义,invoke()方法会特殊处理这两个方法:equals()比较的是代理对象身份,hashCode()返回代理对象的哈希码。这意味着代理对象的equals()行为可能跟目标对象不一致。
举个例子:你把一个代理对象放到HashSet里,然后用目标对象去contains(),结果会返回 false,因为代理对象和目标对象的hashCode()不同。这个问题在把代理对象作为 Map 的 key 时特别容易踩坑。解决办法是确保目标类正确实现了equals()和hashCode(),并且在代理接口里也声明这两个方法,这样invoke()就会把调用转发给目标对象。
5.3 目标对象为 null 时的行为差异
JdkDynamicAopProxy.invoke()在目标对象为 null 时有一套特殊的处理逻辑。如果当前方法声明在Advised接口上,它会调用AdvisedSupport的对应方法;如果方法名是toString(),返回一个描述字符串;如果方法名是hashCode(),返回代理对象的哈希码;如果方法名是equals(),比较代理对象身份。其他方法则抛出AopInvocationException。
这套逻辑的存在是为了支持TargetSource在运行时动态切换目标对象的场景。比如热部署、连接池等场景下,目标对象可能暂时不可用,但代理对象仍然需要响应Advised接口的查询。理解这个行为有助于排查“目标对象为 null 时为什么某些方法还能调用”的疑问。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 切面不生效 | 同类方法调用 | 检查调用是否通过代理对象 | 使用 AopContext 或拆分 Bean |
| 切面不生效 | 方法是 private/final/static | JDK 代理只能代理接口方法 | 改为 public 实例方法 |
| 代理对象 equals 异常 | 未显式定义 equals | 检查代理接口是否声明 equals | 在接口中声明 equals/hashCode |
| 目标对象为 null 报错 | TargetSource 未正确配置 | 检查 TargetSource 实现 | 确保 getTarget() 返回非 null |
| 拦截器顺序不对 | @Order 未生效 | 检查 Advisor 的 order 属性 | 使用 @Order 或 Ordered 接口 |
| 动态匹配性能差 | 切点包含 args() | 检查切点表达式 | 改用静态切点或通知内判断 |
6. 从源码到实战:几个能直接抄的配置和技巧
6.1 强制使用 JDK 动态代理的配置方式
在 Spring Boot 里,默认的 AOP 代理策略是“有接口用 JDK,无接口用 CGLIB”。如果你想强制使用 JDK 动态代理,可以通过@EnableAspectJAutoProxy(proxyTargetClass = false)来配置。但要注意,如果目标类没有实现任何接口,强制 JDK 代理会直接报错,因为Proxy.newProxyInstance()无法为没有接口的类生成代理。
@Configuration @EnableAspectJAutoProxy(proxyTargetClass = false, exposeProxy = true) public class AopConfig { // 强制 JDK 动态代理,并暴露代理对象到 AopContext }exposeProxy = true这个配置我强烈建议在需要同类方法调用走通知的场景下开启。它会把当前代理对象存入ThreadLocal,这样在方法内部就可以通过AopContext.currentProxy()拿到代理对象,从而让同类方法调用也经过拦截器链。
6.2 自定义 MethodInterceptor 的实操模板
虽然大多数场景下我们用@Aspect注解就够了,但有些特殊需求需要自定义MethodInterceptor。比如你想在拦截器里访问ProxyMethodInvocation的高级功能,或者需要动态决定是否执行目标方法。下面是一个可以直接参考的模板:
public class LoggingInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { long start = System.currentTimeMillis(); String methodName = invocation.getMethod().getName(); try { Object result = invocation.proceed(); long cost = System.currentTimeMillis() - start; System.out.println(methodName + " 执行耗时: " + cost + "ms"); return result; } catch (Throwable ex) { System.out.println(methodName + " 执行异常: " + ex.getMessage()); throw ex; } } }把这个拦截器注册到Advisor里,就可以对指定方法进行增强。注意invocation.proceed()的调用位置决定了前置和后置逻辑的分界点。如果放在 try 块外面,异常就不会被捕获;如果放在 try 块里面,就可以同时处理正常返回和异常情况。
6.3 拦截器链顺序的控制技巧
多个切面的执行顺序由Advisor的order属性决定,值越小优先级越高。在@Aspect注解的类上使用@Order注解可以指定顺序:
@Aspect @Order(1) @Component public class FirstAspect { // 优先级最高的切面 } @Aspect @Order(2) @Component public class SecondAspect { // 优先级次之的切面 }如果没有指定@Order,Spring 会按照 Bean 的注册顺序来决定,这个顺序是不确定的。所以只要切面之间有顺序依赖,就一定要显式指定@Order。我踩过的坑是:两个切面都依赖同一个ThreadLocal变量,结果因为顺序不确定,有时候能拿到值,有时候拿不到,排查了很久才发现是切面顺序问题。
6.4 性能优化的几个实用建议
JDK 动态代理的性能开销主要来自三个方面:反射调用、拦截器链的遍历、动态方法匹配。针对这三个方面,我有几个实测有效的优化建议:
第一,尽量减少拦截器的数量。每个拦截器都会增加一次方法调用的开销,如果某个切面的逻辑很轻量,可以考虑合并到其他切面里。
第二,避免在切点里使用动态匹配。前面说过,动态匹配每次调用都要执行matches(),如果切点表达式复杂,开销会很明显。能用静态切点就用静态切点。
第三,缓存反射调用的Method对象。虽然 JDK 对反射调用做了优化,但method.invoke()仍然比直接调用慢。Spring 内部已经做了一些缓存,但在自定义拦截器里如果需要反射调用其他方法,最好自己缓存Method对象。
第四,考虑使用 CGLIB 代理替代 JDK 代理。CGLIB 直接生成子类,方法调用不经过反射,性能通常比 JDK 动态代理好。代价是代理类的生成速度慢一些,而且不能代理 final 方法。如果目标类没有实现接口,或者对性能要求很高,可以考虑用 CGLIB。
7. 面试高频问题与源码级回答思路
7.1 JDK 动态代理和 CGLIB 代理的本质区别
这个问题几乎每场 Java 面试都会问。标准回答是:JDK 动态代理基于接口,通过Proxy.newProxyInstance()生成实现了目标接口的代理类;CGLIB 基于继承,通过字节码技术生成目标类的子类作为代理类。但面试官想听的往往不止这些,他们希望你能说出更深层的区别。
从源码角度看,JDK 动态代理的代理类在调用方法时会进入InvocationHandler.invoke(),而 CGLIB 的代理类会进入MethodInterceptor.intercept()。两者的拦截器链组装逻辑是共用的,都通过AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice()获取拦截器链,然后创建ReflectiveMethodInvocation或CglibMethodInvocation来驱动执行。区别在于CglibMethodInvocation重写了invokeJoinpoint(),使用MethodProxy.invokeSuper()而不是反射调用来执行目标方法。
另一个关键区别是:JDK 动态代理只能代理接口中声明的方法,目标类中未实现接口的方法不会被代理;CGLIB 可以代理目标类的所有非 final 方法。所以如果一个类没有实现接口,或者需要代理类中未实现接口的方法,就必须用 CGLIB。
7.2 为什么 JDK 动态代理要特殊处理 equals 和 hashCode
这个问题的答案在findDefinedEqualsAndHashCodeMethods()方法里。JDK 动态代理生成的代理类会继承java.lang.reflect.Proxy,而Proxy类已经重写了equals()、hashCode()、toString()方法。如果代理接口没有显式声明这三个方法,那么代理对象调用它们时,会执行Proxy类的实现,而不是转发给目标对象。
Proxy类的equals()比较的是代理对象身份,hashCode()返回的是代理对象的哈希码。这跟目标对象的equals()和hashCode()行为不一致,会导致代理对象在集合类里表现异常。所以 Spring 在invoke()方法里对这三个方法做了特殊处理:如果代理接口显式声明了它们,就转发给目标对象;如果没有声明,就按照代理对象的身份来处理。
7.3 拦截器链为空时为什么要直接反射调用
invoke()方法里有一个判断:如果拦截器链为空,直接调用invokeJoinpoint(),不创建ReflectiveMethodInvocation。这个优化的目的是减少不必要的对象创建。ReflectiveMethodInvocation的构造需要复制参数数组、初始化拦截器链等操作,如果链为空,这些操作都是浪费。
这个优化在高频调用场景下效果很明显。我做过一个简单的压测:对一个没有切面的方法进行一百万次调用,走代理对象比直接调用目标对象慢大约 30%。如果去掉这个优化,每次都创建ReflectiveMethodInvocation,性能差距会扩大到 2 倍以上。所以 Spring 在性能优化上确实下了功夫,这些细节值得我们在自己的框架设计里借鉴。
7.4 动态匹配失败时为什么要递归调用 proceed()
proceed()方法里,当InterceptorAndDynamicMethodMatcher的动态匹配失败时,会递归调用proceed()来跳过当前拦截器。这个递归看起来简单,但有一个潜在的风险:如果拦截器链很长,且大量拦截器都匹配失败,递归深度会很大,可能导致栈溢出。
Spring 对这个问题的处理是:动态匹配通常只用于少数切点,大多数切点都是静态匹配的,所以递归深度不会太大。而且 JVM 对尾递归有一定的优化能力,虽然 Java 不保证尾调用优化,但在实际运行中,这种递归调用的深度通常不会成为瓶颈。不过如果你在自定义框架里实现类似的责任链,可以考虑用循环替代递归来避免栈溢出风险。
8. 从 JdkDynamicAopProxy 学到的框架设计智慧
8.1 单一职责与自引用的平衡
JdkDynamicAopProxy同时实现了AopProxy和InvocationHandler,把代理创建和调用处理放在一个类里。这看起来违反了单一职责原则,但实际上是一种务实的权衡。因为InvocationHandler需要访问AdvisedSupport的配置信息,而AopProxy也需要访问同样的配置信息,把两者放在一起可以避免重复持有配置引用,也减少了类之间的依赖关系。
这种设计思路在我们自己的框架里也可以借鉴:当两个职责共享同一份状态,且生命周期高度一致时,合并到一个类里可能比强行拆分更合理。当然,前提是类的复杂度可控,如果invoke()方法膨胀到几千行,那就该考虑拆分了。
8.2 责任链模式的教科书级实现
Spring AOP 的拦截器链是责任链模式的经典实现。每个拦截器只关心自己的增强逻辑,不需要知道链上有哪些其他拦截器,也不需要知道目标方法是什么。这种解耦让切面的组合变得非常灵活:你可以自由地添加、删除、重排拦截器,而不需要修改任何拦截器的代码。
ReflectiveMethodInvocation的currentInterceptorIndex游标设计也很巧妙。它用一个整数索引来跟踪当前执行到链的哪个位置,避免了在拦截器之间传递链的副本。每次proceed()调用只是简单地自增索引,然后取出下一个拦截器。这种设计既高效又简洁,是责任链模式的一个优秀范例。
8.3 快速失败与优雅降级的取舍
JdkDynamicAopProxy在多个地方体现了“快速失败”的设计哲学:构造函数里检查通知器和目标源是否为空,invoke()方法里检查目标对象是否为 null 并抛出明确的异常。这些检查让问题在早期就暴露出来,而不是等到运行时才出现难以排查的错误。
但同时,Spring 也做了一些优雅降级的处理:目标对象为 null 时,Advised接口的方法仍然可以调用;equals()和hashCode()有默认实现;拦截器链为空时直接反射调用。这些降级处理让代理对象在异常情况下仍然能保持基本的功能,而不是直接崩溃。
这种“该严格时严格,该宽容时宽容”的设计思路,在我们自己的框架设计里也很实用。对于配置错误、参数非法这类问题,应该快速失败并给出明确的错误信息;对于运行时可能出现的临时状态(比如目标对象暂时不可用),可以提供合理的默认行为,避免整个系统崩溃。
8.4 性能优化的细节积累
JdkDynamicAopProxy里的性能优化不是靠某个大招,而是靠一系列小细节的积累:拦截器链为空时跳过ReflectiveMethodInvocation创建、静态匹配的切点在代理创建时就完成匹配、Method对象缓存、equals和hashCode的快速路径判断。这些优化单个看起来不起眼,但叠加在一起,让 Spring AOP 在保持灵活性的同时,性能也能满足大多数生产场景的需求。
我在自己的项目里也借鉴了这种思路:不要等到性能出问题了才去优化,而是在设计阶段就把常见的快速路径考虑进去。比如一个方法调用,先判断最简单的条件(参数是否为空、缓存是否命中),再走复杂的逻辑。这种“快速路径优先”的设计,往往能用很小的代码改动换来明显的性能提升。
9. 调试 JdkDynamicAopProxy 的实用技巧
9.1 如何确认当前用的是哪种代理
在开发过程中,经常需要确认一个 Bean 到底是用 JDK 动态代理还是 CGLIB 代理生成的。最简单的方法是在运行时打印 Bean 的类名:
@Autowired private ApplicationContext applicationContext; public void checkProxyType() { Object bean = applicationContext.getBean("myService"); System.out.println("Bean 类名: " + bean.getClass().getName()); System.out.println("是否是 JDK 代理: " + Proxy.isProxyClass(bean.getClass())); System.out.println("是否是 CGLIB 代理: " + bean.getClass().getName().contains("$$")); }JDK 代理的类名通常是com.sun.proxy.$ProxyXX或jdk.proxy2.$ProxyXX,CGLIB 代理的类名会包含$$和BySpringCGLIB后缀。如果打印出来的类名是目标类本身,说明这个 Bean 没有被代理,可能是切点没匹配上,或者 AOP 自动代理没有生效。
9.2 查看拦截器链的调试方法
如果想看一个方法到底经过了哪些拦截器,可以在invoke()方法里打断点,查看chain变量的内容。但在生产环境不方便打断点,可以用反射的方式在运行时获取拦截器链:
public void printInterceptorChain(Object proxy, Method method) throws Exception { Advised advised = (Advised) proxy; List<Object> chain = advised.getInterceptorsAndDynamicInterceptionAdvice( method, proxy.getClass()); for (Object item : chain) { if (item instanceof InterceptorAndDynamicMethodMatcher) { InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) item; System.out.println("动态拦截器: " + dm.interceptor.getClass().getName()); } else { System.out.println("静态拦截器: " + item.getClass().getName()); } } }这个方法需要代理对象实现了Advised接口,Spring 生成的代理对象默认都会实现这个接口。通过打印拦截器链,可以快速定位是哪个切面没有生效,或者哪个切面的顺序不对。
9.3 常见异常的排查思路
使用 JDK 动态代理时,最常见的异常是AopInvocationException和IllegalArgumentException。AopInvocationException通常是因为目标对象为 null,或者目标方法不可访问。IllegalArgumentException通常是因为代理接口数组为空,或者参数类型不匹配。
排查这类异常时,我一般会按照以下顺序检查:第一,确认目标对象是否被正确注入,TargetSource.getTarget()是否返回非 null;第二,确认代理接口数组是否包含目标方法所在的接口;第三,确认目标方法的访问修饰符是否是 public;第四,确认切点表达式是否正确匹配到了目标方法。这四步检查下来,大部分问题都能定位到。
提示:如果异常信息里出现
No visible constructors或Superclass has no null constructors,说明 CGLIB 代理创建失败,通常是因为目标类没有无参构造函数。这种情况下要么给目标类加无参构造函数,要么改用 JDK 动态代理。
10. 我个人在实际操作中的体会
啃JdkDynamicAopProxy源码这件事,我前前后后花了大概两周时间,中间走了不少弯路。最开始我是从invoke()方法硬看的,结果被equals、hashCode的特殊处理绕晕了,完全没搞懂为什么要这么写。后来我换了个思路,先写了一个最简单的 AOP 示例,在invoke()方法里打断点,一步步跟踪方法调用的完整链路,才慢慢把各个环节串起来。
我的体会是,看源码一定要带着问题看。比如“为什么拦截器链为空时要直接反射调用”“为什么动态匹配失败要递归 proceed”“为什么 equals 和 hashCode 要特殊处理”,每个问题背后都对应着一个具体的设计考量。把这些问题搞清楚了,源码自然就通了。
另外,我强烈建议在看源码的同时动手写一些测试代码。比如自己实现一个MethodInterceptor,观察它在链中的执行顺序;或者故意写一个同类方法调用的场景,验证通知不生效的现象。这种“理论+实践”的方式,比单纯看代码效率高得多。
最后分享一个小技巧:如果你在调试 AOP 问题时觉得日志不够详细,可以把 Spring AOP 相关包的日志级别调到 TRACE。在application.properties里加上logging.level.org.springframework.aop=TRACE,就能看到代理创建、拦截器匹配、方法调用的详细日志。这些日志在排查“切面为什么没生效”这类问题时特别有用,能帮你快速定位到是代理没创建、还是切点没匹配、还是拦截器链为空。