【javaEE】Spring AOP(一)

书接上文,我们已经把对象交给了 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 的"坑",都会突然变得合理:

  1. 构造方法无法被增强
    → 因为代理发生在构造方法之后
  2. private 方法不生效
    → 代理无法拦截私有方法调用
  3. 同类内部方法调用失效
    → 调用没有经过代理对象
  4. @Transactional 偶尔失效
    → 本质仍然是"没走代理"

这些都不是 Spring 的 bug,

而是代理机制的自然结果。

九、把 AOP 放回 Bean 生命周期中再看一遍

现在,我们把 AOP 放回到 Bean 生命周期中,重新串一遍完整流程:

  1. 创建 BeanDefinition
  2. 实例化对象
  3. 注入依赖
  4. 执行初始化逻辑
  5. 判断是否需要 AOP 增强
  6. 如果需要,创建代理对象,替换原始 Bean
  7. 对外暴露的,始终是代理对象

所以,Spring AOP 从来都不是一套神秘的黑魔法

它只是:在 Bean 初始化完成之后,用代理对象,接管了方法调用过程

也就是那句老话:

你以为你在调方法,其实你一直在走代理。


相关推荐
麦兜*2 小时前
SpringBoot进阶:深入理解SpringBoot自动配置原理与源码解析
java·spring boot·spring·spring cloud
慕白Lee2 小时前
项目JDK17+SpringBoot3.0升级
java·ide·intellij-idea
之歆9 小时前
Spring AI入门到实战到原理源码-MCP
java·人工智能·spring
yangminlei9 小时前
Spring Boot3集成LiteFlow!轻松实现业务流程编排
java·spring boot·后端
qq_318121599 小时前
互联网大厂Java面试故事:从Spring Boot到微服务架构的技术挑战与解答
java·spring boot·redis·spring cloud·微服务·面试·内容社区
J_liaty9 小时前
Spring Boot整合Nacos:从入门到精通
java·spring boot·后端·nacos
Mr__Miss9 小时前
保持redis和数据库一致性(双写一致性)
数据库·redis·spring
阿蒙Amon10 小时前
C#每日面试题-Array和ArrayList的区别
java·开发语言·c#
daidaidaiyu10 小时前
Spring IOC 源码学习 一文学习完整的加载流程
java·spring