Spring AOP 底层到底怎么跑的,我翻了一圈源码终于搞明白了

Spring AOP 底层到底怎么跑的,我翻了一圈源码终于搞明白了

上一篇博客我整理了 AOP 的八个概念,算是知道了"是什么"。但心里一直有个疑问:Spring 到底是怎么做到的?我在类上加个@Aspect,Spring 就能自动帮我拦截方法了?这背后发生了什么?

这篇就把我查到的东西整理一下,尽量用自己能理解的话说。

代理对象是怎么来的

上一篇说过,AOP 的核心是代理。你调的UserService其实不是真正的UserService,而是 Spring 给你生成的一个代理对象。那这个代理对象是什么时候、怎么生成的?

答案是 Spring 容器启动的时候。

Spring 里有一个东西叫BeanPostProcessor,翻译过来就是 Bean 的后处理器。它的工作是:在每个 Bean 创建完成之后检查一下,看看这个 Bean 有没有被某个切面匹配到。如果有,就不把原始对象放进容器了,而是给它生成一个代理对象放进去。

所以当你在代码里写@Autowired UserService userService的时候,Spring 给你的就已经是代理了。你从头到尾都没碰过原始的UserService

那代理对象是怎么生成的?这就涉及到两种技术了。

两种代理方式:JDK 动态代理和 CGLIB

Spring 生成代理对象有两种方式,取决于目标对象长什么样。

第一种:JDK 动态代理。这是 Java 自带的,用的java.lang.reflect.Proxy这个类。它能在程序运行的时候动态生成一个实现了目标接口的代理类。所有方法调用都会被转发到一个InvocationHandlerinvoke方法里,你就可以在这里面加通知逻辑。

但它有个硬限制:目标对象必须实现了接口。因为 JDK 动态代理的原理是实现接口,不是继承类。没接口就没法用。

第二种:CGLIB 字节码增强。CGLIB 用的是一个叫 ASM 的底层框架,它能在运行的时候动态生成目标类的子类。代理对象是目标类的儿子,重写了父亲的方法。调用的时候通过MethodProxy触发逻辑。

因为是基于继承,所以不需要接口,什么类都能代理。但也有个限制:final修饰的类和方法没法被继承和重写,所以 CGLIB 对final无能为力。

Spring 怎么选?如果目标对象实现了接口,默认用 JDK 动态代理。没实现接口,就自动降级用 CGLIB。不过 Spring Boot 2.x 之后把默认改成了 CGLIB,因为 JDK 代理在某些场景下会有坑,统一用 CGLIB 更省事。

我自己写代码的时候一般不去管它用哪种,让 Spring 自己决定就行。知道有这么回事,主要是为了遇到代理失效的时候能排查原因。比如你给一个final方法加了切面发现没生效,这时候你就知道:哦,CGLIB 没法重写final方法。

Spring AOP 和 AspectJ 的区别:运行期织入

查资料的时候我发现 AOP 的实现不止 Spring 一种,还有一个叫 AspectJ 的东西,功能更强。它俩的区别主要在"织入"的时机上。

AspectJ 是编译期或类加载期织入。它有自己的编译器(叫ajc),在代码编译成.class文件的时候,或者类加载到 JVM 的时候,直接把切面代码改写到目标类的字节码里。改完之后目标类的.class文件就已经包含切面逻辑了。

这种方式性能好,因为运行时就是一段普通的代码,没有额外的代理开销。但代价是你得换编译器,或者配置特殊的 ClassLoader,对项目有侵入。

Spring AOP 是运行期织入。目标类的源代码和字节码一个字都不动。Spring 在程序跑起来之后,在内存中动态生成一个代理对象。你调的是代理,代理再去调真正的目标对象。

性能上比 AspectJ 稍微差一点点,因为多了一层代理转发。但好处是对项目零侵入,不需要换编译器,不需要改构建流程,只要用了 Spring 容器就能直接用。这也是为什么大部分 Spring 项目都用 Spring AOP 而不是 AspectJ。

不过如果你需要拦截字段赋值、构造函数这些 Spring AOP 搞不定的场景,那就只能上 AspectJ 了。

拦截器链:多个切面怎么协作

实际项目里,一个方法上可能叠了好几层切面。比如一个接口既要记日志,又要验权限,还要管事务。这三个切面都匹配到了同一个方法,那它们按什么顺序执行?

Spring 底层用的是责任链模式。

具体过程是这样的:所有的通知(不管是@Before@After还是@Around),Spring 都会把它们统一包装成MethodInterceptor接口。然后把匹配到这个方法的所有拦截器排成一条链,放在一个MethodInvocation对象里。

方法被调用的时候,不是直接去执行目标方法,而是从链的第一个拦截器开始走。每个拦截器做完自己的前置逻辑之后,调用invocation.proceed()把控制权交给下一个拦截器。等所有拦截器都走完了,最后才通过反射调用真正的目标方法。

目标方法执行完之后,调用链又逆序往回走,依次触发各个拦截器的后置逻辑。

画出来大概是这样(假设日志切面在外层,事务切面在内层):

调用代理方法 → 日志拦截器:记录开始时间 → 事务拦截器:开启事务 → 执行真正的目标方法 ← 事务拦截器:提交事务 ← 日志拦截器:记录耗时

每个拦截器只关心自己的事,互相不知道对方的存在。这就是责任链的好处:你加一个新切面,不需要改已有的任何一个切面。

Around 通知的 proceed() 到底在干嘛

五种通知里,环绕通知(@Around)是最灵活的,也是最容易写错的。它的核心就是那个proceed()方法。

proceed()做的事情是:把控制权交给链中的下一个拦截器(或者最终的目标方法)。如果你不调proceed(),后面的拦截器和目标方法都不会执行。

这个特性可以用来做很多事情。比如权限校验:如果用户没权限,直接不调proceed(),方法就被拦住了。但这也意味着如果你忘了调proceed(),业务方法永远不会执行,而且不会报任何错,就是静默失败了。这个坑我踩过一次,排查了半天。

@Around("serviceLayer()")publicObjectcheckPermission(ProceedingJoinPointjoinPoint)throwsThrowable{if(!hasPermission()){thrownewRuntimeException("没有权限");}// 如果这里忘了写 proceed(),目标方法就永远不会执行returnjoinPoint.proceed();}

小结一下

Spring AOP 底层其实就做了三件事:

第一,容器启动时扫描所有 Bean,看它们有没有被切面匹配到,匹配到的就生成代理对象替换掉原始对象。

第二,生成代理的方式有两种:有接口用 JDK 动态代理,没接口用 CGLIB。目标类的字节码一个字都不动。

第三,方法被调用的时候,Spring 把匹配到的所有切面包装成拦截器链,按顺序依次执行,最后才调用目标方法。

概念层面看着很抽象,但底层拆开看其实都是 Java 的基本功:反射、动态代理、责任链模式。搞清楚这几个东西,AOP 的原理就没那么玄了。