ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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 也有Enhancer,Spring 为什么还要在上面包一层ProxyFactory?

原因其实很实际。Spring AOP 需要面对的场景远比"给一个接口生成代理"复杂得多:

  • 有的目标类实现了接口,有的没实现接口;
  • 有的场景需要强制使用 CGLIB,有的需要强制使用 JDK 代理;
  • 切面可能来自@Aspect注解,也可能来自 XML 配置的Advisor;
  • 代理对象可能需要暴露给ThreadLocal做上下文传递;
  • 同一个目标类可能被多个切面同时增强,需要按顺序织入。

如果每个场景都直接调用 JDK 或 CGLIB 的 API,代码会散落得到处都是,维护成本极高。ProxyFactory的价值就在于把这些差异统一收敛到一个抽象层,对外只暴露"设置目标对象、添加通知、获取代理"这几个简单操作,内部自动完成代理方式选择、通知链组装、代理对象生成。

从类图上看,ProxyFactory继承自ProxyCreatorSupport,而ProxyCreatorSupport又持有AopProxyFactory(默认实现是DefaultAopProxyFactory)。这个继承链的设计意图很清晰:ProxyFactory负责对外提供编程式 API,ProxyCreatorSupport负责管理Advisor链和监听器,AopProxyFactory负责真正决定用哪种代理方式并创建代理对象。职责分离得很干净。

2.2 核心字段逐个拆解

打开ProxyFactory的源码,真正属于它自己的字段其实很少,大部分能力都来自父类。但有几个关键字段必须搞清楚:

字段/属性所在类作用
targetSourceAdvisedSupport封装目标对象,支持热替换、池化等高级用法
advisorsAdvisedSupport通知器列表,决定代理对象要执行哪些增强逻辑
interfacesAdvisedSupport显式指定的代理接口列表
proxyTargetClassProxyConfig是否强制使用 CGLIB 代理
optimizeProxyConfig是否对 CGLIB 代理做优化(早期版本有效)
exposeProxyProxyConfig是否将代理对象暴露到AopContext
frozenProxyConfig配置冻结后是否允许修改

这里要特别说一下targetSource。很多人以为ProxyFactory直接持有目标对象,其实不是,它持有的是一个TargetSource。默认情况下是SingletonTargetSource,也就是包装了一个单例对象。但 Spring 之所以要多包一层,是为了支持prototype类型的 target、池化 target、热替换 target 等高级场景。这个设计在ProxyFactoryBean里体现得最明显。

proxyTargetClass这个布尔值也很关键,它直接决定了代理方式的走向。默认是false,意味着如果目标类实现了接口,就用 JDK 动态代理;如果没实现接口,才退回到 CGLIB。一旦你把它设为true,Spring 就会强制走 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 代理复杂不少。核心步骤包括:

  1. 创建Enhancer实例,设置父类为目标类;
  2. 设置接口列表(如果有);
  3. 设置Callback数组,通常包含DynamicAdvisedInterceptor、StaticUnadvisedInterceptor等;
  4. 设置CallbackFilter,决定每个方法用哪个 Callback;
  5. 调用enhancer.create()生成代理类实例。

CallbackFilter是 CGLIB 代理性能优化的关键。它会给目标类的每个方法分配一个索引,DynamicAdvisedInterceptor用于需要走通知链的方法,StaticUnadvisedInterceptor用于不需要增强的方法(比如final方法、static方法),AdvisedDispatcher用于处理Advised接口的方法。这样不需要增强的方法就不会走拦截器链,省去了一次判断开销。

实操心得:CGLIB 代理无法代理final方法和private方法。如果你发现某个方法明明加了切面注解却没生效,第一件事就是检查它是不是final或private的。这是新手最常踩的坑,没有之一。

3.3 通知链的组装与执行

代理对象创建好之后,真正执行增强逻辑的地方在ReflectiveMethodInvocation(JDK 代理)和CglibAopProxy$DynamicAdvisedInterceptor(CGLIB 代理)里。两者的核心逻辑是一致的:把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 List<MethodInterceptor> 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 List<MethodInterceptor> interceptors; private int currentIndex = -1; public SimpleMethodInvocation(Object target, Method method, Object[] args, List<MethodInterceptor> 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注解的值来排序,值越小优先级越高。但这里有个容易混淆的点:优先级高的通知,前置逻辑先执行,后置逻辑后执行。

假设有两个通知 A(order=1)和 B(order=2),执行顺序是:

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 的实用技巧:

技巧一:打印所有 Advisor

if (proxy instanceof Advised) { Advisor[] advisors = ((Advised) proxy).getAdvisors(); for (Advisor advisor : advisors) { System.out.println(advisor); } }

技巧二:查看代理类的字节码

启动时加上-Dcglib.debugLocation=/tmp/cglib,CGLIB 会把生成的代理类字节码 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 源码逐行比对,这个过程中收获的细节,是看多少篇分析文章都换不来的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进