书接上文,我们已经把对象交给了 Spring,类被注册为 Bean,依赖由容器注入,一切看起来都非常优雅。
Bean 在什么时候创建?
依赖在什么时候注入?
初始化方法什么时候执行?
上一篇,我们已经把这些问题拆得很清楚了,但事情并没有结束
当你在代码里写下这样一行:
java
@Autowired
private UserService userService;
你有没有想过一个问题:
你拿到的,真的就是当初 new 出来的那个 UserService 对象吗?
如果你在项目里用过事务、日志、权限校验、监控......
那答案大概率是:不是
要解释清楚这一点,就必须引出 Spring 里另一个绕不开的核心机制:AOP
一、AOP 是从哪里"插进来"的?
在正式聊 AOP 之前,我们先解决一个非常关键、但常被忽略的问题:
AOP 介入 Bean 生命周期的哪一步?
很多人下意识以为:是不是在实例化的时候?是不是在 new 对象的时候?
先给结论:
Spring AOP 不参与 Bean 的实例化过程,而是发生在 Bean 初始化完成之后。
也就是说,到了这一刻:构造方法已经执行完毕,依赖已经注入完成; @PostConstruct 也已经调用过
从生命周期的角度看,这个 Bean 已经是一个"能正常用"的对象了,但 Spring 并不会立刻把它原封不动地放进容器
它会先停下来,问自己一个问题:
这个 Bean,需要被增强吗?
如果答案是"否",那很好,直接放行。
如果答案是"是",事情就开始变得有意思了。
二、AOP 的本质:对 Bean 的一次二次加工
如果某个 Bean 命中了 AOP 的规则,Spring 并不会修改这个 Bean 本身,它做的事情非常克制,但也非常关键:
为这个 Bean 创建一个代理对象,把原始对象包在代理里面
于是,容器中最终保存的,就不再是:
text
UserService
而是一个:
text
UserServiceProxy -> UserService
从这一刻开始,一个非常重要、但常被忽略的事实成立了:
你从 Spring 容器中拿到的 Bean,可能从来都不是原始对象本身,而是一个代理对象
这个代理对象看起来"像" UserService,接口一样、方法一样、用法一样,但它真正的职责,是在方法调用前后,插手处理一些额外逻辑
三、AOP 真正增强的,其实不是"对象"
理解了代理之后,AOP 的行为会突然变得非常清晰:AOP 从来不是在"增强某个对象",而是在接管一次方法调用的全过程。
在没有 AOP 的情况下,一次方法调用非常直接:
java
userService.save();
JVM 会在 userService这个对象上,直接调用 save(),执行完毕,流程结束
但当 AOP 介入之后,这行代码表面上没变,
实际发生的事情却已经完全不同了:真实的调用路径变成了:
text
调用方
↓
代理对象
↓
前置增强逻辑
↓
目标方法
↓
后置增强逻辑
也就是说,你以为你在"调方法",实际上你是在走一条被代理对象精心包装过的调用链
Spring AOP 所做的一切增强,都是围绕着这条调用链展开的
四、为什么说 AOP 是"代码无侵入"的?
既然 AOP 能在方法前后插逻辑,那它是不是偷偷帮我改了代码?
答案是:完全没有
Spring AOP:
- 不修改你的类
- 不修改你的方法
- 不往业务逻辑里塞任何代码
它做的事情只有一件:
用代理对象,替换掉原始 Bean,对外统一暴露代理
于是,业务代码可以始终保持"只关心业务":
- 日志不写在方法里
- 权限校验不污染核心逻辑
- 事务控制不需要手动 try-catch
所有这些"横切关注点",都被集中到了代理中统一处理。
五、Spring 是怎么知道"该拦谁"的?
现在问题来了。
Spring 容器里有这么多 Bean,每个 Bean 又有这么多方法,AOP 不可能对所有方法都生效
那 Spring 怎么判断需要拦截哪些方法呢?
它引入了一个概念:切点(Pointcut)
5.1 切点的本质
切点本质上不是"某个方法",而是一套筛选规则。
它回答的其实是一个非常现实的问题:这个方法,要不要被我接管调用过程?
5.2 execution 表达式
execution 是最常见的一种切点表达方式,它通过方法结构来匹配:
java
execution(* com.xxx.service..*(..))
这种方式的特点是:
- 覆盖范围广
- 适合"统一处理一整类方法"
比如:
service 包下的所有 public 方法,都需要事务控制
5.3 @annotation 表达式
另一种常见方式,是通过注解精确匹配:
java
@annotation(com.xxx.Log)
这种方式更加显式、也更安全,只有你明确标注过的方法,才会被 AOP 增强
六、通知:在方法的哪个"时刻"插手?
切点解决的是"拦谁",那通知解决的就是另一个问题:在方法执行的哪个阶段,插入增强逻辑?
Spring AOP 提供了多种通知类型,对应方法调用的不同时间点:
- @Before:方法执行前
- @After:方法执行后(无论是否异常)
- @AfterReturning:正常返回后
- @AfterThrowing:发生异常后
- @Around:包裹整个方法调用过
6.1 为什么 @Around 的能力最强?
@Around 的特殊之处在于:它完整地掌控了方法是否执行、何时执行、如何返回
在 @Around 中,你可以:
- 决定目标方法是否执行
- 在执行前后插逻辑
- 捕获异常
- 修改返回值
从能力上讲,其他所有通知,本质上都可以用 @Around 模拟
这也是为什么,理解了@Around,就基本理解了 AOP 的核心能力。
七、多个切面同时生效时,会发生什么?
当多个切面同时作用于同一个方法时,Spring 的执行顺序其实非常规律
整体结构可以概括为:
text
前置通知: 1 → 2 → 3
目标方法
后置通知: 3 → 2 → 1
也就是常说的 123321
默认情况下:
- 优先级高的切面在外层
- 优先级低的切面在内层
如果你不指定顺序,Spring 会按切面类名排序;如果你想明确控制,可以使用:
java
@Order(1) // 数字越小,优先级越高
八、那些"奇怪现象",其实全都说得通了
理解了代理机制之后,很多 AOP 的"坑",都会突然变得合理:
- 构造方法无法被增强
→ 因为代理发生在构造方法之后 - private 方法不生效
→ 代理无法拦截私有方法调用 - 同类内部方法调用失效
→ 调用没有经过代理对象 - @Transactional 偶尔失效
→ 本质仍然是"没走代理"
这些都不是 Spring 的 bug,
而是代理机制的自然结果。
九、把 AOP 放回 Bean 生命周期中再看一遍
现在,我们把 AOP 放回到 Bean 生命周期中,重新串一遍完整流程:
- 创建 BeanDefinition
- 实例化对象
- 注入依赖
- 执行初始化逻辑
- 判断是否需要 AOP 增强
- 如果需要,创建代理对象,替换原始 Bean
- 对外暴露的,始终是代理对象
所以,Spring AOP 从来都不是一套神秘的黑魔法
它只是:在 Bean 初始化完成之后,用代理对象,接管了方法调用过程
也就是那句老话:
你以为你在调方法,其实你一直在走代理。