尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入Spring AOP源码:从代理机制到拦截器链的实战解析

深入Spring AOP源码:从代理机制到拦截器链的实战解析 1. 从一次线上故障说起为什么必须懂AOP源码那天晚上系统监控突然报警一个核心交易接口的耗时从平时的50毫秒飙升至5秒。我们紧急排查日志显示业务逻辑本身执行很快问题出在一个记录方法入参和返回值的“审计切面”上。这个切面为了将数据序列化成JSON存入数据库在参数对象嵌套过深时JSON序列化过程意外地触发了对象的延迟加载Lazy Loading导致大量额外的数据库查询最终拖垮了接口。这个切面代码看起来人畜无害就是用Around注解包了一下方法里面调用了Jackson的ObjectMapper。问题根源不在于切面逻辑写错了而在于我们对Spring AOP面向切面编程的代理机制理解不够深——我们不知道在代理方法执行时传入的ProceedingJoinPoint中的参数对象到底处于什么状态更不清楚动态代理是如何与Spring的容器生命周期、事务管理等其他特性交互的。这次经历让我下定决心必须把Spring AOP的源码扒开看个究竟。很多人觉得会用Aspect、Before、After注解就够了但当你需要实现精细的权限控制、处理复杂的异常、优化性能或者像我一样踩进深坑时仅仅停留在API层面是远远不够的。理解源码你才能预判行为、精准调试、设计出健壮高效的切面。今天我就带你深入Spring AOP的核心看看Around注解背后那一行行简洁的切面表达式是如何被编织成精密的代理逻辑的。2. AOP的核心抽象Spring如何定义一次“拦截”在深入代码之前我们必须统一认知Spring AOP的本质是什么它不是一种魔法而是一套基于代理的、模块化横切关注点的设计范式。其核心抽象决定了整个框架的行为边界和能力上限。2.1 连接点、切点与通知三位一体的拦截模型Spring AOP从AOP联盟借鉴并完善了三个核心概念Joinpoint连接点、Pointcut切点和Advice通知。在源码中它们是所有行为的基石。Joinpoint连接点在Spring AOP的语境下连接点特指方法的执行。这一点非常重要也是Spring AOP与更强大的AspectJ在能力上的主要区别AspectJ还支持字段访问、构造器调用等连接点。源码中的org.springframework.aop.Joinpoint接口定义非常简单主要提供了获取当前拦截目标getTarget、获取参数getArgs和继续执行proceed的能力。我们常用的ProceedingJoinPoint是其子接口专门用于Around通知增加了proceed(Object[] args)方法允许我们修改参数后再传递。Pointcut切点这是决定“在何处拦截”的谓词。源码中org.springframework.aop.Pointcut接口是策略模式的经典应用它主要包含两个部分ClassFilter用于过滤目标类。例如execution(* com.example.service.*.*(..))中的com.example.service.*就对应此类过滤。MethodMatcher用于过滤目标方法。这是切点的核心它又分为静态匹配和动态匹配。静态匹配isRuntime() false仅基于方法签名和类信息判断性能高是Before、After等通知的默认选择。动态匹配isRuntime() true则需要在每次方法调用时都进行判断可以访问具体的调用参数Around通知中如果切点表达式使用了args、args、annotation等绑定运行时参数的指示符就会启用动态匹配这也是性能陷阱之一。Advice通知这是“拦截后要做什么”的具体动作。Spring将其建模为拦截器Interceptor所有通知类型最终都会被包装成一个MethodInterceptor。Before对应MethodBeforeAdviceInterceptorAfterReturning对应AfterReturningAdviceInterceptorAfterThrowing对应ThrowsAdviceInterceptor而最强大的Around通知其对应的方法本身就被要求返回一个ProceedingJoinPoint - Object的lambda表达式这个lambda在内部直接被适配成了一个MethodInterceptor。这种统一到MethodInterceptor的设计使得各种通知能够被组织成一个调用链Interceptor Chain顺序执行。2.2 Advisor与Aspect组织拦截逻辑的两种维度有了点Pointcut和动作AdviceSpring提供了两种方式将它们组合起来。Advisor这是Spring AOP最基础的组合单元一个Advisor就是一个Advice加上一个决定其生效范围的Pointcut。PointcutAdvisor是其最常用的子接口。在早期的Spring AOP编程中我们可能需要手动创建Advisor并注册到容器。Aspect这是我们更熟悉的方式通过Aspect注解标注的一个普通Java类。一个Aspect类中可以定义多个通知方法Before,After等。在源码处理过程中Spring会解析这个类为其中的每一个通知方法生成一个对应的Advisor。所以一个Aspect注解的类在运行时等价于多个Advisor的集合。这种设计让声明式AOP变得极其简洁。理解这层抽象至关重要。当你在一个Aspect类里写了三个Before通知Spring在启动时会通过后处理器AnnotationAwareAspectJAutoProxyCreator扫描它们解析每个方法的切点表达式然后生成三个独立的Advisor对象。这些Advisor最终会被应用到符合条件的Bean上形成代理。3. 代理的诞生JDK动态代理与CGLIB的抉择与创建这是Spring AOP最核心的“魔法”发生地。Spring并不会直接修改你的字节码而是通过创建目标对象的代理对象来实现拦截。代理对象持有目标对象Target的引用并在其方法被调用时插入我们定义的拦截逻辑。这里有两个主角JDK动态代理和CGLIB。3.1 两种代理机制的原理与本质区别很多人知道“实现接口就用JDK否则用CGLIB”但背后的原因和差异远不止于此。JDK动态代理原理基于Java原生的java.lang.reflect.Proxy类。在运行时动态生成一个实现了指定接口列表的新类通常名为$Proxy0、$Proxy1。创建过程你需要提供一个InvocationHandler。当代理实例上的方法被调用时调用会被路由到InvocationHandler.invoke()方法。在Spring中这个InvocationHandler就是JdkDynamicAopProxy类它内部维护了前面提到的Advisor拦截器链。关键限制只能代理接口中定义的方法。如果你的目标类有一个public方法不在接口中那么通过JDK代理是无法调用到这个方法的。这也是为什么Spring默认对实现了接口的类使用JDK代理——它只关心接口契约。性能在Java 8及以后版本中JDK动态代理的性能已经非常优秀生成代理类较快。由于是基于接口某些虚拟机优化可能更友好。CGLIB代理原理通过操作字节码生成目标类的一个子类。这个子类重写了父类中所有非final的方法。创建过程使用Enhancer类创建代理需要设置回调过滤器CallbackFilter和回调Callback。在Spring中核心回调是DynamicAdvisedInterceptor它同样持有一个拦截器链。CGLIB通过继承的方式可以代理那些没有实现接口的类的任意方法只要方法不是final。关键能力可以代理类自身的方法。这是它与JDK代理最根本的区别。正因为是继承所以它无法代理final类或final方法。性能早期版本生成代理类较慢且生成的类体积较大。但在多次调用时由于其直接调用重写的方法性能可能与JDK代理相当甚至在某些场景下更优。现代Spring和CGLIB版本已经做了大量优化。选择策略的源码级解读在DefaultAopProxyFactory.createAopProxy()方法中决策逻辑清晰可见如果目标类实现了接口且不是Proxy类且未强制指定使用CGLIB即proxyTargetClassfalse则使用JDK动态代理。如果目标类未实现接口或者显式设置了proxyTargetClasstrue则使用CGLIB。 设置proxyTargetClasstrue意味着“我要代理这个类本身而不是它的接口”这在需要拦截类内部方法调用例如通过this调用的另一个方法时是必须的因为JDK代理基于接口this引用指向的是目标对象本身而非代理对象因此走代理链。3.2 代理对象的创建流程AbstractAutoProxyCreator的核心作用Spring通过BeanPostProcessor机制在Bean初始化后介入创建代理。AbstractAutoProxyCreator是这个过程的统帅。初始化后回调当某个Bean初始化完成后AbstractAutoProxyCreator.postProcessAfterInitialization()方法会被调用。判断是否需要代理该方法首先检查该Bean是否应该被跳过例如是否是AOP基础设施类自身然后调用getAdvicesAndAdvisorsForBean()方法。这是一个模板方法其子类如AnnotationAwareAspectJAutoProxyCreator会在这里完成核心工作扫描容器中所有的Advisor包括从Aspect类解析出来的判断当前Bean是否符合任何一个Advisor的切点Pointcut要求。创建代理如果找到匹配的Advisor则调用createProxy()方法。这个方法会将匹配到的所有Advisor整理成一个拦截器链ListAdvisor。根据前面讲的策略JDK or CGLIB创建一个AopProxy实例JdkDynamicAopProxy或ObjenesisCglibAopProxy。调用aopProxy.getProxy(classLoader)生成最终的代理对象并返回它替换掉原来的原始Bean对象。从此以后容器中注入的以及Autowired进来的就是这个代理对象了。你对它的任何方法调用都会首先经过那条拦截器链。4. 拦截器链的执行一次方法调用的微观旅程当代理对象的方法被调用时真正的拦截开始了。这是理解切面执行顺序、参数传递和异常处理的关键。4.1 责任链模式的完美演绎ReflectiveMethodInvocation无论JDK代理还是CGLIB代理最终都会将调用导向一个核心类ReflectiveMethodInvocation或其CGLIB对应的子类CglibMethodInvocation。这个对象封装了一次方法调用的所有上下文目标对象、方法、参数、拦截器链等。它本身就是一个责任链Chain of Responsibility模式的实现。我们以JDK动态代理为例看下JdkDynamicAopProxy.invoke()方法的核心简化逻辑public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 获取适用于当前方法的拦截器链 ListObject chain this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); // 2. 如果链为空没有切面匹配此方法则直接反射调用目标方法 if (chain.isEmpty()) { return method.invoke(target, args); } // 3. 创建方法调用对象并沿着链执行 MethodInvocation invocation new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); return invocation.proceed(); }关键在于ReflectiveMethodInvocation.proceed()方法public Object proceed() throws Throwable { // 1. 如果所有拦截器都执行完了则调用目标方法 if (this.currentInterceptorIndex this.interceptorsAndDynamicMethodMatchers.size() - 1) { return invokeJoinpoint(); } // 2. 获取下一个拦截器是一个MethodInterceptor Object interceptor this.interceptorsAndDynamicMethodMatchers.get(this.currentInterceptorIndex); // 3. 如果是动态切点匹配器需要运行时再次判断 if (interceptor instanceof InterceptorAndDynamicMethodMatcher) { InterceptorAndDynamicMethodMatcher dm (InterceptorAndDynamicMethodMatcher) interceptor; if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { return dm.interceptor.invoke(this); } else { // 动态匹配不通过跳过此拦截器继续下一个 return proceed(); } } else { // 4. 静态匹配或动态匹配已通过直接执行拦截器逻辑 return ((MethodInterceptor) interceptor).invoke(this); } }这个过程就像穿过一个有多道闸门的走廊拦截器链。proceed()方法就是“走到下一个闸门”的指令。每个拦截器MethodInterceptor的invoke方法内部会先执行自己的通知逻辑比如Before的操作然后在最后调用mi.proceed()将控制权交给链中的下一个拦截器。最后一个拦截器调用proceed()时才会最终抵达“走廊尽头”——执行原始目标方法。4.2 五种通知类型的执行顺序与内部实现理解了责任链五种通知的执行顺序就一目了然了。Spring按照通知类型在链中的位置来排序。对于一个给定的连接点执行顺序是Around通知如果有多则按优先级Before通知目标方法执行AfterReturning通知方法正常返回After通知无论正常返回还是异常类似finallyAfterThrowing通知仅当方法抛出异常时执行注意Around是最特殊的它必须手动调用joinPoint.proceed()来推动链的进行。如果它不调用那么后续的Before、目标方法以及所有After通知都不会执行。而Before和AfterReturning等通知它们的invoke方法内部已经帮你调用了proceed()。一个常见的误区很多人以为After是在AfterReturning或AfterThrowing之后执行。实际上在拦截器链中After对应的AspectJAfterAdvice被包装成一个MethodInterceptor它的invoke方法大致是这样的try { return mi.proceed(); // 先推动链执行即执行目标方法和其他通知 } finally { invokeAdviceMethod(joinPoint, null, null); // 最后在finally块中执行After逻辑 }所以After的逻辑虽然在finally里但因为它包裹了整个proceed()调用所以从外部看它的执行时机是在目标方法和其他AfterReturning/AfterThrowing之后。但因为它位于finally块所以即使后续通知或目标方法抛异常它也会执行。4.3 参数绑定与传递的陷阱回到文章开头的那个故障。在Around通知中我们通过joinPoint.getArgs()获取到的参数数组里面的对象引用直接指向原始参数。如果你修改了数组中的对象例如通过joinPoint.proceed(newArgs)传入新数组那么后续拦截器和目标方法收到的是新数组。但如果你没有调用proceed带参数的重载方法那么目标方法收到的仍然是原始参数对象的引用。关键陷阱当你直接操作joinPoint.getArgs()返回的对象时比如用Jackson序列化如果这个对象是Hibernate/JPA的实体且存在延迟加载的关联集合序列化器在遍历属性时会触发延迟加载导致N1查询问题。这不是Spring AOP的bug而是你对传入对象的状态假设有误。实操心得在编写涉及复杂对象操作的切面尤其是Around时一个最佳实践是除非必要否则不要修改原始参数对象本身更不要对其进行可能触发副作用的操作如序列化、克隆、深度遍历。如果必须操作考虑先进行防御性复制或者明确该操作可能带来的副作用如触发懒加载。对于入参出参日志一种更安全的方式是只记录关键字段或使用toString()方法需确保toString()不会触发懒加载。5. 高级特性与源码级调优掌握了核心流程我们再来看看那些让Spring AOP更强大、也更复杂的高级特性。5.1 引介Introduction为对象动态添加接口引介是AOP中一个特殊的概念它允许我们为现有的对象动态地实现新的接口从而添加新的状态和行为。Spring通过IntroductionInterceptor来实现。虽然不如普通通知常用但在一些特定场景例如为一批Bean统一添加一个Monitorable接口以暴露监控信息非常有用。在源码层面引介通知会被特殊处理它不拦截现有方法而是在代理类上直接添加新的接口和方法实现。5.2 热交换与动态切面ProxyFactory与Advised接口Spring AOP的代理对象都实现了Advised接口。这个接口是动态AOP的钥匙。通过它你可以在运行时获取和修改代理所持有的Advisor链。MyService proxy (MyService) context.getBean(myService); Advised advised (Advised) proxy; // 获取所有Advisor Advisor[] advisors advised.getAdvisors(); // 创建一个新的切点 AspectJExpressionPointcut pointcut new AspectJExpressionPointcut(); pointcut.setExpression(execution(* *.update*(..))); // 创建一个新的通知 MyCustomAdvice advice new MyCustomAdvice(); // 包装成Advisor并添加 DefaultPointcutAdvisor newAdvisor new DefaultPointcutAdvisor(pointcut, advice); advised.addAdvisor(0, newAdvisor); // 添加到链首这个能力非常强大可以实现不重启应用的情况下动态开启或关闭某个切面功能。但使用时要极其小心线程安全问题并且要清楚添加或移除Advisor会立即对所有后续的方法调用生效。5.3 性能调优要点切点表达式的代价尽量使用静态切点。避免在切点表达式中使用args、args、target、this、annotation(参数绑定除外)等需要运行时评估的指示符除非确有必要。动态切点匹配isRuntime() true会在每次方法调用时都执行匹配逻辑带来额外开销。proxyTargetClass的权衡除非你需要拦截通过this调用的内部方法或者目标类没有实现接口否则不要轻易设置proxyTargetClasstrue。使用JDK动态代理通常更轻量并且与基于接口的编程范式更契合。Advisor的数量应用于同一个方法的Advisor越多拦截器链就越长每次方法调用的开销就越大。定期审查切面合并功能相近的切面或者使用更精确的切点表达式减少匹配范围。理解Aspect的代理模式默认情况下Aspect类本身也是一个Spring Bean。如果这个Bean中的通知方法被其他切面拦截例如它上面也有Transactional可能会产生代理嵌套的复杂情况。通常我们将Aspect类定义为不需要代理的如使用Component但避免被其他AOP匹配或者使用aspectOf模式当使用AspectJ织入时。6. 与Spring其他核心机制的交互Spring AOP不是孤立的它与Spring容器的事务管理、异步处理、缓存等特性深度集成理解这些交互能避免很多诡异的问题。6.1 与Transactional的协作与陷阱Transactional本质上也是一个基于AOP的拦截器。当一个方法同时被业务切面和事务切面拦截时代理创建的顺序决定了拦截器链的顺序而顺序可能影响行为。假设我们有一个UserService.updateUser()方法它被一个记录日志的Around切面和一个Transactional切面包围。在代理链中如果日志切面在外层事务切面在内层。那么在日志切面的Around方法中你调用joinPoint.proceed()时才会进入事务切面事务在此刻开启。如果在日志切面里发生了异常事务根本不会开启。如果事务切面在外层日志切面在内层。那么事务会先开启然后进入日志切面。如果在日志切面里发生异常事务会检测到并回滚。如何控制顺序实现Ordered接口或使用Order注解。数值越小优先级越高切面越在外层。通常我们会把事务切面的优先级设得比较高即Ordered.HIGHEST_PRECEDENCE或较小的数值确保它在最外层这样事务的边界能包含所有其他业务操作。6.2 自调用问题this与代理对象这是Spring AOP以及任何基于代理的AOP最经典的坑。在同一个类中一个方法A调用另一个方法B如果方法B上有切面如Transactional这个切面不会生效。Service public class MyService { public void methodA() { this.methodB(); // 这里调用的this是目标对象本身不是代理对象 } Transactional public void methodB() { // 数据库操作 } }原因methodA通过代理对象调用时会进入拦截链。但在methodA的内部this指向的是真实的MyService实例目标对象而不是它的代理。因此this.methodB()是直接调用绕过了代理自然也就没有事务切面。解决方案推荐重构将methodB抽到另一个Service中通过注入调用。自注入在类里注入自己的代理Autowired private MyService self然后通过self.methodB()调用。但这种方式有循环依赖风险且代码不够优雅。使用AspectJ编译时/加载时织入这种方式会直接修改字节码不存在代理边界自调用问题自然解决。但会引入构建的复杂性。理解这个问题的根源就能明白为什么基于接口的编程和良好的职责划分如此重要——它不仅是为了“优雅”更是为了AOP等机制能正确工作。6.3 在Spring Boot中的自动化配置在Spring Boot中只要引入了spring-boot-starter-aop依赖EnableAspectJAutoProxy就已经自动配置好了并且默认设置了proxyTargetClasstrue。这是因为Spring Boot倾向于“约定大于配置”并且很多Spring Data JPA的Repository接口是通过CGLIB代理实现的。了解这一点很重要这意味着在Spring Boot项目中即使你的Service实现了接口默认情况下也会使用CGLIB代理。如果你希望改用JDK动态代理需要在配置类中显式地EnableAspectJAutoProxy(proxyTargetClass false)。扒开Spring AOP的源码最初可能只是为了解决一个具体的性能问题。但这个过程带来的价值远超预期。它让你对Spring容器的Bean生命周期、代理模式、设计模式的应用有了立体的认识。以后再看到Transactional不生效、切面顺序混乱、或者自调用失效这些问题时你脑子里能立刻浮现出那条拦截器链的执行流程图从AbstractAutoProxyCreator到ReflectiveMethodInvocation的每一个关键步骤都清晰可见。这种透过现象看本质的能力才是阅读源码最大的收获。当你自己设计一些需要横切关注点的功能时Spring AOP这套精妙的设计也会成为你最好的参考。
返回列表