Spring AOP ProxyFactory 源码深度解析:代理创建机制与性能优化实战

发布时间:2026/10/13 5:45:31
Spring AOP ProxyFactory 源码深度解析:代理创建机制与性能优化实战
1. 从一次线上问题说起为什么我要啃 ProxyFactory 源码前段时间排查一个线上问题某个订单服务在高峰期出现了诡异的耗时抖动日志里没有任何异常但链路追踪显示每次调用某个方法时都会多出几十毫秒的额外开销。排查了半天最后定位到问题出在一个自定义注解上——这个注解通过 Spring AOP 做了切面增强但代理对象的创建方式选得不太对导致每次调用都走了最重的代理路径。这个问题让我重新翻开了 Spring AOP 的源码而所有代理创建的入口最终都会汇聚到一个类ProxyFactory。不管你用的是Aspect注解、XML 配置的aop:config还是手动编程式地创建代理底层都绕不开它。可以说ProxyFactory 是 Spring AOP 代理创建的总调度中心理解了它你就理解了 Spring AOP 一半以上的核心逻辑。这篇文章适合已经用过 Spring AOP、但对其底层机制一知半解的开发者。我会从 ProxyFactory 的设计思路讲起拆解它的核心字段和创建流程分析 JDK 动态代理与 CGLIB 代理的切换逻辑最后给出几个实际项目中容易踩的坑和排查技巧。读完之后你不仅能看懂 ProxyFactory 的源码还能在实际选型和问题排查时心里有底。2. ProxyFactory 的整体设计与核心思路拆解2.1 为什么 Spring 要单独抽象一个 ProxyFactory很多人第一次看 Spring AOP 源码时会疑惑JDK 本身就有Proxy.newProxyInstance()CGLIB 也有EnhancerSpring 为什么还要在上面包一层ProxyFactory原因其实很实际。Spring AOP 需要面对的场景远比给一个接口生成代理复杂得多有的目标类实现了接口有的没实现接口有的场景需要强制使用 CGLIB有的需要强制使用 JDK 代理切面可能来自Aspect注解也可能来自 XML 配置的Advisor代理对象可能需要暴露给ThreadLocal做上下文传递同一个目标类可能被多个切面同时增强需要按顺序织入。如果每个场景都直接调用 JDK 或 CGLIB 的 API代码会散落得到处都是维护成本极高。ProxyFactory的价值就在于把这些差异统一收敛到一个抽象层对外只暴露设置目标对象、添加通知、获取代理这几个简单操作内部自动完成代理方式选择、通知链组装、代理对象生成。从类图上看ProxyFactory继承自ProxyCreatorSupport而ProxyCreatorSupport又持有AopProxyFactory默认实现是DefaultAopProxyFactory。这个继承链的设计意图很清晰ProxyFactory负责对外提供编程式 APIProxyCreatorSupport负责管理Advisor链和监听器AopProxyFactory负责真正决定用哪种代理方式并创建代理对象。职责分离得很干净。2.2 核心字段逐个拆解打开ProxyFactory的源码真正属于它自己的字段其实很少大部分能力都来自父类。但有几个关键字段必须搞清楚字段/属性所在类作用targetSourceAdvisedSupport封装目标对象支持热替换、池化等高级用法advisorsAdvisedSupport通知器列表决定代理对象要执行哪些增强逻辑interfacesAdvisedSupport显式指定的代理接口列表proxyTargetClassProxyConfig是否强制使用 CGLIB 代理optimizeProxyConfig是否对 CGLIB 代理做优化早期版本有效exposeProxyProxyConfig是否将代理对象暴露到AopContextfrozenProxyConfig配置冻结后是否允许修改这里要特别说一下targetSource。很多人以为ProxyFactory直接持有目标对象其实不是它持有的是一个TargetSource。默认情况下是SingletonTargetSource也就是包装了一个单例对象。但 Spring 之所以要多包一层是为了支持prototype类型的 target、池化 target、热替换 target 等高级场景。这个设计在ProxyFactoryBean里体现得最明显。proxyTargetClass这个布尔值也很关键它直接决定了代理方式的走向。默认是false意味着如果目标类实现了接口就用 JDK 动态代理如果没实现接口才退回到 CGLIB。一旦你把它设为trueSpring 就会强制走 CGLIB哪怕目标类实现了接口。2.3 代理方式选择的决策逻辑代理方式的选择发生在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); }这段逻辑有几个容易忽略的细节第一hasNoUserSuppliedProxyInterfaces判断的是用户是否显式指定了代理接口。如果没指定且目标类本身不是接口那即使proxyTargetClass是false也会走 CGLIB。这就是为什么很多没实现接口的 Service 类即使你没做任何配置Spring 也会自动用 CGLIB 给它生成代理。第二targetClass.isInterface()这个判断很微妙。如果目标类本身就是个接口那只能用 JDK 代理因为 CGLIB 是继承式的没法代理接口。第三Proxy.isProxyClass(targetClass)是为了处理目标对象本身已经是 JDK 代理的情况这时候再套一层 CGLIB 会出问题所以还是走 JDK 代理。注意optimize参数在 Spring 5.x 之后对 CGLIB 的影响已经很小了早期它会让 CGLIB 使用更激进的字节码生成策略但现在基本可以忽略。不要为了性能优化盲目打开它。3. 核心细节解析与实操要点3.1 JDK 动态代理的创建过程当决策逻辑选择 JDK 代理时JdkDynamicAopProxy会被实例化。它的getProxy方法核心逻辑如下final Class?[] proxiedInterfaces AopProxyUtils.completeProxiedInterfaces(this.advised, true); findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);这里有几个关键点值得展开。completeProxiedInterfaces会做几件事把用户指定的接口、目标类实现的接口、以及SpringProxy、Advised、DecoratingProxy这几个 Spring 内部接口合并去重。为什么要加Advised接口因为这样你拿到代理对象后可以把它强转成Advised动态地添加或移除通知器。这是 Spring AOP 提供的一个很实用的后门。findDefinedEqualsAndHashCodeMethods是为了检查代理接口里是否显式声明了equals和hashCode。如果声明了JDK 代理在调用这两个方法时会走拦截器链如果没声明JDK 代理会直接调用Object的实现不走拦截器。这个细节在排查为什么我的 equals 方法没被切面拦截时非常有用。JdkDynamicAopProxy本身实现了InvocationHandler所以Proxy.newProxyInstance的第三个参数传的是this。所有代理方法的调用最终都会进入它的invoke方法在那里完成通知链的组装和执行。3.2 CGLIB 代理的创建过程CGLIB 代理的创建在CglibAopProxy#getProxy里逻辑比 JDK 代理复杂不少。核心步骤包括创建Enhancer实例设置父类为目标类设置接口列表如果有设置Callback数组通常包含DynamicAdvisedInterceptor、StaticUnadvisedInterceptor等设置CallbackFilter决定每个方法用哪个 Callback调用enhancer.create()生成代理类实例。CallbackFilter是 CGLIB 代理性能优化的关键。它会给目标类的每个方法分配一个索引DynamicAdvisedInterceptor用于需要走通知链的方法StaticUnadvisedInterceptor用于不需要增强的方法比如final方法、static方法AdvisedDispatcher用于处理Advised接口的方法。这样不需要增强的方法就不会走拦截器链省去了一次判断开销。实操心得CGLIB 代理无法代理final方法和private方法。如果你发现某个方法明明加了切面注解却没生效第一件事就是检查它是不是final或private的。这是新手最常踩的坑没有之一。3.3 通知链的组装与执行代理对象创建好之后真正执行增强逻辑的地方在ReflectiveMethodInvocationJDK 代理和CglibAopProxy$DynamicAdvisedInterceptorCGLIB 代理里。两者的核心逻辑是一致的把Advisor列表转换成MethodInterceptor列表然后按顺序执行。这里有个很重要的概念叫拦截器链。假设你有三个通知前置通知、环绕通知、后置通知它们会被组装成一个链执行顺序是前置通知 - 环绕通知前半段 - 目标方法 - 环绕通知后半段 - 后置通知如果某个通知抛出了异常链会在抛出点中断后续通知不再执行异常会向上传播。这个行为在排查为什么后置通知没执行时很关键——很可能是因为前面的通知抛异常了。ReflectiveMethodInvocation#proceed方法是整个链式调用的核心它维护了一个currentInterceptorIndex游标每次调用proceed就前进一格直到所有拦截器执行完毕最后调用目标方法。这个设计是典型的责任链模式理解了这个你就理解了 Spring AOP 的执行时逻辑。3.4 编程式使用 ProxyFactory 的完整示例虽然日常开发中我们更多用Aspect但理解编程式用法对排查问题极有帮助。下面是一个完整的示例// 目标对象 public class OrderService { public void createOrder(String orderId) { System.out.println(创建订单: orderId); } } // 前置通知 public class LogBeforeAdvice implements MethodBeforeAdvice { Override public void before(Method method, Object[] args, Object target) { System.out.println(准备执行: method.getName()); } } // 编程式创建代理 public class ProxyFactoryDemo { public static void main(String[] args) { OrderService target new OrderService(); ProxyFactory factory new ProxyFactory(target); factory.addAdvice(new LogBeforeAdvice()); OrderService proxy (OrderService) factory.getProxy(); proxy.createOrder(ORDER_001); } }这段代码虽然简单但涵盖了 ProxyFactory 的核心用法构造时传入目标对象添加通知获取代理。addAdvice方法内部会把Advice包装成Advisor然后加入advisors列表。如果你想强制使用 CGLIB只需要加一行factory.setProxyTargetClass(true);如果你想暴露代理对象到ThreadLocal方便在目标方法内部获取当前代理factory.setExposeProxy(true); // 在目标方法内部通过 AopContext.currentProxy() 获取注意setExposeProxy(true)有性能开销因为它需要把代理对象存到ThreadLocal里。除非确实需要在目标方法内部调用代理方法比如解决自调用不走切面的问题否则不要打开。4. 实操过程与核心环节实现4.1 从零手写一个简化版 ProxyFactory要真正理解 ProxyFactory最好的方式是手写一个简化版。下面这个实现虽然只有几十行但涵盖了核心逻辑public class SimpleProxyFactory { private Object target; private ListMethodInterceptor interceptors new ArrayList(); private boolean proxyTargetClass false; public SimpleProxyFactory(Object target) { this.target target; } public void addInterceptor(MethodInterceptor interceptor) { this.interceptors.add(interceptor); } public void setProxyTargetClass(boolean proxyTargetClass) { this.proxyTargetClass proxyTargetClass; } public Object getProxy() { Class? targetClass target.getClass(); if (proxyTargetClass || targetClass.getInterfaces().length 0) { return createCglibProxy(targetClass); } return createJdkProxy(targetClass); } private Object createJdkProxy(Class? targetClass) { return Proxy.newProxyInstance( targetClass.getClassLoader(), targetClass.getInterfaces(), (proxy, method, args) - { MethodInvocation invocation new SimpleMethodInvocation(target, method, args, interceptors); return invocation.proceed(); } ); } private Object createCglibProxy(Class? targetClass) { Enhancer enhancer new Enhancer(); enhancer.setSuperclass(targetClass); enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) - { MethodInvocation invocation new SimpleMethodInvocation(target, method, args, interceptors); return invocation.proceed(); }); return enhancer.create(); } }配套的SimpleMethodInvocation负责链式调用public class SimpleMethodInvocation implements MethodInvocation { private final Object target; private final Method method; private final Object[] args; private final ListMethodInterceptor interceptors; private int currentIndex -1; public SimpleMethodInvocation(Object target, Method method, Object[] args, ListMethodInterceptor interceptors) { this.target target; this.method method; this.args args; this.interceptors interceptors; } Override public Object proceed() throws Throwable { if (currentIndex interceptors.size()) { return method.invoke(target, args); } return interceptors.get(currentIndex).invoke(this); } // 省略 getMethod、getArguments 等实现 }写完这个简化版再回头看 Spring 的源码你会发现结构几乎一模一样只是 Spring 处理了更多的边界情况和扩展点。这种先手写再对照的学习方式比直接啃源码效率高得多。4.2 代理方式选择的参数计算与验证在实际项目中代理方式的选择往往不是显式配置的而是由目标类的特征自动决定的。下面这张表可以帮助你快速判断目标类特征proxyTargetClass实际代理方式原因实现接口未配置falseJDK 代理默认优先 JDK实现接口配置 truetrueCGLIB强制 CGLIB未实现接口未配置falseCGLIB无接口可用本身是接口任意JDK 代理CGLIB 无法代理接口已是 JDK 代理类任意JDK 代理避免二次代理验证当前 Bean 用的是哪种代理方式有个很实用的小技巧Autowired private OrderService orderService; PostConstruct public void checkProxyType() { System.out.println(代理类: orderService.getClass().getName()); System.out.println(是否 CGLIB: orderService.getClass().getName().contains($$)); System.out.println(是否 JDK: Proxy.isProxyClass(orderService.getClass())); }CGLIB 生成的代理类名里会包含$$符号比如OrderService$$EnhancerBySpringCGLIB$$abc123。JDK 代理的类名通常是com.sun.proxy.$ProxyXX或jdk.proxy2.$ProxyXX。通过类名就能快速判断。4.3 通知顺序的精确控制当多个通知作用于同一个方法时执行顺序就变得很重要。Spring 默认按照Ordered接口或Order注解的值来排序值越小优先级越高。但这里有个容易混淆的点优先级高的通知前置逻辑先执行后置逻辑后执行。假设有两个通知 Aorder1和 Border2执行顺序是A 的前置逻辑 B 的前置逻辑 目标方法 B 的后置逻辑 A 的后置逻辑这就像一个洋葱优先级高的通知在最外层。理解了这个模型你就能精确控制通知的执行顺序。如果你想手动控制顺序可以在编程式创建时按顺序添加factory.addAdvisor(0, advisorA); // 索引越小优先级越高 factory.addAdvisor(1, advisorB);实操心得在排查为什么我的事务注解和自定义切面顺序不对这类问题时先打印出所有 Advisor 的 order 值往往一眼就能看出问题。事务通知的默认 order 是Ordered.LOWEST_PRECEDENCE也就是最低优先级所以它通常在最内层。5. 常见问题与排查技巧实录5.1 切面不生效的六大原因速查切面不生效是 Spring AOP 最高频的问题我整理了一张速查表现象可能原因排查方法方法完全没被拦截方法是final/private/static检查方法修饰符方法没被拦截自调用this 调用用AopContext.currentProxy()或注入自身部分方法没被拦截方法在父类中且是final检查继承链切面类没生效切面类没被 Spring 管理检查是否加了Component注解切面没生效注解的Retention不是RUNTIME检查注解定义代理方式不对目标类没实现接口但期望 JDK 代理检查proxyTargetClass配置其中自调用问题最隐蔽。比如Service public class OrderService { public void methodA() { methodB(); // 这里调用不会走切面 } Transactional public void methodB() { // 事务不会生效 } }原因是methodA内部调用methodB时用的是this引用而不是代理对象。解决办法有三种注入自身、用AopContext.currentProxy()、或者把methodB拆到另一个 Bean 里。5.2 CGLIB 代理的常见异常与解决CGLIB 代理虽然强大但也有一些限制会引发异常问题一Cannot subclass final class目标类是final的CGLIB 无法继承。解决办法是去掉final或者让目标类实现接口改用 JDK 代理。问题二NoSuchMethodError或VerifyError通常是因为 CGLIB 版本和 JDK 版本不兼容。Spring 5.x 之后内置了spring-core里的 CGLIB 重打包版本一般不会有这个问题。如果项目里显式引入了老版本 CGLIB建议排除掉。问题三构造函数被调用两次CGLIB 创建代理时会调用目标类的构造函数如果构造函数里有副作用比如注册监听器可能会执行两次。解决办法是把副作用逻辑移到PostConstruct方法里。注意Spring 4.3 之后CGLIB 代理默认使用Objenesis来实例化对象绕过了构造函数调用。所以构造函数调用两次这个问题在新版本里基本不会出现了。如果你还在用老版本 Spring升级是最简单的解决办法。5.3 性能相关的排查经验回到文章开头提到的那个线上问题。当时的现象是每次调用多出几十毫秒排查后发现是exposeProxy被打开了而且代理对象被频繁地存取ThreadLocal。关掉之后耗时立刻恢复正常。这个经历让我总结了几条性能相关的经验第一exposeProxy能不开就不开。它的开销主要来自ThreadLocal的读写和代理对象的存取在高频调用场景下累积起来很可观。第二CGLIB 代理的创建成本比 JDK 代理高但执行成本反而可能更低。因为 CGLIB 生成的是字节码方法调用是直接的JDK 代理走的是反射每次调用都有反射开销。所以对于单例 Bean创建成本可以忽略执行性能 CGLIB 反而有优势。第三CallbackFilter的优化很重要。Spring 会自动把不需要增强的方法标记为StaticUnadvisedInterceptor避免走拦截器链。如果你手写 CGLIB 代理记得也要做这个优化。第四通知链越长性能开销越大。每个通知都是一次方法调用如果切面逻辑本身很重比如查数据库、发远程调用那性能瓶颈就在切面里而不是代理机制本身。5.4 调试 ProxyFactory 的实用技巧最后分享几个调试 ProxyFactory 的实用技巧技巧一打印所有 Advisorif (proxy instanceof Advised) { Advisor[] advisors ((Advised) proxy).getAdvisors(); for (Advisor advisor : advisors) { System.out.println(advisor); } }技巧二查看代理类的字节码启动时加上-Dcglib.debugLocation/tmp/cglibCGLIB 会把生成的代理类字节码 dump 到指定目录用反编译工具打开就能看到代理类的真实结构。技巧三用条件断点拦截特定方法在JdkDynamicAopProxy#invoke或CglibAopProxy$DynamicAdvisedInterceptor#intercept方法上打条件断点条件设为method.getName().equals(你的方法名)可以精准拦截目标方法的调用。技巧四检查 AOP 自动代理注册器如果切面完全不生效检查一下AnnotationAwareAspectJAutoProxyCreator是否被注册。在 Spring Boot 里EnableAspectJAutoProxy或spring-boot-starter-aop会自动注册它。如果手动配置了EnableAspectJAutoProxy(proxyTargetClass false)但目标类没实现接口就会出问题。我个人在实际操作中的体会是ProxyFactory 这个类看起来简单但它是 Spring AOP 所有魔法的起点。把它的源码读透再去看Aspect注解的处理流程、事务的代理逻辑、缓存注解的实现都会顺畅很多。建议你按照文章里的简化版实现自己敲一遍然后对照 Spring 源码逐行比对这个过程中收获的细节是看多少篇分析文章都换不来的。