在技术圈摸爬滚打这些年,面试Java岗基本绕不开AOP,特别是Spring里那套动态代理机制。网上讲原理的文章一抓一大把,但真正动手写过一遍的人少之又少。我今天把“手写Spring AOP”这件事完完整整拆开讲一遍,不仅讲原理,还给出可以直接跑起来的完整代码,帮你看透AOP从配置到代理生成再到方法拦截的完整链路。
这个内容适合三类人:一是准备面试、想深入理解Spring AOP底层机制的开发者;二是工作中被各种神秘代理问题困扰、想彻底搞懂动态代理差异的Java工程师;三是对Spring 6.0新特性有好奇心、想了解它底层AOP模块变化的技术爱好者。看懂这篇文章后,你自己就能徒手写一个可用的AOP框架核心,Spring里那些注解、通知、切点的运作逻辑会变得通透无比。
1. 手写前的核心认知:AOP的本质是代理模式
1.1 从“动态代理”这个元概念说起
很多人一提到Spring AOP就紧张,觉得这是个高深莫测的东西,连面试都背不利索。其实AOP的底层只有两句话:给目标对象生成一个代理对象,然后在代理对象里插入额外逻辑。就这么简单。你的业务类该干嘛还干嘛,代理类在调用你的业务方法前后,替你做日志、做鉴权、做事务,做完之后再把方法调用转交给真正的目标对象。
这里有个核心概念叫“动态代理”,意思是代理类不是在编译期写死的,而是在运行期根据你的配置现场生成的。Spring AOP支持两种动态代理方式:JDK动态代理和CGLIB动态代理。JDK动态代理要求目标类必须实现接口,它是通过Java原生的java.lang.reflect.Proxy类配合InvocationHandler实现的;CGLIB动态代理则是通过生成目标类的子类来实现的,不要求目标类实现接口。
我打个比方帮你在脑子里烙下这个模型:目标类是一台洗衣机,你想记录每次洗衣的开始和结束时间。JDK动态代理像是你站在洗衣机外面,按下开关前看一眼表、洗完后再看一眼表,洗衣机本身没变;CGLIB动态代理则像是你给洗衣机装了个智能改装套件,让它在内部流程里自动打点记录。不管哪种方式,洗衣的核心功能都没变,只是多了外部关注点的切入。
1.2 Spring AOP为什么要“织入”而不是直接改造业务代码
AOP的全称是Aspect Oriented Programming,面向切面编程。它的设计初衷是把横切逻辑从业务代码中剥离开。什么叫横切逻辑?就是那些散落在各个业务方法里、但又跟核心业务无关的通用逻辑,比如性能监控、日志记录、权限校验、事务管理、异常兜底。
如果不用AOP,你要在每个业务方法里重复写一遍监控代码,改一次监控逻辑就得把所有业务方法全改一遍,简直是灾难。AOP的思路是:让业务代码保持纯净,用“切面”统一处理横切逻辑。这样业务开发只需要关心业务,基础设施的研发只需要维护切面,两边解耦,互不干扰。
手写AOP的价值就在这:当你亲手把代理生成、通知调度、切点匹配这一整套东西写出来,你对Spring AOP的理解就不再是背书,而是真正内化为自己的知识体系。面试官问你“JDK动态代理和CGLIB动态代理的区别”时,你不光能说出“一个基于接口,一个基于继承”,还能现场画出代理对象生成的内存模型,甚至手写出核心代码,这种深度是纯背八股文给不了的。
1.3 手写Spring AOP的完整技术栈选型
我这次手写使用的是Java 17,正好对应Spring 6.0的基线版本。Spring 6.0要求JDK 17及以上,这个版本对反射、动态代理的API做了不少增强,比如强封装JDK内部API后,反射调用受限,但Spring自己封装了一套处理机制。手写时我们不需要用那些复杂的封装,直接用JDK原生API就能实现,这反而能让你更聚焦在核心机制而非外围框架上。
依赖方面只需要一个CGLIB库,如果你用JDK动态代理为主,连这个都不需要。不过既然是手写Spring AOP,我建议JDK动态代理和CGLIB两条路都写一遍,这样才能深刻理解Spring在什么场景下选哪种方案。
实操中我的项目结构是标准的Maven工程,JDK 17,核心依赖一个cglib,其他全部走JDK原生能力。整体模块分为:通知定义(Advice)、切点匹配(Pointcut)、代理工厂(ProxyFactory)、以及最后的测试验证。这套结构基本复刻了Spring AOP的模块划分,只是裁剪到只剩骨架,但骨架已经足以让你理解全貌。
2. 核心细节解析:JDK动态代理与CGLIB的底层差异
2.1 JDK动态代理:接口驱动的轻量方案
JDK动态代理的运作逻辑是这样的:当你调用一个实现了接口的类的业务方法时,Proxy.newProxyInstance()会生成一个实现了同一接口的代理类,这个代理类持有你的InvocationHandler,所有接口方法的调用都会先走到InvocationHandler.invoke()方法里。
关键代码只有一段,我直接给你看:
public class JdkProxyFactory { public static <T> T createProxy(T target, MethodInterceptor interceptor) { Class<?> targetClass = target.getClass(); return (T) Proxy.newProxyInstance( targetClass.getClassLoader(), targetClass.getInterfaces(), (proxy, method, args) -> interceptor.invoke(target, method, args) ); } }这里有个容易踩坑的细节:Proxy.newProxyInstance的第二个参数传的是targetClass.getInterfaces(),也就是说只有目标类声明实现了的接口才会被代理。如果你把实现类强转成接口类型调用业务方法,代理能生效;如果你持有的是实现类类型去调用getClass()这类Object方法时,代理的行为跟直接调用目标对象是有区别的。
JDK动态代理的内部有个重要的缓存机制:ProxyClassFactory会为每一组(类加载器、接口列表)生成一个缓存的代理类,同一个接口组合的代理类只会生成一次。所以你在循环里创建上百个代理对象,性能上不会有太大问题,类只生成一次,后续只是实例化对象。
Spring 6.0里JDK动态代理依然是最基础的方案,但有个值得关注的变化:Spring 6.0内部大量使用了JDK 17中增强的反射能力,同时自己维护了一套强封装处理逻辑。手写时我们用最简单的API即可,不需要也没必要去复刻Spring对JDK 18+的强封装适配。
2.2 CGLIB动态代理:继承机制的花式玩法
当目标类没有实现任何接口时,Spring就会走CGLIB这条路。CGLIB的原理是:在运行期生成目标类的一个子类,子类重写目标类的方法,在重写的方法里插入增强逻辑。所以CGLIB代理的对象,类型上跟目标类是父子关系,调用方拿到的代理对象可以赋值给目标类类型。
CGLIB核心代码:
public class CglibProxyFactory { public static Object createProxy(Object target, MethodInterceptor interceptor) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> interceptor.invoke(target, method, args)); return enhancer.create(); } }这里的设计思路是这样的:Enhancer设置了父类为目标类,然后注册一个MethodInterceptor回调,CGLIB生成的子类在方法调用时会回调这个方法。有个关键点必须注意:CGLIB无法代理final类,也无法代理final方法,因为final方法不能被重写。同样,static方法也不能被代理,静态方法是类级别的,不参与实例的继承体系。
CGLIB生成子类的方式是直接操作字节码,不需要目标类实现接口,所以相比JDK动态代理,它对类的侵入性更低,但代价是生成的字节码相对复杂,代理创建速度比JDK动态代理慢。不过一旦代理类生成并缓存之后,方法调用的开销差距就很小了。Spring里AnnotationAwareAspectJAutoProxyCreator的默认行为是:优先使用JDK动态代理,只有目标类没有接口时才转CGLIB。
2.3 Spring 6.0中CGLIB的地位变化
Spring 6.0发布之后有一个不太被大家关注但很重要的底层变化:Spring把CGLIB从独立的第三方库变成了Spring框架内核的一部分维护。在Spring 6.0之前,Spring用的是org.springframework.cglib包下的CGLIB重打包版本;6.0之后,Spring继续维护并更新了这个分支,支持了JDK 17以及后续版本,同时做了性能优化。
这个变化对你手写AOP有什么启示?就是别再用老版本的CGLIB了,如果要用就直接依赖Spring 6.0内置的那个版本。手写项目里我建议直接引最新的CGLIB主线版本,效果一致。从原理上说,你手写的CGLIB代理跟Spring 6.0内置的机制是同一套思路:生成子类、重写方法、回调拦截。
2.4 动态代理的选型对比速查表
我把JDK动态代理和CGLIB的核心区别整理成一张表,方便你们对比记忆:
| 对比维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 实现机制 | 基于接口,生成实现类 | 基于继承,生成子类 |
| 目标类要求 | 必须实现至少一个接口 | 不要求接口,但类和方法不能是final |
| 性能特点 | 创建速度快,反射调用稍有开销 | 创建稍慢,但字节码直接调用,调用性能略优 |
| 适用范围 | 有接口的Bean,主流场景 | 无接口的类,或需要代理具体类时 |
| Spring默认行为 | 目标类有接口时默认使用 | proxyTargetClass=true或无接口时使用 |
| 核心API | Proxy+InvocationHandler | Enhancer+MethodInterceptor |
这两种方案没有绝对的优劣,选型逻辑很简单:有接口走JDK,没有接口走CGLIB。Spring这样设计是为了兼容各种复杂的类继承关系,同时保证绝大数场景下性能最优。
3. 实操过程:手写一个可运行的AOP框架
前面讲完了原理,现在进入实战。我不会一上来就贴一大坨代码,而是跟着一个普通开发者的思路,从最简单的需求出发,一步步搭出整个框架。
3.1 定义通知类型:AOP的“横切逻辑”抽象
AOP里有一个基础概念叫“通知”(Advice),它表示在目标方法执行的什么时机、插入什么逻辑。Spring定义了五种通知:@Before前置通知、@AfterReturning返回后通知、@AfterThrowing异常后通知、@After最终通知、@Around环绕通知。
手写时我不建议一口气写五种,那样会贪多嚼不烂。我选择两个最核心的:前置通知和环绕通知,因为环绕通知能力最强,它能在方法调用前后都插入逻辑,甚至可以完全接管方法执行,前置通知则是最简单的切入点,能帮你理解链路。
先定义统一的通知接口:
public interface Advice { // 标记接口,没有任何方法,仅用于类型标识 } @FunctionalInterface public interface BeforeAdvice extends Advice { void before(Method method, Object[] args, Object target); } @FunctionalInterface public interface AroundAdvice extends Advice { Object around(ProceedingJoinPoint joinPoint) throws Throwable; }这里引入了一个ProceedingJoinPoint,它是环绕通知里的关键对象,封装了目标方法、目标对象、参数等信息,并且提供proceed()方法来放行目标方法的执行。它的设计是AOP的枢纽:所有通知的执行都是围绕这个joinPoint展开。
3.2 设计代理核心:拦截器链的调用机制
拿到通知之后,怎么把它跟目标方法绑定,还能在正确时机触发?我的思路是设计一个MethodInterceptor,它是整个代理工厂内部的核心调度器。
public interface MethodInterceptor { Object invoke(Object target, Method method, Object[] args) throws Throwable; }把这个接口跟前面的BeforeAdvice结合起来,就能实现最简单的前置通知调度。但真实场景下,一个方法可能对应多个通知,那就引入拦截器链的概念。拦截器链就像一串灯笼,每个灯笼代表一个通知,方法调用就像火种从第一个灯笼传到最后一个灯笼,最后才点亮目标方法。
public class ReflectiveMethodInvocation { private final Object target; private final Method method; private final Object[] args; private final List<Advice> advices; private int currentIndex = -1; public Object proceed() throws Throwable { if (currentIndex == advices.size() - 1) { return method.invoke(target, args); } Advice advice = advices.get(++currentIndex); if (advice instanceof AroundAdvice) { return ((AroundAdvice) advice).around(this); } else if (advice instanceof BeforeAdvice) { ((BeforeAdvice) advice).before(method, args, target); return proceed(); } return proceed(); } }这段代码是整个框架的“心脏”。你仔细看这个递归结构:proceed()方法每调用一次就往后取一个通知,如果是环绕通知就把this传给它的around方法,由它决定何时继续往下走;如果是前置通知就先执行增强逻辑,然后继续递归。当所有通知都执行完了,就调用method.invoke()真正执行目标方法。这个设计跟Spring的ReflectiveMethodInvocation是同一套逻辑,只不过Spring做了更多细节处理。
3.3 代理工厂实现:双引擎自动选择
有了拦截器链,就可以做代理工厂了。代理工厂的核心职责是:根据目标类是否实现了接口,决定用JDK动态代理还是CGLIB动态代理,然后把我们设计好的拦截器挂到代理上。
public class ProxyFactory { private Object target; private List<Advice> advices = new ArrayList<>(); public Object getProxy() { Class<?> targetClass = target.getClass(); if (targetClass.getInterfaces().length > 0) { return new JdkProxyFactory(target, advices).createProxy(); } return new CglibProxyFactory(target, advices).createProxy(); } }值得注意的是,这边的JDK代理工厂里,我们不是简单地在invoke里调用一个拦截器,而是把整个ReflectiveMethodInvocation链路接进去:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return new ReflectiveMethodInvocation(target, method, args, advices).proceed(); }CGLIB那边的实现也是类似的,只不过它处理的是MethodInterceptor回调,需要从MethodProxy那里获取真正的目标方法引用。CGLIB里有个细节:如果你在回调中直接用method.invoke(target, args),会再次触发CGLIB的拦截回调,导致无限递归。正确做法是用proxy.invokeSuper(obj, args)来调用父类的原始方法,这等价于走一遍代理链但不会再次触发同一个拦截器。
3.4 切点匹配:怎么指定“哪些方法要增强”
如果只是给所有方法都加增强,那跟直接改业务类没区别。AOP的优雅之处在于可以通过切点(Pointcut)精确控制增强范围。手写一个简单的切点匹配器,就像给方法加一个“过滤器”。
@FunctionalInterface public interface Pointcut { boolean matches(Method method, Class<?> targetClass); } public class AspectJExpressionPointcut implements Pointcut { private final Pattern pattern; public AspectJExpressionPointcut(String expression) { // 简单支持方法名通配符,如 *UserService.*、find* 等 this.pattern = Pattern.compile(expression.replace(".", "\\.").replace("*", ".*")); } @Override public boolean matches(Method method, Class<?> targetClass) { return pattern.matcher(method.getName()).matches(); } }真实的Spring AOP用的是AspectJ的表达式语言,支持execution(* com.example.service.*.*(..))这种复杂的表达式。我们这里只做一个简化版,用正则来匹配方法名,够用而且容易理解。在此基础上,把切点和通知绑定成Advisor:
public class Advisor { private Pointcut pointcut; private Advice advice; // getter/setter省略 }代理工厂在生成代理的时候,需要先扫描目标类的所有方法,用Advisor的切点判断该方法是否命中,命中的才把该Advice加入该方法的拦截器链。这一步如果做完整,就跟Spring的Advisor机制完全对齐了。
3.5 测试验证:跑一个完整示例
框架写完了,拿一个真实的业务场景来验证。我模拟一个订单服务,业务方法里就打印一句话,周围环绕一些切面逻辑。
public interface OrderService { void createOrder(Long orderId); void cancelOrder(Long orderId); } public class OrderServiceImpl implements OrderService { @Override public void createOrder(Long orderId) { System.out.println("业务方法:创建订单 " + orderId); } @Override public void cancelOrder(Long orderId) { System.out.println("业务方法:取消订单 " + orderId); } }装配切面,给createOrder方法加一个前置通知和一个环绕通知,取消订单方法不加任何通知:
ProxyFactory factory = new ProxyFactory(); OrderService target = new OrderServiceImpl(); factory.setTarget(target); factory.addAdvisor(new Advisor( new AspectJExpressionPointcut("*create*"), (BeforeAdvice) (method, args, obj) -> System.out.println("前置通知:准备创建订单"))); factory.addAdvisor(new Advisor( new AspectJExpressionPointcut("*create*"), (AroundAdvice) joinPoint -> { System.out.println("环绕通知:方法调用前,记录时间"); long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); System.out.println("环绕通知:方法调用后,耗时 " + (System.currentTimeMillis() - start) + "ms"); return result; })); OrderService proxy = (OrderService) factory.getProxy(); proxy.createOrder(10001L); proxy.cancelOrder(10002L);运行结果:
环绕通知:方法调用前,记录时间 前置通知:准备创建订单 业务方法:创建订单 10001 环绕通知:方法调用后,耗时 1ms 业务方法:取消订单 10002注意观察执行顺序:环绕通知先进入,然后它在内部调用了proceed(),这才触发前置通知,前置通知执行完之后递归进入业务方法。如果环绕通知里不调用proceed(),后续的前置通知和业务方法都不会执行,这就是环绕通知的“完全接管”能力。
整个手写过程到这里,你已经把Spring AOP最核心的机制完整走了一遍,包括通知抽象、拦截器链、动态代理生成、切点匹配。接下来我在Spring 6.0的语境下看看我们手写的跟Spring实现的差异和背后的设计考量。
4. 手写版与Spring 6.0 AOP的对比分析
4.1 Spring 6.0版本基线带来的连锁变化
Spring 6.0是Spring Framework的一次大版本升级,它的底线要求是JDK 17+。这个基线变化对AOP的影响是连锁的:JDK 17对反射API做了强封装处理,如果目标类是JDK内部类或使用了反射去访问私有成员,可能触发InaccessibleObjectException。Spring 6.0为此做了大量适配,内部维护了一套反射处理工具,我们手写时不需要这么全面,但要知道Spring的AOP为什么在新版本上如此强调兼容性。
另一个重要变化是Spring 6.0全面转向Jakarta EE命名空间,比如javax.annotation.Resource变成了jakarta.annotation.Resource,但AOP本身的注解还是org.aspectj.lang.annotation.Before、Around这些,没有变。这说明AOP的核心模型非常稳定,多年迭代下来依然是AspectJ风格注解加Spring的织入机制。
4.2 手写版对Spring设计的复刻程度
我把两者做一个逐项对比,帮你定位自己在哪一层:
| 能力项 | 手写版 | Spring 6.0 AOP |
|---|---|---|
| 通知抽象 | 仅支持Before和Around | 五种通知全支持,还有Introduction |
| 切点表达式 | 方法名通配符 | AspectJ完整表达式,支持注解切点 |
| 代理选择 | 按有无接口自动选 | 可配置proxyTargetClass强制CGLIB |
| 拦截器链 | 链式递归调度 | 责任链+ExposeInvocationInterceptor等标准链 |
| 增强排序 | 无 | @Order、Ordered接口完整排序机制 |
| 切面装配 | 手动addAdvisor | 注解扫描+自动代理注册器 |
| 嵌套代理 | 不支持 | 支持嵌套代理和特殊处理 |
| 缓存 | 无 | 代理类缓存、表达式缓存、切点缓存 |
说实话,手写版撑死了算是一个Spring AOP的“教学版”,距离生产级还差得远。但教学版的价值在于它把整个架构骨架清清楚楚地展示出来了:通知、切点、Advisor、ProxyFactory、MethodInvocation,这些概念只要吃透,回头再看Spring源码简直是一马平川。
4.3 为什么Spring 6.0坚持维护两套代理逻辑
Spring 6.0里依然同时维护着JdkDynamicAopProxy和CglibAopProxy两套实现,没有简化成只用一套。之前有一些框架为了简化直接全面切到CGLIB,Spring没有盲目跟风。因为JDK动态代理是Java平台原生能力,兼容性和稳定性最好;CGLIB是字节码生成,在某些受管控的JDK环境下(如SecurityManager启用时)可能受限。两套方案并存,由用户按场景选择容错,这种设计哲学值得我们在写框架时参考。
手写时有个感悟:我们在ProxyFactory里做判断“有接口就走JDK,没接口就走CGLIB”,这个逻辑跟Spring默认行为一致。但Spring还提供了proxyTargetClass=true的强制选项,一旦开启,即使有接口也强制用CGLIB。这个配置位放在@EnableAspectJAutoProxy(proxyTargetClass = true)里,为什么需要强制CGLIB?因为有些接口方法在调用链上会被转成实现类类型,JDK代理返回的代理对象无法强转成实现类类型,而CGLIB代理可以。
4.4 手写后回头读懂Spring 6.0的关键源码位置
手写项目完成后,建议你按下面的路径读一遍Spring源码,会有一种“原来是这里”的豁然开朗感:
org.springframework.aop.framework.ProxyFactory:我们手写的ProxyFactory对应的就是它,负责组装代理;org.springframework.aop.framework.JdkDynamicAopProxy:JDK代理实现,核心在invoke()方法里的拦截器链调度;org.springframework.aop.framework.CglibAopProxy:CGLIB代理实现,核心在DynamicAdvisedInterceptor内部类;org.springframework.aop.framework.ReflectiveMethodInvocation:责任链执行器,跟我们手写的ReflectiveMethodInvocation几乎一一对应;org.springframework.aop.aspectj.AspectJExpressionPointcut:切点表达式解析和匹配。
Spring源码里ReflectiveMethodInvocation的proceed()方法比我们的复杂,它考虑了拦截器链中可能包含ExposeInvocationObserver(用来传递当前MethodInvocation给AOP上下文)、AspectJAfterThrowingAdvice(异常处理通知)等特殊节点,但主干逻辑完全一致。这个源码阅读路径是我手写完项目之后走过一遍的,非常通顺,跟着走你就能把整个AOP从原理到实现串成一条完整的知识线。
5. 常见问题与排查技巧实录
5.1 代理对象调方法无限递归
这个问题几乎每个手写CGLIB的人都会遇到一次,也是排查经验最丰富的一个坑。现象是:代理对象在执行业务方法时,方法内部调用自身的方法,结果整个流程卡住或者疯狂循环。原因很简单,CGLIB生成的子类重写了目标方法,当你在这个重写方法里又调用method.invoke(target, args)时,它触发的还是子类的重写逻辑,相当于自己调自己,形成死循环。
解决办法就是前面提过的:CGLIB回调里要用MethodProxy.invokeSuper(obj, args)来调用父类方法,obj是代理对象本身,invokeSuper会直接调用父类的原始方法实现,绕开重写逻辑。这是CGLIB和JDK代理的一个关键思维差异,写JDK代理时你用的是method.invoke(target, args),而这里必须切换到invokeSuper。
5.2 代理不生效:目标类没有走代理对象
这个问题在真实项目里出现过,表现为:加了环绕通知,但日志里看不到任何切面输出。排查之后发现,调用方拿到的对象压根不是代理对象,而是原始对象。常见原因有三种:一是对象是通过new直接创建的,没有经过代理工厂,代码里拿到的是裸对象;二是目标类虽然有接口,但强转成了实现类类型,JDK代理对象无法强转成功,于是在某个环节悄悄走了原始对象;三是在静态方法或构造方法里直接调用了业务方法,这种情况下代理链路压根没建立。
排查建议很简单,用一行代码打印类名验证:System.out.println(proxy.getClass()),如果看到$Proxy0或者包含$$EnhancerByCGLIB$$字样,说明确实是代理对象;否则就是原始对象。这个方法在分析Spring事务不生效问题时同样好使。
5.3 方法级别的切点匹配不生效
用切点表达式指定方法后,发现有些方法没被增强,但表达式看着没错。我排查过的典型场景是:目标类的方法名跟表达式匹配了,但方法参数列表不同,导致正则在方法名匹配之外还匹配了其他条件;或者目标类里存在重载方法,其中一个命中了切点,另一个没命中。
手写的简易正则切点不会遇到参数匹配的问题,但Spring的AspectJ表达式里有一个高频坑:execution(* com.example.service.*.*(..))这个表达式里,第三个“*”表示方法名,(..)表示任意参数。很多人漏掉(..)导致参数匹配失败。遇到这类问题,建议把表达式单独写成一个测试用例跑一遍,用pointcut.matches(method, targetClass)单独验证,比在完整链路里查要快得多。
5.4 拦截器链顺序混乱
环绕通知、前置通知同时存在时,执行顺序你要在头脑里有明确的图景。不是“前置先执行再执行环绕”,而是环绕通知先拿到控制权,它内部的proceed()调用才触发前置通知,最后才到业务方法。如果多个通知同时作用,顺序取决于两条规则:一是@Order注解或Ordered接口声明的优先级,数值越小优先级越高;二是不同类型通知在环绕通知内部的触发时机。
手写版里我没做排序,直接按List的顺序加入链。这也提醒你:生产环境里大量切面交叉时,如果不理解这个顺序控制原则,很容易出现日志记录顺序、事务开启顺序错乱的问题。Spring里对同一方法同时存在多个切面时有一套完整的排序机制,核心就在AdvisorAdapterRegistrationManager和DefaultAdvisorChainFactory里,有兴趣可以深入读读源码。
5.5 常见问题速查表
| 症状 | 可能原因 | 排查优先级 |
|---|---|---|
| 代理后方法内自调用不走增强 | 方法内部用this调用自身,绕过了代理 | 高 |
| CGLIB代理栈溢出或卡死 | 回调里用了method.invoke而非invokeSuper | 高 |
| 代理对象无法强转为实现类 | JDK代理只实现接口 | 高 |
| 切面完全不触发 | 调用方拿的不是代理对象 | 高 |
| 部分方法未增强 | 切点表达式匹配不到位 | 中 |
| 通知顺序不符合预期 | 缺少排序机制或@Order配置错误 | 中 |
| 构造器或static方法中的调用不增强 | 代理机制不作用于这些调用 | 低 |
这个表是我在实际学习和项目排查中反复验证过的,覆盖了手写AOP和真实Spring项目中最常见的问题。遇到问题先按表排查,能省不少时间。
5.6 手写框架的边界域:哪些场景不必深究
手写版毕竟不是为了替代Spring,做到哪一步算合适,我个人的判断是:当你能在不查资料的情况下把代理工厂、拦截器链、切点匹配的代码完整默写出来,并且能解释清楚代理对象的内存模型时,你对手写AOP的掌握已经超过绝大多数候选人。再往下,比如实现@Aspect注解解析、自动代理注册器、AspectJ表达式完整解析,这些工作重复度高但思路没有新增量,真正需要时读Spring源码比再造轮子更有价值。
我还想说一个实操中的心得:手写项目最好的验证方式不是跑通一个简单示例就完了,而是做几个变体测试,比如让同一个方法匹配多个通知、用CGLIB代理一个没有接口的类、在环绕通知里直接吞掉异常不让业务方法执行。这几个场景能帮你把框架边界摸清楚,也让你对AOP的设计取舍有更直观的感受。
6. 手写Spring AOP对理解Spring 6.0后续特性的价值
Spring 6.0核心亮点之一是AOT(Ahead-of-Time)编译优化,这项技术在Spring Boot 3.0里以Native Image的形式落地。有人会问,AOT和AOP之间是什么关系?简单说,AOP是运行期的动态织入,而AOT是编译期的静态优化,两者天然存在张力。Spring 6.0在个别场景下对AOP链路做了优化,但AOP运行时模型没有根本改变,依然是代理模式加拦截器链。
这意味着你手写AOP打下的基础,在面对Spring Boot 3.0的Native Image配置时特别有用。比如,使用AOP时动态生成的代理类在AOT编译期是未知的,所以你需要用@RegisterReflectionForBinding或AOT提示来告诉编译器提前生成这些类的元信息。没有AOP底层原理的理解,你根本想不通为什么Native Image下AOP会报反射错误,也不知道去哪里加配置。
所以,手写AOP不只是面试技巧,更是你深入理解Spring现代架构的一块基石。JDK动态代理、CGLIB字节码生成、拦截器链责任模式,这些底层机制你在Spring 6.0、Spring Boot 3.0、乃至未来几个大版本里都会反复接触到。把这些底层能力打磨扎实,遇到新问题时你才能快速定位到机制层,而不是停留在API层靠搜文档碰运气。
按我个人的看法,任何框架都会换代,但底层机制是长存的。把手写AOP这个项目做完,你的收获不只是一段代码,而是一整套关于如何设计可扩展的基础设施的思维方法。选择代理模式作为插桩手段、用责任链作为通知调度模型、用切点表达式做精确匹配,这些设计决策在任何中间件设计里都有借鉴价值。从这个角度说,花一个周末手写一遍Spring AOP,性价比极高。