IoC 容器负责创建并连接 Bean,自动配置则按条件提供框架基础设施。有些能力还需要包围一次方法调用:调用前开启事务,正常返回后提交,失败时回滚;或者记录方法耗时,而不把计时代码复制到每个业务方法中。
Spring AOP 使用代理对象拦截这类 Bean 方法调用。本章通过心跳服务的计时案例理解切面、切点、通知、代理与目标对象,并提前说明代理边界为什么会影响后面的 @Transactional。
1. 横切关注点
业务方法应该首先表达业务流程:
1public HealthStatus check(String service) {2String normalizedService = normalizeService(service);3return new HealthStatus(properties.status(), normalizedService);4}
如果直接在每个方法内部添加耗时、事务或权限代码,业务流程会被重复基础设施逻辑包围:
1Controller 方法:开始计时 -> 业务调用 -> 结束计时2Service 方法:开始计时 -> 业务处理 -> 结束计时3Repository 方法:开始计时 -> SQL 执行 -> 结束计时
这类会跨越多个类和方法的功能称为横切关注点。常见例子包括:
- 事务边界;
- 方法耗时与指标;
- 权限检查;
- 缓存;
- 重试。
AOP 适合包装具有明确边界的通用行为,不适合隐藏正常业务步骤。HealthService 仍然应该显式调用 Repository,不能因为使用切面就让核心业务关系从代码中消失。
2. 切面术语
Spring AOP 中几个术语分别描述「在哪里」与「执行什么」:
| 术语 | 含义 | 当前案例 |
|---|---|---|
| Aspect | 一组横切逻辑 | HealthTimingAspect |
| Join point | 可以被拦截的方法执行位置 | HealthService.check() 的执行 |
| Pointcut | 选择哪些连接点 | 匹配 HealthService 的 check 方法 |
| Advice | 在匹配位置执行的逻辑 | 调用前后记录耗时 |
| Target | 真正包含业务实现的对象 | 原始 HealthService |
| Proxy | 调用方实际拿到的包装对象 | 容器生成的 HealthService 代理 |
一次代理调用可以表示为:
1HealthController2-> HealthService 代理3-> 匹配切点4-> 执行环绕通知的前半段5-> 调用目标 HealthService.check()6-> 执行环绕通知的后半段7-> 返回 Controller
通知还可以只在调用前、正常返回后或抛出异常后运行。本章使用环绕通知,是因为计时需要同时覆盖方法开始与结束,并确保异常路径也能记录耗时。
3. 创建计时切面
项目需要加入 Spring Boot 4 提供的 AspectJ Starter。它包含 Spring AOP 使用 @Aspect 切点表达式所需的 AspectJ Weaver,并触发 AOP 自动配置:
1<dependency>2<groupId>org.springframework.boot</groupId>3<artifactId>spring-boot-starter-aspectj</artifactId>4</dependency>
然后创建计时切面:
01package cn.usehook.spring.health;0203import org.aspectj.lang.ProceedingJoinPoint;04import org.aspectj.lang.annotation.Around;05import org.aspectj.lang.annotation.Aspect;06import org.slf4j.Logger;07import org.slf4j.LoggerFactory;08import org.springframework.stereotype.Component;0910@Aspect11@Component12public class HealthTimingAspect {1314private static final Logger log = LoggerFactory.getLogger(HealthTimingAspect.class);1516@Around("execution(* cn.usehook.spring.health.HealthService.check(..))")17public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {18long startedAt = System.nanoTime();1920try {21return joinPoint.proceed();22} finally {23long elapsed = System.nanoTime() - startedAt;24log.info("health_check_elapsed_ns={}", elapsed);25}26}27}
@Aspect 声明这个类包含切面规则,@Component 让组件扫描把它注册为 Bean。@Around 中的表达式匹配 HealthService 上名称为 check 的任意重载方法,(..) 表示参数数量和类型不限。
joinPoint.proceed() 才会继续执行目标方法。忘记调用它,相当于切面直接截断业务调用;调用多次则会让业务方法执行多次。这里在 finally 中记录耗时,因此正常返回和抛出异常都会执行日志代码,原异常仍然继续传播。
这个切面适合观察机制。生产指标通常应交给 Micrometer 记录直方图和标签,而不是只把每次耗时写成普通日志;后面的可观测性章节会继续处理。
4. 代理类型
Spring AOP 主要使用两种运行时代理:
| 代理方式 | 基础 | 特点 |
|---|---|---|
| JDK 动态代理 | Java 接口 | 代理实现相同接口,调用方应通过接口类型使用 |
| 类代理 | 目标类的子类 | 可以代理没有接口的类,但受 final 等继承规则限制 |
当前 HealthService 没有单独接口,因此 Spring Boot 默认的类代理方式可以包装它。Controller 构造参数仍然写 HealthService,容器实际注入的是可以赋给该类型的代理对象。
1private final HealthService healthService;23public HealthController(HealthService healthService) {4this.healthService = healthService;5}
是否为了使用代理而创建接口,要回到业务抽象本身判断。只有一个实现且没有替换边界的 Service 不需要机械增加接口;类代理能够处理这种情况。
类代理不能覆盖 final 方法,也不能通过继承代理 private 方法。Spring AOP 关注的是经过代理对象的 Bean 方法调用,不是改写任意 Java 指令。
5. 调用边界
代理只能拦截经过代理对象的调用。对象在自身方法中使用 this 调用另一个方法时,调用不会先返回外层代理:
1public void checkAll() {2check("spring");3}45public HealthStatus check(String service) {6// 业务逻辑7}
外部调用代理的 checkAll() 后,内部的 check() 是目标对象自身调用。如果切点只匹配 check(),这次内部调用不会触发对应代理通知。
当前事务文章会让 Controller 直接调用带 @Transactional 的公开 check() 方法,并把复用逻辑放入不要求独立事务的私有方法。这样外部调用先经过代理,事务边界才会建立。
还要注意以下边界:
- 使用
new HealthService(...)手动创建的对象不由容器代理; private方法不能成为普通代理的外部调用入口;- 同类自调用不会重新经过代理;
- 在构造方法中调用业务方法时,代理和 Bean 初始化可能尚未完成;
- 捕获并吞掉异常会让事务等外层通知误以为方法正常返回。
因此,注解只是提供元数据。真正执行横切逻辑的是容器创建的代理、拦截器以及背后的事务管理器或缓存管理器。
6. 顺序与验证
同一个方法可能同时匹配计时、事务和其他切面。可以使用 @Order 或 Ordered 明确切面优先级;数值越小,通常越靠近调用链外层,返回时则越晚退出。
1低 order 值切面:进入2-> 高 order 值切面:进入3-> 目标方法4-> 高 order 值切面:退出5低 order 值切面:退出
只有当先后顺序会改变语义时才显式设置,不要依赖未声明的偶然顺序。例如计时是否包含获取数据库连接的等待时间,要由监控目标决定。
启动应用并请求心跳接口,可以观察计时日志:
1curl http://localhost:8080/api/v1/health
1health_check_elapsed_ns=483920
如果日志没有出现,应依次确认切面是否位于组件扫描范围、AOP Starter 是否存在、切点是否匹配公开方法,以及调用是否真的经过容器注入的 Bean。
Spring AOP 的核心不是背诵切点表达式,而是分清调用方拿到的代理与真正执行业务的目标对象。后面的事务、缓存和监控都会建立在这个模型上。下一篇进入 Spring MVC,沿一次 HTTP 请求观察 Web 层如何找到 Controller 并转换参数与响应。