很多人第一次接触 AOP 时,感觉像是在看魔术:
“我就加了一个
@Transactional注解,也没写commit()和rollback(),事务怎么就自动提交了?”
“我就标了个@Log,日志怎么就凭空出现了?”
于是,大家惊呼:“Spring AOP 是魔法!”现在就让我们撕开这层“魔法”的包装纸。世界上没有魔法,只有底层原理和设计模式。今天,我们就用手术刀般的精度,把 AOP 解剖给你看。
OOP 造车,AOP 搞服务
首先,我们要理清OOP(面向对象)和AOP(面向切面)的关系。它们不是对立的,而是互补的。
OOP = 汽车制造厂🏭
- 你关注的是核心部件:引擎(Engine)、轮胎(Tire)、底盘(Chassis)。
- 代码里全是业务逻辑:
orderService.createOrder(),userService.login()。 - 痛点:如果你在制造引擎的代码里,硬编码了“加油”、“洗车”、“买保险”的逻辑,那这台引擎简直没法维护!每次换个保险公司,你都得拆引擎?
AOP = 汽车售后服务中心
- 你关注的是横切服务:加油(事务)、洗车(日志)、安检(权限)、保险(监控)。
- 核心理念:这些服务是所有车都需要的,但它们不属于某一辆车的核心结构。
- 运作方式:车(Bean)出厂后,经过“服务站”(AOP 容器)。服务站给车套上一层“保护壳”(代理对象)。
- 你想加油?车子开进加油站(前置通知),加完油再上路。
- 你想洗车?车子开完回来,自动进洗车房(后置通知)。
- 结果:引擎代码里干干净净,只有“转动”的逻辑。加油洗车的逻辑,全在服务站里。
OOP 纵向继承,解决复用;
AOP 横向切割,解决耦合。
核心术语解析
别被英文术语吓到,我们把它翻译成“人话”:当然这比较基础也简单:
| 术语 | 英文 | 通俗解释 (人话) | 生活化比喻 (汽车场景) |
|---|---|---|---|
| 切面 | Aspect | 专门封装了“横切逻辑”的类。 (比如:把日志、事务、权限校验都写在这个类里) | 售后服务站; 它不造车,但提供加油、洗车、保养等全套增值服务。 |
| 连接点 | JoinPoint | 程序运行过程中,所有可能插入代码的位置。 (比如:每个方法执行前、执行后、抛出异常时) | 汽车的全生命周期时刻 启动、加速、刹车、熄火、加油、维修... (理论上这些时刻都可以被干预) |
| 切入点 | Pointcut | 从所有连接点中,真正选中要拦截的那些特定位置。 (通过规则过滤,比如只拦截 save开头的方法) | 选定的服务时刻 你决定只在 “加油” 和 “洗车” 这两个时刻介入服务,其他的(如“加速”、“刹车”)不管。 |
| 通知 | Advice | 在切入点具体要执行的代码逻辑。 (比如:方法执行前打印日志,执行后提交事务) | 具体的动作指令 前置通知:“加满95号汽油”; 后置通知:“记录本次行驶里程”。 |
| 织入 | Weaving | 把切面代码应用到目标对象,生成代理对象的过程。 (编译时、加载时或运行时) | 改装/组装过程 把服务站的功能“贴”到车上,最终交付给用户的是一辆带有售后服务的 “增强版汽车” (代理对象)。 |
切面(服务站)定义了要在哪些切入点(选定的时刻,如加油)执行什么通知(具体动作,如加满油),通过织入(改装)过程,将这些功能应用到程序的连接点(所有可能的时刻)上,最终实现业务逻辑与非业务逻辑的解耦。
原理:静态 vs 动态,谁是真身
AOP 的实现主要有两派:AspectJ和Spring AOP。它们的区别决定了性能和灵活性
1. 静态 AOP (AspectJ) —— “编译时整容”
- 原理:在代码编译阶段(甚至加载阶段),直接修改
.class文件字节码。 - 过程:
- 你写代码:
public void save() { ... } - AspectJ 编译器(ajc)介入。
- 生成的字节码变成了:
- 你写代码:
public void save() { logBefore(); // 硬塞进去的代码 // 原始逻辑 logAfter(); // 硬塞进去的代码 }- 优点:性能极致(没有代理开销,直接调用),功能最强(能拦截字段访问、构造器等)。
- 缺点:需要特殊编译器或 Agent,配置复杂,灵活性稍差。其实还好、相对来说
- 适用场景:对性能极其敏感,或需要拦截非方法调用的场景(如监控字段变化)
2. 动态 AOP (Spring AOP) —— “运行时戴面具”
原理:基于动态代理。不修改原始类,而是在运行时创建一个代理对象(Proxy)
- 过程:
- Spring 容器启动。
- 发现
OrderService需要 AOP。 - Spring不直接给你
OrderService实例,而是给你一个OrderServiceProxy。 - 当你调用
proxy.save()时:- Proxy 先执行
logBefore()。 - Proxy 调用
target.save()(原始对象)。 - Proxy 再执行
logAfter()。
- Proxy 先执行
- 实现技术:
- JDK 动态代理:如果目标类实现了接口,默认用这个。基于反射,只能代理接口方法。
- CGLIB:如果目标类没接口,用这个。基于字节码生成子类,重写方法来拦截。
- 优点:灵活,配置简单(注解搞定),无需特殊编译器。
- 缺点:有轻微性能损耗(反射/子类调用),只能拦截公共方法。
- Spring 的选择:默认使用动态 AOP。因为它够用了,且开发体验最好。
ProxyFactory的决策树
“有接口用 JDK,没接口用 CGLIB”,这太浅了。Spring 内部是通过ProxyFactory进行精密计算的。
核心决策逻辑源码剖析
当AnnotationAwareAspectJAutoProxyCreator决定要代理一个 Bean 时,它会调用createProxy方法。核心逻辑在DefaultAopProxyFactory中:
// org.springframework.aop.framework.DefaultAopProxyFactory public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { // 决策树开始 if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class..."); } // 情况 A: 目标是接口 -> 使用 JDK 动态代理 if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } // 情况 B: 目标是类 -> 使用 CGLIB return new ObjenesisCglibAopProxy(config); } // 情况 C: 默认情况 (有用户定义的接口) -> 使用 JDK 动态代理 else { return new JdkDynamicAopProxy(config); } }proxyTargetClass=true:如果你强制配置了这个(或在@EnableAspectJAutoProxy(proxyTargetClass = true)中设置),Spring 会无视接口,直接使用 CGLIB。这在某些需要代理protected方法或最终类(配合特定配置)时有用,但会有轻微性能损耗。Optimize:这是一个很少用的标记,用于告诉代理可以做一些激进优化(如去掉某些检查),通常不建议开启。
字节码层面的“真身”对比
场景:UserService接口及其实现UserServiceImpl
方案 A:JDK 动态代理 ($Proxy0)
- 继承关系:
extends java.lang.ProxyimplementsUserService,SpringProxy,Advised... - 特点:
- 它是一个全新的类,由 JVM 在运行时生成。
- 它不能继承
UserServiceImpl的任何非接口方法。 - 所有方法调用最终都转发给
InvocationHandler。
$Proxy0 (JDK Proxy) |-- h (InvocationHandler) --> JdkDynamicAopProxy |-- advised (AdvisedSupport) --> 包含 Target(UserServiceImpl) + InterceptorChain方案 B:CGLIB 动态代理 (UserServiceImpl$$EnhancerBySpringCGLIB$$...)
- 继承关系:
extends UserServiceImplimplementsSpringProxy,Advised... - 特点:
- 它是
UserServiceImpl的子类。 - 它可以重写(Override)父类的所有非
final方法。 - 利用
MethodInterceptor拦截方法调用。
- 它是
UserServiceImpl$$EnhancerBySpringCGLIB$$... (CGLIB Proxy) |-- CALLBACK_0 (MethodInterceptor) --> DynamicAdvisedInterceptor |-- advised (AdvisedSupport) --> 包含 Target(UserServiceImpl) + InterceptorChain- JDK 代理是组合(持有目标对象)。
- CGLIB 代理是继承(伪装成目标对象)。
- 坑点:如果你的 Bean 中有
public void internalMethod()没有在接口中定义,JDK 代理无法拦截该方法(因为接口里没有),而 CGLIB 可以。这就是为什么 Spring Boot 2.x 后默认倾向于 CGLIB(proxyTargetClass=true)的原因之一,为了减少这种“意外”。
执行的黑盒 —— 拦截器链的递归模型
AOP 最难理解、也是最核心的部分。Advice 是如何按顺序执行的——递归调用(责任链模式 + 栈帧)
核心类:ReflectiveMethodInvocation
深度源码剖析:Spring AOP 是如何“变魔术”的
场景设定
so easy
@Service public class OrderService { @Transactional // 这是一个切点! public void createOrder() { System.out.println("创建订单..."); // 模拟业务 } }1. 启动BeanPostProcessor 登场
还记得我们之前讲的BeanPostProcessor吗?Spring AOP 的核心就是一个特殊的 BPP:AnnotationAwareAspectJAutoProxyCreator。
时序图:Spring 容器启动与代理创建:简易版
- 偷梁换柱:容器中存的从来都不是原始的
OrderService,而是那个带着“面具”的Proxy。 - 透明性:调用者(Controller 或其他 Service)完全感知不到,因为它们拿到的引用类型依然是
OrderService(接口或父类)。
2. 运行:代理对象的“拦截”艺术
在 Controller 中调用orderService.createOrder()时,实际发生了什么
// 这是 Spring 内部生成的代理类的大致逻辑 (简化版) public class OrderService$Proxy extends OrderService implements SpringAopProxy { private OrderService target; // 原始对象 private List<Interceptor> interceptors; // 拦截器链 (事务、日志等) @Override public void createOrder() { // 1. 获取拦截器链 List<Interceptor> chain = this.getInterceptorsAndDynamicInterceptionAdvice(...); // 2. 创建调用链执行器 MethodInvocation invocation = new ReflectiveMethodInvocation(target, this, method, args, chain); // 3. 执行链条 (责任链模式) invocation.proceed(); } } // 责任链执行逻辑 class MethodInvocation { int currentInterceptorIndex = -1; public Object proceed() { currentInterceptorIndex++; if (currentInterceptorIndex == interceptors.size()) { // 链子走完了,调用原始目标方法 return target.invoke(); } // 获取下一个拦截器 (比如事务拦截器) Interceptor interceptor = interceptors.get(currentInterceptorIndex); // 执行拦截器逻辑 (它内部会再次调用 proceed,形成递归) return interceptor.invoke(this); } } // 事务拦截器示例 class TransactionInterceptor implements Interceptor { public Object invoke(MethodInvocation inv) { TransactionStatus tx = beginTransaction(); // 前置:开启事务 try { Object ret = inv.proceed(); // 继续调用链 (下一个拦截器 或 目标方法) commit(tx); // 后置:提交事务 return ret; } catch (Exception e) { rollback(tx); // 异常:回滚事务 throw e; } } }所谓的“魔法”,其实就是递归调用+责任链模式。
- 代理方法被调用。
- 第一个拦截器(事务)执行前置逻辑。
- 调用
proceed(),进入第二个拦截器(日志)。 - 日志执行前置,调用
proceed(),进入原始方法。 - 原始方法执行完毕,返回。
- 日志执行后置,返回。
- 事务执行后置(提交),返回。
什么时候该用 AOP
| ✅ 推荐使用 (横切关注点) | ❌ 不推荐使用 (核心业务逻辑) |
|---|---|
| 日志记录 (Log):统一记录请求参数、耗时、异常堆栈。 | 业务流程:订单状态流转、金额计算逻辑。 |
| 事务管理 (Transaction):数据库原子性操作。 | 数据校验:具体的业务规则校验(如“库存是否充足”)。 |
| 权限控制 (Security):检查用户是否有角色/权限。 | 复杂算法:推荐算法、加密解密核心实现。 |
| 性能监控 (Monitor):统计 API 响应时间、QPS。 | 状态依赖:逻辑强依赖于特定实例状态的。 |
| 缓存处理 (Cache):统一读取/更新缓存逻辑。 | 私有方法:Spring AOP 默认无法拦截私有方法。 |
| 参数校验 (Validation):统一 JSR-303 校验。 | 自调用问题:类内部方法互相调用(this.method())不会触发 AOP。 |
避坑指南:自调用失效 如果你在 ServiceA 内部调用 this.methodB(),而 methodB 有 @Transactional,事务不会生效! 原因:this 指向的是原始对象,绕过了代理对象。 解法:注入自身 (@Autowired ServiceA self) 然后调用 self.methodB(),或者使用 AopContext.currentProxy()。很多初级开发者喜欢用 AOP 炫技,把业务逻辑拆得七零八落,导致代码难以追踪。这是错误的!
“AOP 不是用来炫技的,它是为了将‘横切关注点’从核心业务中剥离出来,让代码回归纯粹的业务逻辑。”
- 纯粹性:你的 Service 层应该只关心“做什么”(业务),而不关心“怎么做保障”(事务、日志)。
- 可测试性:剥离了横切逻辑后,你的单元测试可以专注于业务断言,无需 Mock 复杂的日志或事务管理器。
- 可维护性:当需要修改日志格式或事务隔离级别时,只需修改一个切面,全系统生效。