← 返回 Java 后端知识路线
阶段 04Spring 与后端

Spring AOP:代理如何把日志和事务接进调用链

用方法计时和事务边界示例理解切点、通知、代理与自调用陷阱,知道 AOP 什么时候值得用。

第 26 / 35 篇
AOP代理切点事务

先看这一课值不值得学

学完后,你手里多了哪些代码积木

在不改业务方法的情况下统一处理横切逻辑。

本课正式新增

语法 / API / 命令你必须会到什么程度
@Aspect / @Pointcut声明切面和匹配范围
@Before / @AfterReturning在连接点前后执行通知
@Around / ProceedingJoinPoint包围调用并决定是否继续

本课只借用,先别硬背

  • 事务代理只作例子,第 20 课的事务原理仍是基础

学完必须能独立写

  • 统一记录方法耗时和异常
  • 解释自调用为何可能绕过 Spring 代理
本课目录
  1. 1. 现实问题:每个 Service 都复制日志和事务代码
  2. 2. 最小可运行示例:围绕 Service 方法计时
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:事务、日志和审计的分工
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:每个 Service 都复制日志和事务代码

文章查询、发布和归档都需要记录耗时,写操作还需要事务。如果把开始时间、try-catch、commit 复制到每个方法,主业务会被横切代码淹没。AOP 通过代理在目标方法前后插入通用行为,但它不是把任意代码“自动执行”,而是要求调用经过匹配的代理。

最重要的排查问题是:调用方手里的对象是目标对象还是代理?方法是否匹配切点?异常有没有穿过通知?只有这些问题能回答,AOP 才不是玄学。

2. 最小可运行示例:围绕 Service 方法计时

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
class TimingAspect {
    @Around("execution(* com.example.article..*(..))")
    Object time(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        try {
            return joinPoint.proceed();
        } finally {
            long elapsed = System.nanoTime() - start;
            System.out.println(joinPoint.getSignature() + " took " + elapsed + "ns");
        }
    }
}

切点包名只是示例,真实项目应尽量匹配明确的 Service 注解或命名边界,避免拦截健康检查、内部工具和框架方法。finally 让成功和异常都记录耗时,但日志字段要包含 trace id、方法名和耗时,不要把参数全文打出来。

3. 调用链与对象变化

容器创建 ArticleService 后,自动代理创建器判断它是否匹配切点;若匹配,容器把代理引用注入 Controller。Controller 调用的是代理对象,代理进入 Around 通知,通知调用 proceed 才进入真实目标方法,目标返回值再沿链返回;异常则向外传播并经过 finally。

如果 ArticleService 内部直接调用 this.publish(),调用没有经过代理,外层通知、事务或缓存可能不会执行。注入自身代理是一个应谨慎使用的补救,更好的做法是拆出协作者,让跨边界调用自然经过容器引用。

4. 为什么这样设计

日志、事务、权限和缓存横跨多个业务方法,适合 AOP 统一治理;领域规则、状态迁移和核心计算不应隐藏在切面里,否则阅读一个方法无法知道它的副作用。切点要窄、通知要可观察、顺序要有文档。

Spring AOP 主要是运行时代理:接口场景可以使用 JDK 动态代理,类场景可能使用子类代理。final 类、final 方法、私有方法和内部调用都可能不按你期待的方式被拦截。AOP 不是编译器魔法,代理边界是设计的一部分。

5. 项目落点:事务、日志和审计的分工

发布文章的事务放在外部 Service 方法,审计记录属于事务内的数据库操作;发送邮件、埋点和缓存失效按提交后事件处理。性能切面记录方法和数据库耗时,错误切面统一转译异常,业务代码只返回领域结果。若切面决定权限,必须有独立安全测试。

练习:给 @UseCase 注解写切点,只记录带该注解的公共 Service;写一个成功、抛异常和 self-invocation 测试,确认哪些通知执行。再比较把计时写进方法和用 AOP 的日志字段、可测试性和排错体验。

6. 易错排查

  • 切面没有执行:检查 AOP 依赖、切点表达式、Bean 是否由容器管理、调用是否经过代理。
  • 事务不回滚:异常被 catch、异常类型不匹配或调用没有经过事务代理。
  • 切点过宽:日志量暴增或递归拦截,先收窄包和注解范围。
  • 自调用绕过通知:拆分 Bean 或通过外部协作者调用,别把代理细节散到业务代码。

7. 一页复习

AOP 的调用链是调用方 → 代理 → 通知 → proceed → 目标 → 返回/异常。它适合横切关注点,不应隐藏核心业务。先确认代理身份、切点命中和异常传播,再判断配置是否正确。事务的可靠性来自边界和测试,不是注解出现了就成功。

把 AOP 的对象状态画成嵌套引用:Controller 持有的是 ArticleService 代理,代理持有目标 Service 和通知链,通知的 proceed 才得到目标返回值。计时通知在 finally 读取耗时,事务通知可能在外层决定提交/回滚,缓存通知可能在目标执行前返回。顺序不同,输入输出和副作用就不同。

项目中可以用 @UseCase 缩小切点,TimingAspect 放 observability,事务放 application service 的边界,安全放 SecurityFilterChain。切面不要直接改变 Article 状态,也不要在通知里偷偷调用另一个写接口。方法是否经过代理、异常是否穿透、异步线程是否带 traceId,都应该有集成测试而不是只看注解。

排错时通知完全不执行,先确认目标是否为容器 Bean、AOP 依赖和切点签名;只执行部分通知,查 self-invocation、private/final 方法和代理类型;事务提交但业务失败,查异常是否被 catch 或转换为正常返回;日志耗时异常,查通知是否把外部等待、数据库等待和序列化分别记录。不要先把切点扩大到 execution(* *(..))

练习是为 publish 写计时、权限和事务的时序测试:成功时记录进入/退出,领域异常时确认 rollback,内部调用时确认是否绕过代理。再把 AOP 日志字段映射到实际项目的 traceId、articleId 和 status,验证线上可以从一次请求定位到目标方法。

代理链中的输入输出需要逐层记录:Controller 传入 article id,权限通知可能先拒绝;事务通知打开连接,缓存通知可能命中并直接返回;未命中后目标 Service 查询并改变数据库;退出时事务决定提交,计时通知写耗时。任何通知都能改变是否进入下一层,所以切面顺序属于 API 行为的一部分。

Spring AOP 的代理类型影响可拦截范围。接口引用可能由 JDK proxy 承载,类代理依赖可重写方法;private、final 和同类内部调用不经过外部代理。看到 @Transactional@Cacheable 或自定义 @Around 不生效,先确认调用者持有的是代理,别先重写业务逻辑。拆出协作者通常比注入自身代理更清楚。

项目中日志/指标切面应放 observability,事务和缓存放 application/config,授权放 Security,领域规则放 domain。切面只做横向动作并保留异常,不应该把“文章是否能发布”藏在一个全局切面里。集成测试验证代理链,Service 单测验证核心规则,两者不能互相替代。

排错时切点没命中查包、注解、Bean 管理和方法可见性;通知顺序错查 @Order 与外层/内层时序;事务不回滚查异常传播和 self-invocation;日志时间异常查是否包含数据库、网络和序列化。练习是给同一发布方法写成功、领域异常、缓存命中和内部调用四条时序,标出每个通知是否执行。

验证清单:对容器取出的 ArticleService 打印接口/代理类型,调用一次外部方法并记录权限、事务、目标、缓存、计时的进入与退出顺序;再让目标抛领域异常,断言异常原样到达处理层且事务按规则回滚。用同类内部调用和拆出的协作者各跑一次,比较通知是否命中;用缓存命中路径确认目标方法没有被错误执行。复盘时画出每个代理持有的目标引用和 proceed 返回值,若一个切面改变了核心结果或吞掉异常,就把它移回业务层或修正通知契约。

进阶附录:多个通知的顺序

日志、权限、事务和缓存的顺序会改变结果,例如权限应先于数据库访问,事务应覆盖写操作,缓存更新要考虑提交时机。用 @Order 只是建立一个顺序数字,仍需要时序图和集成测试证明组合行为。

本课按「Spring Framework 7 AOP、代理与横切关注点」的学习范围组织,正文与示例均为本站原创整理。