☰
Spring AOP ProxyFactory 源码深度解析:代理创建与通知链组装
2026/10/13 4:27:09 网站建设 项目流程

1. 从一次线上问题说起:为什么我要啃 ProxyFactory 源码

前段时间排查一个线上问题,某个业务方法上的自定义注解突然失效了,日志里没有任何异常,方法也正常执行,就是增强逻辑没生效。当时第一反应是切面没注册上,查了 Bean 定义、切点表达式、通知类型,全都正常。最后把断点打在ProxyFactory的getProxy()方法上,才发现问题出在代理对象的创建环节——目标类被另一个框架提前代理过一层,导致我配置的切点匹配到了外层代理类而不是原始类。

这个问题折腾了大半天,也让我下定决心把 Spring AOP 最核心的ProxyFactory彻底梳理一遍。很多人学 Spring AOP,上来就背@Aspect、@Pointcut、@Around的用法,能跑通 Demo 就觉得自己会了。但真到了生产环境,遇到代理失效、增强顺序错乱、循环依赖、this调用不走代理这些坑,光会写注解是远远不够的。

ProxyFactory是 Spring AOP 里真正干活的角色,它把"切面配置"翻译成"代理对象",是连接声明式 AOP 和底层字节码生成的桥梁。搞懂它,你就能明白:为什么有的 Bean 被 JDK 动态代理、有的被 CGLIB 代理、exposeProxy到底解决什么问题、多个通知是怎么串成一条调用链的。这篇文章我会从设计思路、核心字段、代理创建流程、通知链组装、常见问题排查几个维度,把ProxyFactory拆开揉碎讲清楚,适合已经用过 Spring AOP 但想深入源码的开发者,也适合正在准备面试、想搞明白底层原理的朋友。

2. ProxyFactory 的整体设计与选型思路

2.1 它在 Spring AOP 体系里处于什么位置

Spring AOP 的整个链路可以粗略分成三段:切面解析、代理创建、方法调用。@Aspect注解的解析由AnnotationAwareAspectJAutoProxyCreator这类后置处理器完成,它负责把切面信息转换成Advisor集合;而ProxyFactory处在中间那段,接收目标对象和Advisor列表,产出最终的代理对象;方法调用阶段则由JdkDynamicAopProxy或CglibAopProxy里的拦截器链负责。

你可以把ProxyFactory理解成一个"代理工厂的调度中心"。它本身不直接生成字节码,而是根据配置决定用哪种代理策略,然后把活派给具体的AopProxy实现。这种设计的好处是职责分离:ProxyFactory管配置和决策,AopProxy管子节码生成,AdvisorChainFactory管通知链组装,各司其职。

从继承关系看,ProxyFactory继承自ProxyCreatorSupport,而ProxyCreatorSupport又持有AopProxyFactory(默认是DefaultAopProxyFactory)。这个AopProxyFactory才是真正决定"用 JDK 还是 CGLIB"的角色。理解这层关系很重要,因为很多代理选型的问题,答案都藏在DefaultAopProxyFactory的createAopProxy方法里。

2.2 为什么要有 ProxyFactory 这层抽象

有人会问,Spring 直接调 JDK 的Proxy.newProxyInstance或者 CGLIB 的Enhancer不就行了,为什么要多一层ProxyFactory?

核心原因是统一编程模型。JDK 动态代理和 CGLIB 的 API 差异很大:前者基于接口,需要InvocationHandler;后者基于继承,需要MethodInterceptor和CallbackFilter。如果业务代码直接依赖这两套 API,切换代理方式时改动量巨大。ProxyFactory把两者的差异封装起来,对外只暴露addAdvisor、setTarget、getProxy这几个统一方法,屏蔽了底层实现细节。

另一个原因是通知链的统一管理。无论用哪种代理,方法调用时都需要按顺序执行多个通知(前置、后置、环绕、异常、返回)。ProxyFactory通过AdvisedSupport维护Advisor列表,再由AdvisorChainFactory统一组装成MethodInterceptor链,这样 JDK 和 CGLIB 两条路径可以复用同一套通知链逻辑。

还有一个容易被忽略的点:ProxyFactory支持运行时代理配置。你可以在创建代理之前动态添加、删除Advisor,甚至替换目标对象。这种灵活性在框架集成场景里非常有用,比如某些 ORM 框架需要在运行时根据实体类动态织入增强逻辑。

2.3 核心字段与继承结构拆解

ProxyFactory的字段大部分来自父类,我挑几个最关键的说明:

字段/属性所在类作用
targetSourceAdvisedSupport封装目标对象,支持热替换、池化等高级用法
advisorsAdvisedSupport通知器列表,决定织入哪些增强逻辑
interfacesAdvisedSupport代理需要实现的接口列表
proxyTargetClassProxyConfig是否强制使用 CGLIB 代理
exposeProxyProxyConfig是否把代理对象暴露到AopContext
optimizeProxyConfig是否对 CGLIB 代理做优化
frozenAdvisedSupport冻结配置后不允许再修改通知链

这里要特别提一下targetSource。很多人只知道setTarget(Object),其实它内部会包装成一个SingletonTargetSource。如果你需要每次调用都拿新对象,可以用PrototypeTargetSource;需要对象池就用PoolingTargetSource。TargetSource的设计让ProxyFactory的能力远超简单的静态代理。

proxyTargetClass和optimize这两个布尔值经常被混淆。proxyTargetClass=true表示强制走 CGLIB;optimize=true表示对 CGLIB 生成的代理做优化(比如使用更激进的字节码策略),但它只在 CGLIB 路径下生效。实际项目里我一般只设proxyTargetClass,optimize很少动,因为优化带来的收益有限,反而可能引入兼容性问题。

3. 代理创建的核心流程与实操要点

3.1 getProxy 方法到底做了什么

getProxy()是ProxyFactory最常用的入口,它内部调用createAopProxy().getProxy()。整个流程可以拆成两大步:决定代理策略和生成代理对象。

第一步,createAopProxy()会委托给AopProxyFactory,默认实现是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); }

这段逻辑有几个关键判断点,我逐个解释:

  • isOptimize()或isProxyTargetClass()为 true:说明用户明确要求用 CGLIB,进入 CGLIB 分支。
  • hasNoUserSuppliedProxyInterfaces()为 true:说明没有指定任何接口,JDK 动态代理无法工作(它必须基于接口),所以也只能走 CGLIB。
  • 目标类是接口或已经是代理类:即使要求 CGLIB,也退回 JDK 代理。因为接口本身无法被继承增强,已经是代理类的话再 CGLIB 会出问题。

第二步,拿到AopProxy实例后调用getProxy()。JDK 路径会调Proxy.newProxyInstance,CGLIB 路径会配置Enhancer并生成子类。两条路径最终都会把AdvisedSupport里的通知链信息传进去,供方法调用时使用。

3.2 JDK 代理与 CGLIB 代理的选型实战

选型这件事,理论上一句话:有接口用 JDK,没接口用 CGLIB。但实际项目里要考虑的因素更多。

先看一个我踩过的坑。某次我把proxyTargetClass设成 true,想强制用 CGLIB,结果目标类里有个final方法,增强死活不生效。原因是 CGLIB 通过继承生成子类,final方法无法被重写,自然也就无法织入通知。这个坑在 JDK 代理里不存在,因为 JDK 代理是基于接口的,跟方法是不是 final 没关系。

再看另一个场景。目标类实现了接口,但接口方法签名和实现类不完全一致(比如实现类多了一些 public 方法)。如果用 JDK 代理,代理对象只能调用接口里声明的方法,实现类独有的方法调不到。这时候就得用 CGLIB,让代理类继承实现类,才能覆盖所有 public 方法。

我把选型要点整理成一张表:

场景推荐策略原因
目标类实现了接口,且只需增强接口方法JDK 代理轻量、无第三方依赖、生成快
目标类没有接口CGLIBJDK 代理无法工作
需要增强实现类独有方法CGLIBJDK 代理只能看到接口方法
目标方法被final修饰JDK 代理CGLIB 无法重写 final 方法
目标类是final类JDK 代理CGLIB 无法继承 final 类
对性能极度敏感、代理对象创建频繁JDK 代理CGLIB 生成字节码开销更大

提示:Spring Boot 2.x 之后默认proxyTargetClass=true,也就是默认走 CGLIB。这是为了支持@Transactional标注在类上、以及避免接口注入问题。如果你的项目依赖 JDK 代理的某些特性,记得显式配置spring.aop.proxy-target-class=false。

3.3 通知链是怎么组装出来的

代理对象创建好之后,方法调用时怎么知道该执行哪些通知?这就要靠AdvisorChainFactory。ProxyFactory在创建AopProxy时,会把AdvisedSupport传进去,后者持有AdvisorChainFactory(默认DefaultAdvisorChainFactory)。

当代理方法被调用时,JdkDynamicAopProxy.invoke或CglibAopProxy的拦截器会调用AdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice,把当前方法匹配的Advisor转换成MethodInterceptor列表。

这个转换过程有几个细节值得注意:

  • 切点匹配:每个Advisor里的Pointcut会判断当前方法是否匹配,不匹配的直接跳过。
  • 通知适配:Advisor里的Advice会被AdvisorAdapter适配成MethodInterceptor。比如BeforeAdvice会被MethodBeforeAdviceAdapter包装。
  • 顺序排序:多个Advisor会按照Ordered接口或@Order注解排序,决定通知的执行顺序。

组装好的拦截器链会被缓存起来(CachedIntrospectionResults类似机制),避免每次调用都重新计算。这也是为什么 Spring AOP 在方法调用层面的性能损耗相对可控。

3.4 exposeProxy 到底解决什么问题

exposeProxy这个配置我第一次看到时完全不知道干嘛用的,直到遇到"同类方法调用不走代理"的问题才明白。

假设有个 Service 类,方法 A 调用了同类的方法 B,而方法 B 上标注了@Transactional。直接调this.B()是不会走代理的,因为this是原始对象,不是代理对象,事务自然不生效。解决办法之一就是开启exposeProxy,然后通过AopContext.currentProxy()拿到当前代理对象,再调用((Service) AopContext.currentProxy()).B()。

开启方式很简单:

// 注解方式 @EnableAspectJAutoProxy(exposeProxy = true) // XML 方式 <aop:aspectj-autoproxy expose-proxy="true"/>

但这里有个坑:AopContext底层用的是ThreadLocal,如果方法调用跨线程,子线程里拿不到代理对象。另外,开启exposeProxy会带来轻微的性能开销,因为每次调用都要往ThreadLocal里塞代理对象。所以我的建议是:能用设计规避就用设计规避(比如把方法 B 抽到另一个 Bean 里),实在不行再开exposeProxy。

4. 手写一个 ProxyFactory 实操案例

4.1 场景设定与目标

光看源码容易飘,我写一个不依赖 Spring 容器的纯手工案例,用ProxyFactory给一个普通对象织入日志和耗时统计。这样能最直观地看到ProxyFactory的每个 API 在干什么。

场景:有一个订单查询服务,我想在不修改原代码的前提下,给它的查询方法加上"入参日志"和"耗时统计"两个增强。

4.2 代码实现与逐行注释

先定义业务接口和实现:

public interface OrderService { String queryOrder(Long orderId); } public class OrderServiceImpl implements OrderService { @Override public String queryOrder(Long orderId) { // 模拟业务耗时 try { Thread.sleep(50); } catch (InterruptedException e) { } return "订单-" + orderId; } }

接着定义两个通知。这里用MethodInterceptor(来自 AOP 联盟包),因为它最灵活:

import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; // 日志通知 public class LogInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("[日志] 调用方法: " + invocation.getMethod().getName() + ", 参数: " + java.util.Arrays.toString(invocation.getArguments())); return invocation.proceed(); } } // 耗时通知 public class TimeInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { long start = System.currentTimeMillis(); try { return invocation.proceed(); } finally { System.out.println("[耗时] " + invocation.getMethod().getName() + " 执行了 " + (System.currentTimeMillis() - start) + " ms"); } } }

最后用ProxyFactory组装:

import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.NameMatchMethodPointcut; public class Main { public static void main(String[] args) { // 1. 创建目标对象 OrderService target = new OrderServiceImpl(); // 2. 创建 ProxyFactory,设置目标和接口 ProxyFactory factory = new ProxyFactory(); factory.setTarget(target); factory.setInterfaces(OrderService.class); // 3. 构造切点:只匹配 queryOrder 方法 NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); pointcut.addMethodName("queryOrder"); // 4. 组装 Advisor 并加入工厂 factory.addAdvisor(new DefaultPointcutAdvisor(pointcut, new LogInterceptor())); factory.addAdvisor(new DefaultPointcutAdvisor(pointcut, new TimeInterceptor())); // 5. 生成代理对象 OrderService proxy = (OrderService) factory.getProxy(); // 6. 调用 String result = proxy.queryOrder(1001L); System.out.println("返回结果: " + result); } }

运行结果大致是:

[日志] 调用方法: queryOrder, 参数: [1001] [耗时] queryOrder 执行了 52 ms 返回结果: 订单-1001

4.3 关键参数与执行顺序验证

上面代码里有几个点值得展开。

第一,setInterfaces的作用。如果不设置接口,ProxyFactory会走 CGLIB 分支。这里显式设置了OrderService.class,所以走的是 JDK 代理。你可以把这一行注释掉再跑一次,会发现代理类变成了OrderServiceImpl的子类,输出结果一样,但底层机制完全不同。

第二,通知的执行顺序。我加了两个 Advisor,日志在前、耗时在后。实际输出里日志先打印,耗时后打印,说明拦截器链是按添加顺序执行的。如果想让耗时通知包住日志通知,把添加顺序反过来即可。这里涉及一个常见误区:很多人以为@Order能控制一切,其实对于手动添加的 Advisor,添加顺序才是决定因素,@Order只在自动扫描切面时起作用。

第三,NameMatchMethodPointcut的匹配规则。它按方法名精确匹配,addMethodName("queryOrder")只匹配这个名字。如果想用通配符,可以用JdkRegexpMethodPointcut或者AspectJExpressionPointcut。后者支持完整的 AspectJ 切点表达式,功能最强,但解析开销也最大。

注意:手动addAdvisor时,如果 Advisor 没有实现Ordered接口,排序完全依赖添加顺序。生产环境里如果通知顺序很重要,建议给 Advisor 显式实现Ordered或继承AbstractPointcutAdvisor并设置 order 值。

4.4 从手工案例反推 Spring 自动代理

手工案例跑通后,再回头看 Spring 的自动代理就清晰多了。AnnotationAwareAspectJAutoProxyCreator本质上就是在 Bean 初始化后,帮我们做了上面那些手工步骤:解析切面得到 Advisor 列表、创建 ProxyFactory、设置 target 和 interfaces、添加 Advisor、调用 getProxy、把代理对象放回容器。

区别在于,自动代理会处理更多边界情况:比如多个切面的排序、@Order的解析、循环依赖的处理、exposeProxy的自动设置等。但核心骨架跟我们的手工案例一模一样。理解了手工流程,再看自动代理的源码就不会迷路。

5. 常见问题与排查技巧实录

5.1 增强不生效的排查清单

增强不生效是最高频的问题,我整理了一个排查顺序,基本能覆盖 90% 的场景:

排查项检查方法常见原因
切点是否匹配打印切点表达式,用AopUtils.canApply验证表达式写错、包路径不对
代理是否创建打印AopUtils.isAopProxy(bean)目标类没被后置处理器扫描到
代理类型是否正确打印AopUtils.isJdkDynamicProxy/isCglibProxy接口缺失或proxyTargetClass配置冲突
方法是否可被增强检查是否final、static、privateCGLIB 无法重写 final 方法
调用是否走代理检查是否this调用同类内部调用绕过代理
通知是否注册打印Advised.getAdvisors()切面类没加@Aspect或没被扫描

这个清单我实际用过很多次,按顺序查下来基本都能定位。其中"同类内部调用"和"final 方法"是最隐蔽的两个坑,因为它们不会报错,只是静默失效。

5.2 循环依赖下代理创建的坑

Spring 解决循环依赖靠的是三级缓存,其中第二级缓存earlySingletonObjects存的就是提前暴露的代理对象。这里有个细节:如果 A 依赖 B,B 依赖 A,且 A 需要被代理,那么 A 的代理对象会在 A 完全初始化之前就被创建出来。

这个机制带来的问题是:如果 A 的代理配置依赖于某个还没初始化的 Bean,就可能拿到不完整的配置。我遇到过一次,切面里注入了另一个 Bean,而那个 Bean 又依赖被代理的目标对象,结果切面里的依赖是 null。解决办法是给切面依赖加@Lazy,延迟到真正调用时再解析。

另一个相关配置是allowCircularReferences。Spring Boot 2.6 之后默认禁止循环依赖,如果你的项目升级后突然报循环依赖错误,要么重构代码消除循环,要么显式开启这个开关。但我的建议是优先重构,循环依赖本身就是设计问题。

5.3 代理对象类型转换异常

ClassCastException在 AOP 场景里也很常见,典型报错是"cannot be cast to XXX"。原因通常是:目标类实现了接口,但代码里试图把代理对象强转成实现类。

比如:

OrderService proxy = (OrderService) factory.getProxy(); OrderServiceImpl impl = (OrderServiceImpl) proxy; // 这里会抛异常

如果走的是 JDK 代理,代理对象只实现了OrderService接口,跟OrderServiceImpl没有继承关系,强转必然失败。解决办法有两个:要么强制用 CGLIB(setProxyTargetClass(true)),要么在代码里面向接口编程,不要强转实现类。

这个坑在依赖注入时也会出现。如果你用@Autowired注入实现类类型,而代理是 JDK 代理,就会注入失败。Spring 4.3 之后支持按接口注入,所以最佳实践是面向接口注入,从根源上避免这个问题。

5.4 性能相关的经验数据

关于 JDK 代理和 CGLIB 的性能,网上说法很多,我分享几个实测数据(仅代表我自己的测试环境,不同 JDK 版本和场景会有差异):

  • 代理对象创建:JDK 代理平均比 CGLIB 快 3 到 5 倍。因为 CGLIB 需要生成字节码并加载类,开销明显更大。
  • 方法调用:在 JDK 8 之后,两者差距已经很小,JDK 代理甚至在某些场景下更快。早期"JDK 代理调用慢"的说法已经不太成立。
  • 内存占用:CGLIB 生成的代理类会占用 Metaspace,如果代理类数量巨大(比如成千上万个不同的目标类),Metaspace 压力会比较明显。

基于这些数据,我的建议是:不要为了性能刻意选 JDK 代理,优先根据功能需求选型。如果功能上两者都满足,再考虑性能。大多数业务系统里,AOP 调用的性能损耗相对于业务逻辑本身可以忽略不计。

提示:如果确实遇到 AOP 性能瓶颈,先别急着换代理方式,优先检查切点表达式是否过于宽泛。一个execution(* *(..))的切点会匹配所有方法,导致每次调用都要走完整的匹配流程,这才是真正的性能杀手。

6. 几个容易被忽略的高级用法

6.1 用 TargetSource 实现动态目标切换

前面提过TargetSource,这里展开讲一个实用场景:灰度发布。假设你有一个服务,需要根据请求头里的用户标识,把请求路由到不同版本的目标对象上。用TargetSource可以优雅实现:

public class GrayTargetSource implements TargetSource { private final Object stableVersion; private final Object grayVersion; public GrayTargetSource(Object stableVersion, Object grayVersion) { this.stableVersion = stableVersion; this.grayVersion = grayVersion; } @Override public Class<?> getTargetClass() { return stableVersion.getClass(); } @Override public boolean isStatic() { return false; } @Override public Object getTarget() { // 根据上下文决定返回哪个版本 return isGrayUser() ? grayVersion : stableVersion; } @Override public void releaseTarget(Object target) { // 无需释放 } private boolean isGrayUser() { // 实际场景从 ThreadLocal 或请求上下文获取 return false; } }

然后把它设置到ProxyFactory:

factory.setTargetSource(new GrayTargetSource(stableObj, grayObj));

这样每次方法调用时,TargetSource.getTarget()都会被调用,实现动态路由。这个用法比在业务代码里写 if-else 优雅得多,而且对业务代码零侵入。

6.2 冻结配置防止运行时篡改

AdvisedSupport提供了setFrozen(true)方法,冻结之后任何修改 Advisor 的操作都会抛AopConfigException。这个功能在框架开发里很有用,可以防止下游代码意外修改代理配置。

factory.setFrozen(true); factory.addAdvisor(...); // 这里会抛异常

我一般只在编写基础框架时用这个,业务代码里很少需要。但如果你在做一个被多方复用的组件,冻结配置能避免很多"莫名其妙增强失效"的问题。

6.3 用 Introduction 给对象动态添加接口

Spring AOP 除了方法增强,还支持Introduction(引入),可以在运行时给对象添加新的接口实现。这个功能用ProxyFactory实现起来很直接:

// 定义一个要引入的接口和实现 public interface Auditable { void audit(); } public class AuditableImpl implements Auditable { @Override public void audit() { System.out.println("执行审计逻辑"); } } // 通过 IntroductionAdvisor 引入 DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor(new AuditableImpl()); factory.addAdvisor(advisor); // 代理对象现在可以强转成 Auditable Auditable auditable = (Auditable) factory.getProxy(); auditable.audit();

这个用法在需要给一批对象统一添加某个能力(比如审计、标记)时特别方便,比继承或组合更灵活。不过要注意,Introduction只能添加接口,不能添加类,因为 Java 是单继承的。

7. 我个人的一些实操体会

ProxyFactory这个类我前前后后读了好几遍,每次都有新收获。最开始只关注getProxy()怎么用,后来慢慢理解了AdvisedSupport的配置管理、AopProxyFactory的策略选择、AdvisorChainFactory的链式组装,才算真正把 Spring AOP 的骨架摸清楚。

如果你也想深入这块,我的建议是:先跑通手工案例,再对照自动代理源码。手工案例能让你看清每个 API 的职责,自动代理源码能让你理解 Spring 是怎么把这些 API 串起来的。两者结合,比单纯读源码效率高得多。

另外,遇到 AOP 相关的问题,别急着搜答案,先按"切点匹配、代理创建、方法调用"三段式排查。大部分问题都能通过打印AopUtils.isAopProxy、Advised.getAdvisors()、AopUtils.getTargetClass这几个信息定位到。这几个方法我几乎每次排查都会用到,建议你也记下来。

最后分享一个小技巧:调试 AOP 问题时,可以在DefaultAdvisorChainFactory的getInterceptorsAndDynamicInterceptionAdvice方法上打断点,看看当前方法到底匹配到了哪些拦截器。这个方法能直接告诉你"为什么这个通知没执行",比看日志快得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询