ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Spring AOP 核心原理:ProxyFactory 代理创建与选型机制

Spring AOP 核心原理:ProxyFactory 代理创建与选型机制 1. 从一次代理失效的排查说起去年帮一个朋友排查线上问题现象很怪一个订单服务里明明加了Transactional日志里也看到事务拦截器被调用了但数据库里就是没回滚。他一开始怀疑是数据库引擎的问题查了半天 InnoDB 配置又怀疑是异常被吞了加了一堆try-catch打印折腾到凌晨两点。最后我让他把断点打在ProxyFactory的getProxy()方法上一看就明白了——他那个 Bean 是通过new出来的压根没走 Spring 容器代理对象根本没生成事务拦截器自然形同虚设。这个坑其实特别典型。很多人用 Spring AOP 用了好几年Aspect、Around、Transactional写得很溜但一旦遇到代理没生效切面顺序不对this 调用不走切面这类问题就抓瞎了。根因在于大家只记住了注解怎么用却没搞清楚这些注解背后到底是谁在干活。而那个真正干活的核心角色就是ProxyFactory。ProxyFactory是 Spring AOP 里最被低估的一个类。它不像Aspect那样天天出现在业务代码里也不像BeanPostProcessor那样在面试题里被反复背诵但它是整个 Spring AOP 创建代理对象的总调度室。你写的每一个切面、每一个通知、每一个Transactional最终都要经过它才能变成一个能拦截方法调用的代理对象。理解ProxyFactory等于拿到了 Spring AOP 源码的钥匙——你能看懂代理是怎么选的、通知是怎么排的、拦截器链是怎么串起来的遇到问题也能自己定位而不是靠猜。这篇内容适合两类人一类是已经会用 Spring AOP但想搞清楚底层原理的中高级开发者另一类是正在读 Spring 源码被ProxyFactory、AdvisedSupport、JdkDynamicAopProxy这些类绕晕的人。我会从ProxyFactory的定位讲起拆解它创建代理的完整链路重点讲清楚 JDK 动态代理和 CGLIB 的选型逻辑、通知链的构建过程以及实际使用中最容易踩的几个坑。全程结合源码但不会贴大段看不懂的代码而是告诉你每一行代码在解决什么问题。2. ProxyFactory 到底站在 Spring AOP 的哪个位置2.1 它不是给业务用的是给框架自己用的先纠正一个认知偏差。很多人以为ProxyFactory是给开发者手动创建代理用的工具类这个理解只对了一半。它确实可以手动用但它的主要服务对象是 Spring 框架自身。Aspect注解的解析、Transactional的代理创建、Cacheable的缓存拦截底层统统都是通过ProxyFactory来生成代理对象的。你可以把ProxyFactory理解成一个代理工厂的标准化流水线。它把我要给某个对象加代理这件事拆成了几个标准步骤收集通知Advice、确定目标对象TargetSource、选择代理方式JDK 还是 CGLIB、生成代理对象。Spring 的各种 AOP 功能本质上都是在往这条流水线上投喂不同的原料。从类图上看ProxyFactory继承自ProxyCreatorSupport而ProxyCreatorSupport又继承自AdvisedSupport。这条继承链很关键AdvisedSupport负责持有目标对象、通知列表、拦截器列表以及一个AopProxyFactory。ProxyCreatorSupport负责管理Advisor列表并提供createAopProxy()方法来决定用哪种代理。ProxyFactory则是最外层的门面把上面这些能力包装成好用的 API。所以当你调用proxyFactory.getProxy()的时候实际上是沿着这条继承链一路往上最终由ProxyCreatorSupport决定代理策略由JdkDynamicAopProxy或CglibAopProxy完成实际的字节码生成。2.2 和 AspectJ、Aspect 的关系别搞混这里必须澄清一个高频误解Spring AOP 和 AspectJ 不是一回事。AspectJ 是一套完整的 AOP 解决方案有自己的编译器和织入机制能在编译期、类加载期织入切面。而 Spring AOP 是运行时织入它不修改字节码文件而是在运行时动态生成代理对象。Aspect这个注解虽然名字来自 AspectJ但在 Spring 里它只是一个标记告诉 Spring这个类里定义了切面逻辑。真正解析这个注解、把里面的Before、After、Around转换成Advice对象的是 Spring 自己的AnnotationAwareAspectJAutoProxyCreator。而这个 Creator 最终创建代理时用的还是ProxyFactory。换句话说Aspect是语法糖ProxyFactory才是发动机。你写Around的时候觉得很优雅但 Spring 在背后做的事情是解析注解、生成AspectJMethodBeforeAdvice之类的通知对象、塞进ProxyFactory的Advisor列表、然后调用getProxy()生成代理。理解了这条链路你就知道为什么有些切面写法会失效——因为你的写法根本没被解析成Advisor自然也就进不了ProxyFactory的流水线。2.3 一个最小可运行的 ProxyFactory 示例光讲概念太虚直接上一个能跑的例子。假设我们有一个简单的服务接口和实现public interface OrderService { void createOrder(String orderId); } public class OrderServiceImpl implements OrderService { Override public void createOrder(String orderId) { System.out.println(创建订单: orderId); } }现在用ProxyFactory手动给它加一个方法执行前的通知import org.springframework.aop.MethodBeforeAdvice; import org.springframework.aop.framework.ProxyFactory; import java.lang.reflect.Method; public class ProxyFactoryDemo { public static void main(String[] args) { OrderServiceImpl target new OrderServiceImpl(); ProxyFactory proxyFactory new ProxyFactory(); proxyFactory.setTarget(target); proxyFactory.addAdvice(new MethodBeforeAdvice() { Override public void before(Method method, Object[] args, Object target) { System.out.println(方法执行前拦截: method.getName()); } }); OrderService proxy (OrderService) proxyFactory.getProxy(); proxy.createOrder(ORDER-001); } }运行结果会先打印方法执行前拦截: createOrder再打印创建订单: ORDER-001。这个例子虽然简单但它完整走了一遍ProxyFactory的核心流程设置目标、添加通知、生成代理、调用方法。后面要讲的所有源码细节都是在这个骨架上展开的。注意这个例子默认走的是 JDK 动态代理因为OrderServiceImpl实现了接口。如果你把接口去掉只留实现类ProxyFactory会自动切换到 CGLIB。这个切换逻辑就是下一节要重点讲的。3. JDK 动态代理还是 CGLIBProxyFactory 的选型逻辑3.1 选型发生在 createAopProxy 方法里代理方式的选择不是随机的也不是你随便配的而是由ProxyCreatorSupport.createAopProxy()方法根据一组条件计算出来的。核心判断逻辑在DefaultAopProxyFactory里简化后的逻辑是这样的public AopProxy createAopProxy(AdvisedSupport config) { 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); } }这段代码信息量很大我拆开讲。三个条件只要满足任意一个就会进入可能用 CGLIB的分支isOptimize()是否开启了优化模式这个一般不用除非你明确知道自己在做什么。isProxyTargetClass()是否强制代理目标类。EnableAspectJAutoProxy(proxyTargetClass true)或者 Spring Boot 2.x 之后默认的spring.aop.proxy-target-classtrue都会让这个值为 true。hasNoUserSuppliedProxyInterfaces()目标对象没有实现任何用户自定义接口。进入分支后还要再看一眼目标类本身如果目标类本身就是个接口或者已经是个代理类那还是用 JDK 动态代理否则才用 CGLIB。3.2 为什么 Spring Boot 默认改成了 CGLIB这是很多人困惑的点。Spring 早期默认用 JDK 动态代理但从 Spring Boot 2.x 开始默认改成了 CGLIB。原因其实很实际JDK 动态代理要求目标类必须实现接口而现实中有大量业务类是不实现接口的比如各种XxxService直接就是 class。如果默认用 JDK 代理这些类就没法被代理开发者得手动加接口或者改配置体验很差。CGLIB 通过继承目标类来生成子类代理不要求接口适用面更广。代价是它不能代理final类和方法因为继承不了。所以 Spring Boot 选择 CGLIB 作为默认是在通用性和限制之间做的权衡。这里有个实操经验如果你的项目里大量使用了接口 实现类的分层结构其实用 JDK 动态代理更轻量生成的代理类更小启动也更快。可以在配置里显式关掉 CGLIBspring: aop: proxy-target-class: false但要注意一旦关掉所有没实现接口的 Bean 就无法被 AOP 代理了Transactional会静默失效。这个坑我见过不止一次排查起来很费劲因为不报错只是不生效。3.3 两种代理方式的性能与限制对比对比维度JDK 动态代理CGLIB代理前提目标类必须实现接口目标类不能是 final方法不能是 final代理机制实现相同接口委托调用继承目标类覆写方法生成速度较快较慢需要生成子类字节码调用速度反射调用略慢直接调用略快代理类数量每个接口一个代理类每个目标类一个代理类典型失效场景目标类无接口目标类是 final 或方法 final关于调用速度网上很多文章说 CGLIB 比 JDK 快这个结论要加限定条件。在 JDK 8 之后JDK 动态代理的性能已经大幅优化两者差距很小。而且 Spring 在 CGLIB 代理里也做了缓存实际业务中这点差异基本可以忽略。选型时更应该关注的是能不能代理而不是哪个快。还有一个容易被忽略的点CGLIB 代理会创建目标类的子类这意味着目标类的构造函数会被调用两次一次是原始 Bean一次是代理子类。如果你的构造函数里有副作用比如注册监听器、启动线程可能会出问题。这个坑在写一些有状态的服务时特别容易踩。4. 通知链是怎么一步步构建起来的4.1 从 addAdvice 到拦截器链的转换ProxyFactory提供了addAdvice()和addAdvisor()两个方法看起来差不多但内部处理逻辑不同。addAdvice()添加的是裸的Advice对象Spring 会把它包装成一个DefaultPointcutAdvisorpointcut 是Pointcut.TRUE也就是匹配所有方法。而addAdvisor()添加的是已经带好 pointcut 的Advisor匹配逻辑由你自己控制。真正关键的是AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice()这个方法。它做的事情是遍历所有Advisor用AdvisorChainFactory把每个Advisor转换成MethodInterceptor最终形成一个拦截器链。这个转换过程有个细节如果一个Advisor的 pointcut 是动态的比如基于运行时参数判断那它会被包装成InterceptorAndDynamicMethodMatcher在调用时还要再做一次匹配。为什么要转成MethodInterceptor因为MethodInterceptor是 AOP 联盟定义的标准接口只有一个invoke(MethodInvocation invocation)方法。所有的通知类型——前置、后置、环绕、异常、返回——最终都会被适配成MethodInterceptor。比如MethodBeforeAdvice会被MethodBeforeAdviceInterceptor包装AfterReturningAdvice会被AfterReturningAdviceInterceptor包装。这样整个链路上跑的都是统一类型调用逻辑就简化了。4.2 拦截器链的执行顺序与责任链模式拦截器链构建好之后执行时用的是责任链模式。以JdkDynamicAopProxy.invoke()为例核心逻辑是public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodInvocation invocation new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); return invocation.proceed(); }ReflectiveMethodInvocation.proceed()是责任链的核心。它维护了一个currentInterceptorIndex游标每次调用proceed()就把游标加一取出下一个拦截器执行。如果游标已经到链尾就直接反射调用目标方法。每个拦截器在执行完自己的逻辑后可以选择继续调用invocation.proceed()也可以直接返回从而中断链路。这个设计带来的一个重要特性是拦截器的顺序决定了通知的执行顺序。Around通知如果排在前面它的proceed()之前的代码会在所有后续通知之前执行proceed()之后的代码会在所有后续通知之后执行。这就是为什么Order注解能控制切面顺序——它影响的是Advisor在列表里的排序进而影响拦截器链的顺序。4.3 一个容易搞错的顺序问题很多人以为Before一定在Around之前执行其实不一定。执行顺序取决于Advisor的排序。如果Around切面的Order值更小它会先执行它的proceed()之前的部分会先于Before执行。我见过一个案例有人在Around里做了参数校验在Before里做了日志记录结果发现日志里记录的参数是校验前的原始参数而不是校验后修改的参数。原因就是Around的 order 更小先执行了proceed()之后才轮到Before。要控制这个顺序最稳妥的方式是显式给切面加Order并且理解order 越小越靠外这个规则。靠外的切面其proceed()之前的逻辑最先执行proceed()之后的逻辑最后执行。可以把它想象成洋葱最外层的是 order 最小的切面。5. 手动使用 ProxyFactory 时最容易踩的四个坑5.1 目标对象为 null 导致的空指针ProxyFactory允许你先设置通知再设置目标但如果在getProxy()之前目标还是 null生成的代理在调用方法时会抛NullPointerException。更隐蔽的情况是你设置了TargetSource但TargetSource.getTarget()返回 null。这种情况在池化目标对象比如PoolingTargetSource时可能出现池空了就会返回 null。排查这类问题的技巧是在getProxy()之前打印一下proxyFactory.getTargetSource().getTarget()确认目标对象确实存在。如果用的是自定义TargetSource要确保getTarget()方法在目标不可用时抛出明确异常而不是返回 null。5.2 代理对象被再次代理导致的通知重复这个坑在手动使用ProxyFactory时特别常见。假设你有一个已经代理过的对象又把它作为目标传给一个新的ProxyFactory那最终生成的代理会包含两层拦截器链通知会执行两次。在 Spring 容器里AbstractAutoProxyCreator会通过AopUtils.isAopProxy()判断对象是否已经是代理避免重复代理。但手动使用时没有这层保护。判断一个对象是否已经是代理可以用AopUtils.isAopProxy(obj)或AopUtils.isJdkDynamicProxy(obj)、AopUtils.isCglibProxy(obj)。在手动创建代理前加一道判断能避免很多诡异问题。5.3 this 调用不走代理的经典问题这个问题在 Spring 容器里也常见但手动用ProxyFactory时更容易理解。假设目标类里有两个方法a()和b()a()内部调用了this.b()。当你通过代理对象调用a()时a()的调用会被拦截但a()内部通过this调用的b()不会走代理因为this指向的是原始目标对象不是代理对象。解决办法有三种一是把b()的调用改成通过代理对象调用注入自身或AopContext.currentProxy()二是用AspectJ的编译期织入三是重新设计把b()的逻辑抽到另一个 Bean 里。第一种方案需要开启exposeProxyproxyFactory.setExposeProxy(true);然后在代码里用AopContext.currentProxy()获取当前代理。但要注意exposeProxy会把代理对象存到ThreadLocal里用完必须清理否则会有内存泄漏风险。Spring 在invoke()方法里用try-finally保证了清理手动使用时也要注意这一点。5.4 通知里抛异常导致的事务回滚失效这个坑和ProxyFactory本身关系不大但和拦截器链的执行密切相关。如果Around通知里捕获了异常但没有重新抛出那后续的Transactional拦截器就感知不到异常事务不会回滚。因为事务回滚依赖的是异常传播到TransactionInterceptor如果异常在更外层的切面被吞了事务拦截器就以为一切正常。正确的做法是如果Around里需要处理异常要么处理后重新抛出要么明确知道自己在做什么。我一般建议在Around里只做日志和监控不要吞异常。如果确实需要转换异常类型要确保转换后的异常是RuntimeException或者配置了rollbackFor。6. 从源码看代理对象的生成时机与缓存6.1 getProxy 方法做了什么ProxyFactory.getProxy()最终调用的是ProxyCreatorSupport.createAopProxy()拿到AopProxy后调用其getProxy()方法。以JdkDynamicAopProxy为例它的getProxy()逻辑是public Object getProxy(ClassLoader classLoader) { Class?[] proxiedInterfaces AopProxyUtils.completeProxiedInterfaces(this.advised, true); findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); }关键在completeProxiedInterfaces()它会收集目标对象实现的所有接口再加上 Spring 自己需要的SpringProxy、Advised、DecoratingProxy这几个接口。为什么要加这几个接口因为 Spring 需要通过它们来判断一个对象是不是代理、获取代理的Advisor列表、获取最终的目标类。这也是为什么你可以把一个代理对象强转成Advised然后调用getAdvisors()查看它有哪些通知。6.2 代理类的缓存机制JDK 动态代理本身有缓存Proxy.newProxyInstance()生成的代理类会被缓存在WeakCache里key 是类加载器和接口数组。CGLIB 也有缓存Spring 在CglibAopProxy里用MapClass?, Constructor?缓存了代理类的构造函数避免每次都重新生成。但要注意这个缓存是代理类级别的不是代理对象级别的。每次调用getProxy()都会创建一个新的代理对象只是复用了代理类的字节码。所以如果你在循环里反复调用getProxy()会创建大量代理对象虽然代理类只生成一次但对象创建本身也有开销。正确的做法是代理对象创建一次缓存起来复用。在 Spring 容器里这个缓存由AbstractAutoProxyCreator的proxyTypes和SingletonBeanRegistry共同保证每个单例 Bean 只会被代理一次。手动使用时你需要自己保证这一点。6.3 一个验证代理类型的实用方法调试 AOP 问题时快速确认一个对象用的是哪种代理能省很多时间。可以写一个工具方法public static String getProxyType(Object obj) { if (AopUtils.isJdkDynamicProxy(obj)) { return JDK动态代理; } else if (AopUtils.isCglibProxy(obj)) { return CGLIB代理; } else if (AopUtils.isAopProxy(obj)) { return 其他AOP代理; } else { return 非代理对象; } }在排查为什么事务不生效时先打印一下 Bean 的代理类型如果显示非代理对象那基本可以确定是代理没生成问题出在配置或 Bean 的创建方式上而不是事务本身。这个技巧我用了很多次每次都能快速缩小排查范围。7. 几个真实场景下的排查思路7.1 切面不生效的通用排查链路遇到切面不生效我一般按这个顺序排查确认目标 Bean 是否被 Spring 管理。如果是new出来的或者通过Bean方法返回但没走容器代理不会生成。确认切面类是否被 Spring 扫描到。Aspect类需要是一个 Bean通常加Component。确认代理类型是否匹配。如果目标类没接口且用了 JDK 代理代理不会生成。确认 pointcut 表达式是否匹配。可以用AopUtils.canApply()手动验证。确认方法是否是final或static。这两种方法无法被 CGLIB 代理。确认调用是否走了代理。this调用不走代理。这个链路覆盖了 90% 以上的切面失效场景。按顺序排查基本都能定位到根因。7.2 循环依赖与代理的相互影响Spring 解决循环依赖时会提前暴露一个早期引用。如果这个 Bean 需要被 AOP 代理早期引用暴露的是代理对象还是原始对象答案是如果代理创建发生在postProcessAfterInitialization阶段那早期引用暴露的是原始对象后续注入到其他 Bean 里的是代理对象这会导致同一个 Bean 在不同地方类型不一致。Spring 通过getEarlyBeanReference()方法解决了这个问题在提前暴露引用时如果 Bean 需要被代理就先创建代理对象并暴露出去。这样其他 Bean 注入到的就是代理对象保持一致。理解这一点对排查循环依赖相关的 AOP 问题很有帮助。7.3 性能敏感场景下的代理选择在高频调用的场景下代理带来的开销不能忽略。JDK 动态代理每次调用都要走反射CGLIB 虽然直接调用但代理类的生成和类加载也有成本。如果某个方法每秒被调用几十万次代理的开销可能会显现出来。优化思路有几个一是尽量缩小切面的匹配范围只拦截必要的方法减少拦截器链的长度二是对于性能极度敏感的方法考虑不用 AOP直接手写装饰器三是用Around时避免在通知里做重操作比如每次都查数据库。我见过一个案例有人在Around里每次都打印完整的方法参数和返回值日志量巨大导致性能下降了一半。把日志级别调高或者改成采样打印后性能恢复正常。8. 我个人的几点使用体会ProxyFactory这个类我建议每个用 Spring 的开发者都花时间读一遍源码。它不长核心逻辑就那么几百行但它是理解 Spring AOP 的最佳入口。读懂了它你再去看Transactional、Cacheable、Async这些注解的实现会发现它们都是同一套模式的变体解析注解生成Advisor塞进ProxyFactory生成代理拦截器链执行。手动使用ProxyFactory的场景其实不多主要是在写框架扩展、自定义 AOP 逻辑、或者做单元测试时。但即使不手动用理解它的工作原理也能帮你快速定位问题。我自己的习惯是遇到 AOP 相关的诡异问题先在getProxy()和invoke()两个方法上打断点看代理有没有生成、拦截器链里有哪些拦截器、执行顺序是什么。这两个断点基本能解决大部分问题。最后分享一个小技巧如果你想看某个 Bean 最终被哪些Advisor代理了可以把它强转成Advised然后打印getAdvisors()。这个方法在排查为什么这个切面没生效时特别有用能直接看到实际生效的Advisor列表而不是靠猜。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表