多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

手写Spring AOP:从JDK动态代理到拦截器链的完整原理与实战

手写Spring AOP:从JDK动态代理到拦截器链的完整原理与实战 Spring AOP天天写注解一加事务、日志、权限全都变成“隐形”的。可真让你离开Spring环境自己动手做一版手写Spring AOP很多平时觉得理所当然的东西会瞬间露馅。我围绕Spring 6.0把AOP的原理重新梳理了一遍又按照源码思路手写了一个简化版最后发现动态代理、切面解析、拦截器链这些概念全部串通了。这篇内容适合两类人一类是天天用Aspect却没看过Spring底层源码的Java开发者另一类是面试时总被“AOP实现原理——JDK动态代理”和“CGLIB动态代理”问住的后端。如果你愿意花40分钟做一个能跑通的最小版本之后再去看Spring Framework里的ProxyFactory和Advisor会轻松很多。我常觉得Spring AOP就像一块魔法馅饼你只看到外层注解和切入点表达式却不知道中间那层代理到底怎么生成的。手写过一次之后你会发现Spring AOP的核心其实就三个词代理、链、匹配。真动手写并没有想象中那么复杂。1. 手写Spring AOP的整体设计与思路拆解想做一个Mini版本的Spring AOP第一件事不是写代码而是想清楚容器里到底需要哪些组件它们之间怎么配合。1.1 为什么我坚持让每个Spring开发者手写一次AOP之前面试过一个自称“Spring应用很熟”的候选人。他能把注解的事务、缓存、异步讲得头头是道但当我问“代理对象的创建发生在Bean生命周期的哪一步”时他愣了半天。这不怪他日常业务开发里几乎没有任何机会触碰到藏在底层的AopProxy对象。手写一次Spring AOP最大的价值是把“魔法”变成“有迹可循”。你会在过程中发现Spring是在Bean实例化并初始化完成之后通过BeanPostProcessor的postProcessAfterInitialization拿到Bean实例再根据切面表达式决定要不要为这个Bean生成代理对象。代理对象内部维护着一条拦截器链外部调用方法时链上的通知一个一个执行最后才落到目标方法本身。这种“底层感觉”才是真正值钱的东西。之后你再排查Transactional失效、切面不执行、循环依赖导致代理失效时不用靠猜直接在心里预演一遍流程就能定位。所以“手写Spring”历来都是进阶源码最好的路径没有之一。1.2 先拆好三张牌Pointcut、Advice、Advisor各司其职动手之前必须把三个核心概念放对位置否则代码会越写越乱。Pointcut负责描述“在哪里切”。在Spring里最常见的切入点是execution表达式通过方法修饰符、返回类型、类名、方法名、参数列表来锁定目标方法。手写的时候可以把表达式简化成“类名方法名”的匹配。Advice负责描述“切进去之后做什么”。Spring中有Before、AfterReturning、AfterThrowing、Around四种通知但在运行时最终都会被适配成MethodInterceptor这种回调形式这样才能纳入统一的拦截器链。Advisor负责把Pointcut和Advice组合成一个整体。当一个Aspect切面类被解析时Spring会把其中每一条通知方法都封装成一个Advisor放到缓存列表里。使用Advisor而不是同时保留散落的Pointcut和Advice是为了让代码好组织没有Advisor你扫描到的只是一堆零散方法有了它才形成“在哪个点执行什么动作”的完整入口。1.3 Spring 6.0对AOP底层带来了哪些新变化Spring 6.0是一个大版本基础要求直接升到JDK 17。对AOP而言底层必须支持最新的字节码版本所以Spring对ASM和CGLIB做了重新整合。原来项目需要单独引入cglib依赖但在Spring 6.0中CGLIB相关的类比如org.springframework.cglib.proxy.Enhancer被直接放进了spring-core模块基础依赖少了一个Jar包冲突问题也少了一类。代理创建的逻辑在Spring 6.0里也做了收敛整体走AopProxyFactory加AopProxy的路径。DefaultAopProxyFactory会根据AdvisedSupport配置判断默认情况下目标类实现了接口就用JDK动态代理如果设置proxyTargetClass为true或者目标类没有接口就用CGLIB。有一个细节很容易被忽略Spring Boot在自动配置AOP时默认把proxyTargetClass设成了true所以Boot环境下的绝大多数Service走的是CGLIB。手写的时候要模拟这个“选择逻辑”不能只实现一种动态代理否则遇到没有接口的类时直接暴露短板。2. 动态代理AOP的地基AOP能无侵入地为业务代码加逻辑靠的不是直接修改源码而是用代理对象把目标对象包起来外部调用的其实是代理对象。手写Spring AOP前至少要能写出JDK动态代理和CGLIB两种方案中的一种最好两种都写一遍。2.1 JDK动态代理要求目标有接口靠InvocationHandler转发JDK动态代理是java.lang.reflect包下官方提供的方案。它要求目标类必须实现至少一个接口运行时为接口生成代理类。核心是InvocationHandler所有对代理对象方法的调用都会进入handler的invoke方法。一个最小可用示例import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class SimpleJdkProxy { public static Object create(Object target, SimpleCallback callback) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return callback.invoke(target, method, args); } }); } }在上面的代码里SimpleCallback是一个简单的回调接口。Spring AOP中的ReflectiveMethodInvocation承担的就是这个回调的完整逻辑它持有当前方法、参数、目标对象以及一整条拦截器链。每调用一次proceed就在链上往后走一个节点直到所有通知执行完再通过method.invoke(target, args)触发真实方法。JDK动态代理有两个局限第一是目标类必须实现接口第二是通过反射调用方法相对慢一点。但它的优点是生成速度快、对字节码没有强依赖代理类会被JDK缓存复用所以在很多场景之下反而是更稳的选择。2.2 CGLIB动态代理绕过接口限制直接生成目标子类CGLIB完全不依赖接口。它用ASM字节码技术在运行时生成目标类的一个子类并重写目标类的方法。重写后的方法里先回调MethodInterceptor再由用户决定是否调用父类方法。典型写法如下import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; public class SimpleCglibProxy { public static Object create(Class? targetClass, MethodInterceptor callback) { Enhancer enhancer new Enhancer(); enhancer.setSuperclass(targetClass); enhancer.setCallback(callback); return enhancer.create(); } }需要注意的是CGLIB和final是“死对头”final类无法被继承final方法无法被重写。如果目标类里存在final方法切面对该方法不会生效这是很多人排查半天才发现的原因。Spring在生成代理时会尽量识别这些情况但手写时你必须要自己清楚。在Spring 6.0中CGLIB相关代码被整合进spring-core使用的类名也不是net.sf.cglib.proxy.Enhancer而是org.springframework.cglib.proxy.Enhancer。这点在排查“ClassNotFoundException: Enhancer”时特别有用。2.3 代理选择策略和Spring 6.0的默认做法手写Spring AOP时还要模拟Spring的选择逻辑。最简单的写法是public class MiniProxyFactory { public static Object createProxy(Object target, ListMiniAdvisor advisors) { boolean hasInterface target.getClass().getInterfaces().length 0 !target.getClass().isInterface(); if (hasInterface) { return new JdkProxyFactory(target, advisors).getProxy(); } return new CglibProxyFactory(target, advisors).getProxy(); } }Spring保留两种动态代理并非“选择困难”而是各有适用场景。JDK动态代理只对接口方法生效没办法对目标类的具体方法完全控制CGLIB能代理具体类更多灵活性但代价是生成并加载字节码。Spring 6.0里DefaultAopProxyFactory的判断逻辑也是先看AdvisedSupport配置再看目标类接口情况最后决定走哪条路。3. 手写一个能跑通的最小AOP框架现在进入正题。下面这一版手写实现我不准备去抄Spring的完整源码而是按照它的核心链路做一个代码量可控、便于理解的Mini版本。3.1 先定义切入点接口和简单表达式匹配Spring的Pointcut分ClassFilter和MethodMatcher两个层次。为了演示链路我只定义一个方法匹配接口public interface MiniPointcut { boolean matches(Method method, Class? targetClass); }然后做一个简单的表达式切点按类名前缀和方法名前缀匹配public class PatternPointcut implements MiniPointcut { private final String classPattern; private final String methodPattern; public PatternPointcut(String classPattern, String methodPattern) { this.classPattern classPattern; this.methodPattern methodPattern; } Override public boolean matches(Method method, Class? targetClass) { if (method null) { return false; } return targetClass.getName().startsWith(classPattern) method.getName().startsWith(methodPattern); } }这个实现非常粗糙真正的AspectJExpressionPointcut会解析execution表达式、处理通配符和注解匹配。但它的核心职责已经体现出来了决定某个Bean的方法到底要不要被代理。表达式规则掌握越细后面拓展MiniAOP就越顺手。3.2 实现拦截器链这是整个手写过程最精髓的部分Spring AOP运行时不是简单地把几个代理方法嵌套起来它维护的是一条有序的拦截器链。手写关键部分如下public interface MiniMethodInvocation { Object proceed() throws Throwable; } public interface MiniMethodInterceptor { Object invoke(MiniMethodInvocation invocation) throws Throwable; }接着是链路执行器public class ReflectiveMethodInvocation implements MiniMethodInvocation { private final ListMiniAdvisor advisors; private final Object target; private final Method method; private final Object[] args; private int currentIndex -1; public ReflectiveMethodInvocation(ListMiniAdvisor advisors, Object target, Method method, Object[] args) { this.advisors advisors; this.target target; this.method method; this.args args; } Override public Object proceed() throws Throwable { currentIndex; if (currentIndex advisors.size()) { return method.invoke(target, args); } MiniAdvisor advisor advisors.get(currentIndex); return ((MiniMethodInterceptor) advisor.getAdvice()).invoke(this); } }你仔细看proceed方法整条链的推进逻辑都在这里。第一次调用proceed时currentIndex从-1变成0执行第一个Advisor的通知如果这个通知内部再次调用proceedcurrentIndex变成1继续执行第二个通知直到currentIndex等于advisors.size()所有通知都执行完毕此时才触发目标方法。Around通知之所以有“一票否决权”就是因为它可以决定在什么时机、甚至是否调用proceed。前置通知会先输出内容再调用proceed后置通知则等proceed返回之后再做动作。理解了这段代码你基本就理解了Spring AOP整个执行骨架。这也是手写Spring AOP时最值得反复琢磨的一段。3.3 用JDK和CGLIB分别创建代理对象代理工厂需要把ReflectiveMethodInvocation和动态代理绑在一起。JDK方案public class JdkProxyFactory { private final Object target; private final ListMiniAdvisor advisors; public JdkProxyFactory(Object target, ListMiniAdvisor advisors) { this.target target; this.advisors advisors; } public Object getProxy() { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) - new ReflectiveMethodInvocation(advisors, target, method, args).proceed() ); } }CGLIB方案import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; public class CglibProxyFactory { private final Object target; private final ListMiniAdvisor advisors; public CglibProxyFactory(Object target, ListMiniAdvisor advisors) { this.target target; this.advisors advisors; } public Object getProxy() { Enhancer enhancer new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback(new MethodInterceptor() { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return new ReflectiveMethodInvocation(advisors, target, method, args).proceed(); } }); return enhancer.create(); } }注意CGLIB生成的代理对象虽然是目标类的子类但调用时控制权在代理对象手上。如果你强行打印obj.getClass()看到的是代理类而不是目标类如果只把目标类对象传进去不配合MethodProxy或反射父类方法其实是不会自动被调用的这正好解释了为什么我们需要拦截器链最后去触发method.invoke(target, args)。3.4 用BeanPostProcessor把代理挂进容器手写出代理工厂之后还差最后一步怎么让普通业务Bean自动变成代理。这里必须依靠BeanPostProcessor。在Spring容器中每个Bean实例化和初始化之后都会经过BeanPostProcessor的postProcessAfterInitialization方法。我们在这个方法里扫描已经解析好的Advisor列表匹配当前Bean的类和方法如果匹配成功就返回代理对象替换原来的Bean。public class MiniAspectJAutoProxyCreator implements BeanPostProcessor { private final ListMiniAdvisor advisors new ArrayList(); public void registerAdvisor(MiniAdvisor advisor) { this.advisors.add(advisor); } Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { ListMiniAdvisor matchAdvisors new ArrayList(); for (MiniAdvisor advisor : advisors) { MiniPointcut pointcut advisor.getPointcut(); Method[] methods bean.getClass().getMethods(); for (Method method : methods) { if (pointcut.matches(method, bean.getClass())) { matchAdvisors.add(advisor); break; } } } if (matchAdvisors.isEmpty()) { return bean; } return MiniProxyFactory.createProxy(bean, matchAdvisors); } }这里已经能看出一条完整的自动化链路解析切面、构建Advisor、匹配Bean、生成代理、替换原Bean。有个很关键的细节是postProcessAfterInitialization阶段返回的代理对象会被放回单例池之后其他Bean依赖注入时拿到的都是代理对象。这个设计保证了外部调用统一走代理而目标对象内部的字段状态依然保留在原实例上。为了让这个手写AOP真正跑在Spring容器里把它注册成一个普通Bean即可Configuration public class MiniAopConfig { Bean public MiniAspectJAutoProxyCreator miniAspectJAutoProxyCreator() { return new MiniAspectJAutoProxyCreator(); } }启动一个AnnotationConfigApplicationContext包一个Aspect切面类进来再注入一个普通Service调用方法后就能在控制台看到通知日志。整个过程花不了多少时间但看到日志的那一刻你会有一种“原来是这么回事”的确认感。3.5 再补一个简化版的切面解析器如果把Spring的Aspect交给手写的框架处理就需要能解析注解。这里我做一个最精简的解析public class MiniAspectJAdvisorFactory { public ListMiniAdvisor parse(Object aspectInstance) { ListMiniAdvisor advisors new ArrayList(); Method[] methods aspectInstance.getClass().getDeclaredMethods(); for (Method m : methods) { if (m.isAnnotationPresent(MiniBefore.class)) { MiniBefore before m.getAnnotation(MiniBefore.class); MiniPointcut pointcut new PatternPointcut(before.classPattern(), before.methodPattern()); MiniAdvice advice new MinitBeforeAdvice(aspectInstance, m); advisors.add(new DefaultMiniAdvisor(pointcut, advice)); } } return advisors; } }为了让代码尽量简单我只创建了MiniBefore但你可以照葫芦画瓢扩展MiniAfter、MiniAround。核心不是注解种类多全而是让Advisor从切面类中解析出来的过程足够清晰。4. 进阶细节切面匹配、代理缓存与执行顺序手写版本跑通之后真正要贴合Spring的行为还要处理几个很容易忽略的细节。4.1 方法级匹配与类级匹配要分开处理Spring的Pointcut在设计上分ClassFilter和MethodMatcher先做类级过滤再做方法级过滤。这样可以避免对不相关的类遍历所有方法。手写时如果只做类匹配类上能用而具体方法上不能用的切面也会被织入导致结果不符合预期。严谨的做法是在创建代理前遍历目标类的所有方法并调用方法匹配器在类过滤阶段就快速剔除一批无关类两处配合性能和正确性都对。4.2 代理缓存对单例Bean很重要Spring容器里大部分Bean都是单例每次调用都重新生成代理对象会带来极大的性能浪费。真实实现里AopProxyFactory拿到一次代理后会缓存。手写版本虽然不需要完整做性能优化但建议在MiniProxyFactory里用一个ConcurrentHashMap缓存class到代理对象的映射。这样一来只有第一次创建代理时会触发字节码生成或动态代理类加载后续直接复用。4.3 多个切面的执行顺序是一个高频面试题当多个切面同时命中同一个方法时执行顺序取决于切面的Order值。Spring会收集实现了Ordered接口或者标注了Order的切面数字越小优先级越高。在拦截器链上前置逻辑按顺序执行后置逻辑则逆序执行形成一种洋葱式的嵌套。手写版本里实现顺序也非常直白给MiniAdvisor增加一个order字段调用ReflectiveMethodInvocation之前对Advisor列表排序。面试时你如果能把这个顺序和拦截器链的关系讲清楚比背一堆概念强太多。5. 我在手写过程中踩过的坑与排查实录这部分是真正的经验沉淀。我建议你在做自己的手写实现时故意踩一遍这些坑经历过一次比背多少遍概念都管用。5.1 自调用失效为什么this.xxx()永远切不到最常见的现象在一个被代理的Service里一个方法调用同类中另一个方法另一个方法的切面却不生效。原因很简单AOP代理是外部容器注入并持有的但方法内部使用this调用时这个this指向的是目标对象而不是代理对象。切面逻辑是挂在代理对象上的this调用直接绕过代理当然切不到。解决办法有几种通过ApplicationContext重新getBean获取代理对象再调用把被调用方法移到另一个类或者使用AopContext.currentProxy()显式获取代理。手写过程中你一定会撞到这个坑撞一次就终生难忘。5.2 目标类没有接口却坚持用JDK代理会抛ClassCastException有些手写实现为了省事不管有没有接口一律JDK动态代理。如果目标类没有实现任何接口代理对象和目标对象压根不属于同一个类型层次强行转回目标类时会直接抛ClassCastException。修起来也简单创建代理前判断目标类是否有接口没有接口就走CGLIB。Spring Boot里由于默认proxyTargetClass为true所以很少遇到这个问题但手写代码时你必须自己判断。5.3 循环依赖场景下AOP代理被绕过Spring解决单例Bean循环依赖靠三级缓存提前暴露对象引用。如果两个Bean互相依赖且都有切面可能出现一个Bean在AOP步骤尚未执行时就被提前暴露导致注入到对方的是未代理对象AOP因此失效。Spring 6.0对这套机制处理得更加保守官方文档也一直在强调循环依赖本身是一种坏味道应该通过构造器注入或重新设计依赖来绕开。排查思路上如果发现一个Service调用另一个Service时明明查了切面却不生效可以先怀疑是不是循环依赖导致提前暴露。5.4 手写AOP常见问题速查表现象原因排查建议切面完全不生效BeanPostProcessor没有注册或切面解析时机不对确认postProcessAfterInitialization被触发且Advisor已塞入列表代理后强转目标类失败代理方式选择错误可能没接口却用了JDK代理创建前判断接口数量无接口走CGLIB拦截器链无限递归proceed里currentIndex没有正确递增检查索引边界和advisors.size()判断同对象内部方法未被增强this调用目标对象而非代理对象通过容器获取代理对象或拆分方法到另一个类多个切面顺序不符合预期没有按Order排序给切面增加Order信息并在代理创建前排序final方法无法拦截CGLIB无法重写final方法将方法改为非final或改用接口由JDK代理到这里手写Spring AOP从设计到实现再到踩坑排查整个闭环已经完整了。我个人更想说的是手写一版不是要重新造一个框架而是要你亲手揭开Spring的扩展点。当你以后再遇到Spring内部机制相关的问题时平时积累的这套手写经验会第一时间跳出来告诉你去AopProxy里找答案还是去Advisor链上抓现场。这份MiniAOP值得一直保留它是你理解Spring容器和动态代理之间的粘合剂也是以后排查各种切面问题时最趁手的底层脚手架。
返回列表