资讯详情

深入解析JdkDynamicAopProxy:JDK动态代理与AOP调用链源码实战

📅 2026/10/13 1:12:25 | 华诺云谱 👁 阅读
深入解析JdkDynamicAopProxy:JDK动态代理与AOP调用链源码实战
1. 为什么我们要啃 JdkDynamicAopProxy 这块硬骨头做 Java 后端开发的朋友Spring 这套东西基本是绕不开的。平时写业务代码Transactional、Cacheable、自定义注解切面用起来确实顺手一个注解下去日志、事务、权限校验全给你安排得明明白白。但用归用很多人对底层到底怎么把切面逻辑“织”进业务方法这件事其实一直是模糊的。面试被问到“Spring AOP 底层怎么实现的”能答出“动态代理”四个字再往下问“JDK 动态代理和 CGLIB 有什么区别”“代理对象是怎么被创建的”“调用链是怎么串起来的”就开始支支吾吾了。我自己也是从这个阶段过来的。早些年排查一个事务失效的问题明明方法上加了Transactional结果回滚死活不生效最后发现是同类内部方法调用走了this压根没经过代理对象。那次之后我才下决心把 Spring AOP 的源码从头捋了一遍而JdkDynamicAopProxy就是这条链路上最核心的一环——它是基于 JDK 动态代理实现 AOP 的那个具体执行者所有走接口代理的切面逻辑最终都要经过它的invoke方法。这篇文章适合谁看如果你已经会用 Spring AOP但对其内部机制一知半解想搞清楚“代理对象怎么生成”“通知怎么被调用”“调用链怎么组织”这些问题那这篇就是写给你的。我会从整体设计思路讲起把JdkDynamicAopProxy的源码一段段拆开配上可复现的实操代码和调试技巧尽量做到你看完能自己动手验证而不是停留在“好像懂了”的层面。核心关键词就三个JDK 动态代理、AOP 调用链、源码调试。下面正式开始。2. 整体设计思路Spring 为什么选 JDK 动态代理2.1 两种代理方式的分工与取舍要理解JdkDynamicAopProxy得先搞清楚 Spring 在代理这件事上的整体策略。Spring AOP 支持两种代理方式JDK 动态代理和 CGLIB。前者要求目标类必须实现接口生成的代理对象和目标对象实现同一套接口后者通过继承目标类生成子类来代理不要求接口。那 Spring 是怎么选的核心逻辑在DefaultAopProxyFactory里。判断规则其实不复杂如果目标类实现了至少一个接口默认走 JDK 动态代理如果没实现任何接口就走 CGLIB。当然从 Spring Boot 2.x 开始默认配置改成了proxyTargetClasstrue也就是优先用 CGLIB这个变化很多人没注意到导致一些老代码迁移时行为不一致。为什么 Spring 一开始要优先用 JDK 动态代理我理解主要有几点考虑。第一JDK 动态代理是 JDK 原生支持的不依赖第三方库生成的代理类是java.lang.reflect.Proxy的子类稳定可靠。第二基于接口代理在语义上更“干净”代理对象和目标对象是兄弟关系而非父子关系不会出现子类覆盖父类方法带来的意外。第三接口本身就是一种契约基于契约做增强符合面向接口编程的思路。但 JDK 动态代理也有硬伤只能代理接口中声明的方法。如果目标类有个 public 方法但没在接口里声明那这个方法就代理不到。这也是为什么后来 Spring Boot 默认转向 CGLIB 的原因之一——业务代码里不写接口的情况太普遍了。2.2 JdkDynamicAopProxy 在整条链路中的位置Spring AOP 创建代理的入口是ProxyFactory它内部持有AdvisedSupport封装了目标对象、通知、切点等所有配置信息。当你调用getProxy()时ProxyFactory会把创建代理的活儿委托给AopProxyFactory而DefaultAopProxyFactory根据前面说的规则决定返回JdkDynamicAopProxy还是ObjenesisCglibAopProxy。JdkDynamicAopProxy本身实现了AopProxy和InvocationHandler两个接口。实现AopProxy是为了提供getProxy()方法让外部能拿到代理对象实现InvocationHandler是为了在代理对象的方法被调用时能接管调用并织入通知逻辑。这个设计很巧妙——它把“创建代理”和“处理调用”两件事集中在了一个类里职责清晰。整个调用链路大致是这样的业务代码调用代理对象的方法 → 触发JdkDynamicAopProxy.invoke()→ 在invoke里根据方法匹配到对应的通知链 → 把通知链包装成一个ReflectiveMethodInvocation→ 沿着拦截器链依次执行通知 → 最终通过反射调用目标方法。理解了这个链路再看源码就不会迷路。2.3 一个容易被忽略的设计Advised 接口的支持JdkDynamicAopProxy的invoke方法里有一段逻辑专门处理代理对象自身实现的方法比如Advised接口里的getTargetSource()、getAdvisors()这些。这些方法不应该被切面拦截而是直接调用advised对象的对应方法。这个细节很多人看源码时会跳过但它其实体现了 Spring 的一个设计考量代理对象不仅要能执行业务逻辑还要能被框架自身查询和操作比如运行期动态添加或移除通知。如果没处理好这部分就会出现“想给代理对象加个通知结果调用addAdvisor时反而被切面拦截了”的诡异问题。所以这段逻辑虽然不起眼但很关键。3. 核心源码逐段拆解与实操要点3.1 getProxy 方法代理对象是怎么造出来的先看getProxy()方法这是创建代理对象的入口。核心代码大致是这样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); }这段代码看着简单但每一步都有讲究。completeProxiedInterfaces负责收集所有需要代理的接口它不只是拿目标类实现的接口还会把SpringProxy、Advised、DecoratingProxy这些 Spring 内部接口加进去。为什么要加因为 Spring 需要能识别出“这是一个 Spring 生成的代理对象”并且在需要的时候能查询代理的配置信息。findDefinedEqualsAndHashCodeMethods这个方法也很有意思。它检查代理接口里有没有显式声明equals和hashCode如果有就设置一个标志位。因为equals和hashCode是Object的方法默认情况下代理对象会用自己的实现但如果接口里重新声明了就得特殊处理。这个细节平时根本感知不到但在某些框架集成场景下会出问题。最后一行Proxy.newProxyInstance就是 JDK 原生 API 了传入类加载器、接口数组和InvocationHandler也就是this返回代理对象。这里有个坑类加载器的选择。如果传错类加载器可能导致代理对象和目标对象不在同一个类加载器命名空间下出现ClassCastException。Spring 默认用目标类的类加载器一般没问题但在 OSGi 或自定义类加载器环境下要特别小心。提示调试getProxy时可以在Proxy.newProxyInstance前后打日志把proxiedInterfaces数组里的接口名都打印出来你会清楚看到 Spring 到底给代理对象加了哪些接口这对理解后续invoke的分支逻辑很有帮助。3.2 invoke 方法总览一个方法里的三个世界invoke方法是JdkDynamicAopProxy的灵魂也是整个类里最复杂的一段。它大致可以分成三个逻辑块处理Object基础方法、处理 Spring 内部接口方法、处理真正的业务方法增强。我先把整体结构列出来再逐个拆。public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy null; boolean setProxyContext false; TargetSource targetSource this.advised.targetSource; Object target null; try { // 块一处理 equals / hashCode if (!this.equalsDefined AopUtils.isEqualsMethod(method)) { return equals(args[0]); } if (!this.hashCodeDefined AopUtils.isHashCodeMethod(method)) { return hashCode(); } // 块二处理 DecoratingProxy 和 Advised 接口方法 if (!this.advised.opaque method.getDeclaringClass().isInterface() method.getDeclaringClass().isAssignableFrom(Advised.class)) { return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); } // 块三真正的业务方法增强 Object retVal; if (this.advised.exposeProxy) { oldProxy AopContext.setCurrentProxy(proxy); setProxyContext true; } target targetSource.getTarget(); Class? targetClass (target ! null ? target.getClass() : null); ListObject chain this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); if (chain.isEmpty()) { Object[] argsToUse AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { MethodInvocation invocation new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); retVal invocation.proceed(); } // 处理返回值 Class? returnType method.getReturnType(); if (retVal ! null retVal target returnType ! Object.class returnType.isInstance(proxy) !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { retVal proxy; } else if (retVal null returnType ! Void.TYPE returnType.isPrimitive()) { throw new AopInvocationException(Null return value from advice does not match primitive return type for: method); } return retVal; } finally { if (target ! null !targetSource.isStatic()) { targetSource.releaseTarget(target); } if (setProxyContext) { AopContext.setCurrentProxy(oldProxy); } } }3.3 块一equals 和 hashCode 的特殊处理第一块逻辑处理的是equals和hashCode。为什么这两个方法要单独拎出来因为它们是Object的方法任何对象都有代理对象也不例外。如果不特殊处理调用代理对象的equals时会走到后面的业务增强逻辑可能被切面拦截这显然不合理。this.equalsDefined这个标志位就是前面findDefinedEqualsAndHashCodeMethods设置的。如果代理接口里没有显式声明equals那AopUtils.isEqualsMethod(method)判断为真时直接调用equals(args[0])也就是JdkDynamicAopProxy自己的equals实现。这个实现比较的是advised对象逻辑上说得通。这里有个容易踩的坑如果你在切面里对equals做了增强或者依赖代理对象的equals做集合去重一定要清楚这个行为。我见过有同事把代理对象放进HashSet结果因为hashCode走的是JdkDynamicAopProxy的实现和预期不一致排查了半天。3.4 块二Advised 接口方法的直通处理第二块逻辑处理的是Advised接口及其父接口的方法。判断条件有三个!this.advised.opaque没设置 opaque 标志、method.getDeclaringClass().isInterface()方法声明在接口上、method.getDeclaringClass().isAssignableFrom(Advised.class)这个接口是 Advised 或其子接口。满足条件时直接通过反射调用this.advised的对应方法不走通知链。这就是前面说的“框架自身操作代理对象”的通道。比如你调用((Advised) proxy).getAdvisors()拿到的就是配置的通知列表而不是被切面增强后的结果。opaque这个标志位默认是 false可以通过ProxyFactory.setOpaque(true)设置。设置成 true 后代理对象就不再暴露Advised接口的方法了相当于“不透明”。这个配置很少用但在某些需要隐藏框架细节的场景下有用。注意如果你在业务代码里把代理对象强转成Advised来动态修改通知一定要确认opaque是 false否则会抛ClassCastException。这个坑我在一个动态限流的需求里踩过当时想运行期给某个代理加个限流通知结果死活转不过去最后发现是框架某处把 opaque 设成了 true。3.5 块三通知链的获取与执行第三块是真正的核心。先看exposeProxy的处理如果开启了暴露代理会把当前代理对象设置到AopContext的 ThreadLocal 里这样在目标方法内部可以通过AopContext.currentProxy()拿到代理对象。这是解决“同类内部调用不走代理”问题的标准手段但代价是每次调用都要操作 ThreadLocal有轻微性能开销而且必须开启exposeProxy配置。接着是targetSource.getTarget()获取目标对象然后this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)获取这个方法对应的拦截器链。这个方法内部会做切点匹配把符合条件的通知和拦截器收集起来包装成InterceptorAndDynamicMethodMatcher列表。如果链为空说明这个方法不需要增强直接反射调用目标方法否则创建ReflectiveMethodInvocation调用proceed()沿着链执行。ReflectiveMethodInvocation.proceed()是另一个核心类它维护了一个currentInterceptorIndex游标每次调用proceed就取出下一个拦截器执行拦截器内部再调用invocation.proceed()就继续往下走形成递归式的责任链。这个设计是 Spring AOP 通知执行顺序的基础理解它才能搞清楚Around、Before、After的执行时机。最后是返回值的处理。有一段逻辑是如果返回值就是目标对象本身且返回类型能匹配代理对象那就返回代理对象。这是为了处理方法返回this的场景保证返回的始终是代理对象避免后续调用绕过切面。还有一段是处理原始类型返回 null 的情况直接抛异常因为原始类型不能为 null。4. 完整实操手写一个 JDK 动态代理复现 AOP 效果4.1 准备目标接口和实现类光看源码容易飘咱们动手写一个最小复现。先定义一个接口和实现类public interface OrderService { String createOrder(String orderId); void cancelOrder(String orderId); } public class OrderServiceImpl implements OrderService { Override public String createOrder(String orderId) { System.out.println(执行创建订单订单号 orderId); return SUCCESS- orderId; } Override public void cancelOrder(String orderId) { System.out.println(执行取消订单订单号 orderId); } }这两个方法一个带返回值一个不带方便后面验证返回值处理逻辑。4.2 用 Spring 的 ProxyFactory 创建代理不直接手写InvocationHandler而是用 Spring 的ProxyFactory这样能完整走一遍JdkDynamicAopProxy的流程import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.MethodBeforeAdvice; import java.lang.reflect.Method; public class AopDemo { public static void main(String[] args) { OrderService target new OrderServiceImpl(); ProxyFactory factory new ProxyFactory(target); factory.addAdvice(new MethodBeforeAdvice() { Override public void before(Method method, Object[] args, Object target) { System.out.println([前置通知] 准备执行方法 method.getName()); } }); OrderService proxy (OrderService) factory.getProxy(); System.out.println(代理对象类型 proxy.getClass().getName()); String result proxy.createOrder(ORD-001); System.out.println(返回结果 result); proxy.cancelOrder(ORD-002); } }运行这段代码你会看到代理对象类型是com.sun.proxy.$Proxy0这样的名字说明走的是 JDK 动态代理。输出里前置通知会在每个方法执行前打印说明增强生效了。4.3 断点调试亲眼看看 invoke 的执行路径光跑通还不够得亲眼看看invoke是怎么走的。在JdkDynamicAopProxy.invoke方法的第一行打个断点然后以 Debug 模式运行上面的main方法。你会看到第一次进入invokemethod是createOrderargs是[ORD-001]。单步往下走会经过equals/hashCode判断不匹配经过Advised接口判断不匹配然后走到getInterceptorsAndDynamicInterceptionAdvice拿到一个包含我们那个MethodBeforeAdvice的链。接着创建ReflectiveMethodInvocation调用proceed()进入拦截器执行前置通知最后反射调用目标方法。第二次进入invokemethod是cancelOrder流程类似但因为返回类型是 void最后返回值处理那段逻辑走的分支不同。实操心得调试时把chain变量加到 Watch 窗口观察它的 size 和每个元素的类型。你会发现MethodBeforeAdvice被包装成了MethodBeforeAdviceInterceptor这是 Spring 把通知适配成拦截器的标准做法。理解这层适配对搞清楚通知执行顺序很关键。4.4 验证同类内部调用不走代理的经典问题现在改一下OrderServiceImpl加一个内部调用public class OrderServiceImpl implements OrderService { Override public String createOrder(String orderId) { System.out.println(执行创建订单订单号 orderId); this.cancelOrder(orderId); // 内部调用 return SUCCESS- orderId; } // cancelOrder 不变 }再跑一遍你会发现cancelOrder的前置通知没有打印。原因就是this.cancelOrder走的是目标对象自己的引用压根没经过代理对象自然也就不会触发invoke。这就是我开头提到的那个事务失效问题的根源。解决办法有两个一是通过AopContext.currentProxy()拿到代理对象再调用但需要开启exposeProxy二是把cancelOrder拆到另一个 Bean 里通过依赖注入调用。前者配置简单但有 ThreadLocal 开销后者更符合 Spring 的设计哲学我一般推荐后者。5. 常见问题与排查技巧实录5.1 代理对象强转失败怎么办最常见的报错就是ClassCastException比如OrderServiceImpl cannot be cast to OrderService或者反过来。原因通常是代理方式和预期不一致。如果你期望 JDK 动态代理但目标类没实现接口Spring 会走 CGLIB生成的代理对象是目标类的子类强转成接口没问题但强转成目标类本身在某些情况下会出问题。排查思路先打印proxy.getClass()和proxy.getClass().getInterfaces()看清楚代理对象到底实现了哪些接口、继承了什么类。然后检查ProxyFactory的proxyTargetClass配置确认代理方式。如果是 Spring Boot 项目还要注意spring.aop.proxy-target-class这个配置项的默认值。5.2 通知不生效的几种典型场景通知不生效是另一个高频问题。我整理了一个速查表现象可能原因排查方法所有方法都不增强切点表达式写错打印advised.getAdvisors()看通知是否注册部分方法不增强方法没在接口中声明检查代理方式考虑改用 CGLIB内部调用不增强走的是 this 引用用AopContext.currentProxy()或拆分 Bean私有方法不增强JDK 代理只能代理接口方法私有方法本身就不该被 AOP 增强静态方法不增强静态方法不属于对象无法通过代理增强需换思路这个表基本覆盖了我这些年遇到的大部分情况。其中“部分方法不增强”最隐蔽因为接口里没声明的方法JDK 动态代理根本不会去代理它调用时直接走目标对象连invoke都不会进。5.3 性能相关的注意事项JDK 动态代理的性能开销主要在两块一是反射调用二是拦截器链的遍历。反射调用在 JDK 8 之后有MethodHandle和LambdaMetafactory的优化但 Spring AOP 默认还是用Method.invoke。如果对性能极其敏感可以考虑用 CGLIB 或者 AspectJ 编译期织入。拦截器链的遍历本身开销不大但如果切点表达式写得过于复杂每次调用都要做模式匹配累积起来也不容忽视。我的经验是切点尽量精确到方法名或注解避免用execution(* com.xxx..*.*(..))这种大范围匹配。提示可以用-Dspring.aop.proxy-target-classtrue强制走 CGLIB对比一下两种方式的性能差异。在方法调用密集的场景下CGLIB 通常略快因为它不需要接口方法查找但生成代理类的开销更大启动时会慢一点。5.4 调试源码时的实用技巧最后分享几个调试 Spring AOP 源码的技巧。第一把org.springframework.aop包的日志级别调到 DEBUG能看到很多内部状态。第二在ReflectiveMethodInvocation.proceed()里打断点观察currentInterceptorIndex的变化能直观理解责任链的推进过程。第三用 IDEA 的 Evaluate Expression 功能在断点处直接调用advised.getAdvisors()查看通知列表比翻代码快得多。还有一个我常用的方法写一个自定义的MethodInterceptor在invoke方法里打印invocation.getMethod().getName()和invocation.getArguments()然后加到ProxyFactory里。这样每次方法调用你都能看到完整的入参和调用顺序排查通知顺序问题时特别有用。6. 从 JdkDynamicAopProxy 延伸出去的几个思考6.1 和 CGLIB 代理的本质差异理解了JdkDynamicAopProxy再看CglibAopProxy会轻松很多。两者的核心差异在于代理对象的生成方式JDK 是“实现接口”CGLIB 是“继承类”。这导致 CGLIB 能代理更多方法包括没在接口里声明的 public 方法但也有局限比如无法代理 final 方法和 final 类。从invoke的角度看CGLIB 用的是MethodInterceptor.intercept()参数里多了个MethodProxy可以直接调用目标方法而不用反射这是它性能优势的来源之一。但两者的通知链组织逻辑是共通的都依赖Advised和ReflectiveMethodInvocation。所以吃透一个另一个就是换个壳。6.2 对日常开发的启发搞懂JdkDynamicAopProxy之后我对日常开发有几个实实在在的改变。第一写业务代码时尽量面向接口这样能享受 JDK 动态代理的稳定性和语义清晰。第二遇到事务、缓存、异步这些依赖 AOP 的注解会下意识检查调用路径有没有绕过代理。第三自定义注解切面时会注意切点表达式的精确度避免误伤和性能损耗。还有一个容易被忽略的点代理对象的toString、equals、hashCode行为。前面说过equals和hashCode有特殊处理但toString没有它会走正常的增强逻辑。如果你在日志里打印代理对象看到的是切面增强后的结果而不是目标对象的toString。这个细节在排查问题时偶尔会带来困惑。6.3 后续可以深入的方向如果你已经把JdkDynamicAopProxy啃下来了下一步可以往这几个方向走。一是ReflectiveMethodInvocation的完整源码搞清楚通知的执行顺序和proceed的递归机制。二是AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice的切点匹配逻辑理解MethodMatcher和Pointcut的协作。三是 Spring 事务管理里TransactionInterceptor是怎么和 AOP 结合的这是 AOP 最经典的应用场景。我个人在实际操作中的体会是源码这东西看一遍只能混个脸熟真正理解得靠动手改、动手调。你可以试着给JdkDynamicAopProxy加几行日志或者写个自定义的AopProxy实现替换掉默认的看看会发生什么。这种“破坏性”的实验比老老实实读代码收获大得多。最后再分享一个小技巧把 Spring 的源码下载到本地用 IDEA 关联上调试时能直接跳到源码行比看反编译的 class 文件舒服太多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑