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

发布时间:2026/6/20 1:23:55
Spring AOP 底层到底怎么跑的,我翻了一圈源码终于搞明白了 上一篇博客我整理了 AOP 的八个概念算是知道了是什么。但心里一直有个疑问Spring 到底是怎么做到的我在类上加个AspectSpring 就能自动帮我拦截方法了这背后发生了什么这篇就把我查到的东西整理一下尽量用自己能理解的话说。代理对象是怎么来的上一篇说过AOP 的核心是代理。你调的UserService其实不是真正的UserService而是 Spring 给你生成的一个代理对象。那这个代理对象是什么时候、怎么生成的答案是 Spring 容器启动的时候。Spring 里有一个东西叫BeanPostProcessor翻译过来就是 Bean 的后处理器。它的工作是在每个 Bean 创建完成之后检查一下看看这个 Bean 有没有被某个切面匹配到。如果有就不把原始对象放进容器了而是给它生成一个代理对象放进去。所以当你在代码里写Autowired UserService userService的时候Spring 给你的就已经是代理了。你从头到尾都没碰过原始的UserService。那代理对象是怎么生成的这就涉及到两种技术了。两种代理方式JDK 动态代理和 CGLIBSpring 生成代理对象有两种方式取决于目标对象长什么样。第一种JDK 动态代理。这是 Java 自带的用的java.lang.reflect.Proxy这个类。它能在程序运行的时候动态生成一个实现了目标接口的代理类。所有方法调用都会被转发到一个InvocationHandler的invoke方法里你就可以在这里面加通知逻辑。但它有个硬限制目标对象必须实现了接口。因为 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还是AroundSpring 都会把它们统一包装成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 的原理就没那么玄了。