一、先搞懂本质:两者根本不是同一种东西
很多人把 Spring AOP 和 AspectJ 放在一起比较,就好像在比较"自行车"和"摩托车"——它们都是两个轮子,但驱动方式完全不同。
Spring AOP 的本质是"运行时织入",说白了就是给目标对象套了一层代理壳子。 它的底层实现依赖两种动态代理机制:如果目标类实现了接口,就用 JDK 动态代理;如果没有接口,就用 CGLIB 生成子类代理。所有的切面逻辑,都是在代理对象的方法调用前后插入的。目标对象本身的字节码,一行都没有被改动。
AspectJ 的本质是"编译期织入",它直接修改字节码。 AspectJ 有自己独立的编译器 ajc,在代码编译阶段就把切面逻辑织入到了目标类的字节码中。运行时不需要任何代理,切面代码已经成为类的一部分。
这个根本性的差异,决定了后续所有的区别。
二、Spring AOP 赢在"简单"二字
工程世界里,"简单"从来不是贬义词,而是最高级的竞争力。
Spring AOP 的接入成本极低。 你只需要在项目中引入 spring-boot-starter-aop 依赖,在配置类上加一个注解,然后用 @Aspect 标记你的切面类,完事。整个过程不需要任何额外的编译器,不需要修改构建流程,不需要学习新的语法。
AspectJ 的接入成本则高出一个量级。 你需要在构建工具中配置 AspectJ 编译器或织入器,编译流程变复杂了,调试也变麻烦了。更要命的是,一旦你想关掉某个切面,Spring AOP 只需要在配置中注释掉一行,重启即可;而 AspectJ?你必须重新编译整个项目。
这不是夸张。在实际项目中,需求变更是常态。今天加一个日志切面,明天可能要临时关掉它排查问题。Spring AOP 让你五秒钟完成操作,AspectJ 让你等编译完成。这种效率差距,在高频迭代的业务开发中,是致命的。
三、关于性能:你可能被误导了
网上流传一个数据:AspectJ 比 Spring AOP 快 9 到 35 倍,10 纳秒对比 355 纳秒。这个数字是真的,但它极具误导性。
首先,这个差距是在切面数量极其庞大的场景下测出来的——有测试用了上万个切面,每个请求要穿越数千个切面,此时 Spring AOP 的代理链确实会累积可观的开销。但请问,你的项目里有上万个切面吗?绝大多数项目,切面数量在个位数到两位数之间。在这个量级下,两者的性能差异可以忽略不计。
其次,Spring AOP 的性能瓶颈从来不在单次调用上,而在于代理链的层数。每个切面都会增加一层代理调用,当切面过多时,请求确实会慢几毫秒。但解决方案也很简单:控制切面数量,合理规划切点表达式,而不是一上来就上 AspectJ。
所以我的判断是:如果你的切面数量在几十个以内,Spring AOP 的性能完全够用。只有当你真的有成千上万个切面、且对单次调用的纳秒级延迟有极致要求时,AspectJ 才值得考虑。 而后一种场景,在普通业务开发中几乎不存在。
四、Spring AOP 的能力边界,其实比你想象的宽
很多人选择 AspectJ,是因为觉得 Spring AOP "能力不够"。这个认知需要纠正。
AspectJ 确实支持更多的连接点类型:字段的读写、构造方法的执行、静态代码块的初始化……而 Spring AOP 只支持方法执行这一种连接点。听起来 Spring AOP 确实"矮了一截"。
但请你冷静想一想:在实际业务开发中,你真正需要切入字段读写或构造方法的场景有多少?
绝大多数横切关注点——日志记录、权限校验、事务管理、性能监控、异常处理——全部都是围绕"方法执行"展开的。Spring AOP 对这些场景的覆盖率是百分之百。
更关键的一点:Spring AOP 已经集成了 AspectJ 的注解风格。 也就是说,你用 @Aspect、@Before、@Around 这些注解写出来的切面,既可以被 Spring AOP 运行,也可以被 AspectJ 运行。这意味着你今天用 Spring AOP 写的代码,明天如果真的需要迁移到 AspectJ,几乎不需要修改任何逻辑。
Spring 团队自己都推荐:对于自定义切面,优先使用 @AspectJ 风格。这不是随便说说的,这是给你留了后路。
五、Spring AOP 的致命陷阱:this 调用
既然我这么推崇 Spring AOP,那它有没有坑?有,而且这个坑非常经典,踩过的人不在少数。
在 Spring AOP 中,同一个类内部的方法互相调用,切面不会生效。
原因很简单:Spring AOP 是基于代理的。当你在类 A 的方法 a 中调用同一个类的方法 b 时,调用走的是 this.methodB(),而不是代理对象.methodB()。代理对象根本没有被经过,切面自然也就不会触发。
这个问题在开发中极易出现。比如你在一个 Service 类里,把一个公共方法抽取出来,然后在多个业务方法中调用它。你给这个公共方法加了日志切面,结果发现日志只在外部调用时才打印,内部调用时消失了。很多开发者会因此怀疑人生,以为 AOP 出了 bug。
解决方案也不复杂:要么把需要切面的方法抽取到另一个 Bean 中,通过依赖注入调用;要么在类内部获取代理对象后再调用。 但无论如何,你必须知道这个陷阱的存在。
相比之下,AspectJ 因为是直接修改字节码,不存在这个问题。无论怎么调用,切面都会生效。这是 AspectJ 为数不多的、真正有价值的优势之一。但代价是,你要为此引入一整套编译期织入的复杂度。值不值得,你自己掂量。
六、什么时候你真的该用 AspectJ?
说了这么多 Spring AOP 的好话,我并不是在说 AspectJ 一无是处。在以下几种场景中,AspectJ 是不可替代的:
第一,你需要切入非 Spring 管理的对象。 Spring AOP 只能对 Spring 容器中的 Bean 生效。如果你想对一个普通的 POJO、一个第三方库的类、甚至一个域对象做切面,Spring AOP 束手无策,只有 AspectJ 能做到。
第二,你需要字段级别或构造方法级别的切入。 比如你想监控某个字段每次被修改时的值变化,或者想在对象构造时执行某些逻辑。这种需求 Spring AOP 无法满足。
第三,你的切面数量达到了数千甚至上万个,且性能测试证明代理链已成为瓶颈。 这种情况在一些基础架构层面的项目中确实存在,比如某些启动耗时优化的场景,需要对所有方法调用都插入追踪逻辑。
除此之外,请老老实实用 Spring AOP。
七、一个真实的选型故事
曾经有一个团队,在项目初期为了"技术先进性",全面采用了 AspectJ。结果呢?每次需求变更,都要重新编译整个项目;新来的同事看不懂 ajc 的配置,调试时一脸茫然;排查问题时,因为字节码已经被修改,堆栈信息变得难以解读。
后来他们把所有切面迁移回了 Spring AOP。迁移过程异常顺利——因为切面本身就是用 @AspectJ 风格写的,改几行配置就完成了。迁移之后,构建速度快了,调试方便了,新人上手也快了。
这个故事说明了一个道理:技术选型不是越高级越好,而是越合适越好。
写在最后
Spring AOP 和 AspectJ 之间的选择,本质上不是一个技术问题,而是一个工程问题。
Spring AOP 用"运行时代理"换来了极致的简单和可控。它的能力边界刚好覆盖了百分之九十以上的业务场景,接入成本低到可以忽略,维护成本低到让你忘记它的存在。
AspectJ 用"编译期织入"换来了极致的性能和能力。但这份极致,是用复杂度、学习成本和工程负担买来的。
作为一线开发工程师,我的建议只有一句话:先用 Spring AOP,如果它真的不够用了,再考虑 AspectJ。 而根据我的经验,这个"不够用"的时刻,在绝大多数项目的整个生命周期里,都不会到来。
别被技术的光环迷惑了双眼。能用五行配置解决的问题,就不要引入一整套编译器。这才是真正的工程智慧。