Spring AOP 与事务

本专题导航#

主题
上一篇IoC 与依赖注入
本篇AOP 与事务
下一篇Spring MVC

背景与动机#

IoC 解决的是「谁创建 Bean、谁注入依赖」;AOP 解决的是「横切逻辑放哪」——日志、权限、事务、监控如果写在每个业务方法里,代码重复且难维护。Spring 的声明式事务 @Transactional 本质上就是 AOP + 事务拦截器,不搞懂代理,就很难理解事务为什么有时「加了注解却不生效」。

→ 前置:IoC 与依赖注入(AOP 代理在 Bean 生命周期 BeanPostProcessor 后置阶段生成)

核心原理拆解#

1. 为什么需要 AOP#

没有 AOP 时,每个 Service 方法都要手写相同套路:

public void transfer(...) {
log.info("enter transfer");
TransactionStatus tx = txManager.begin();
try {
// 业务 SQL
txManager.commit(tx);
} catch (Exception e) {
txManager.rollback(tx);
throw e;
}
}

横切关注点(Cross-cutting Concern):与具体业务无关、却散布在很多方法上的逻辑。AOP 把这些逻辑抽到切面(Aspect),在匹配的切点(Pointcut)上统一织入通知(Advice)

Spring AOP 基于运行时代理实现,不修改业务类源码;与 IoC 配合:容器注入的往往是代理对象,而不是原始 Target。

2. AOP 核心概念#

术语含义类比
切面(Aspect)横切逻辑的模块,含切点 + 通知一套「事务/日志规则」
连接点(Join Point)程序里可被拦截的点(Spring AOP 主要是方法执行路口
切点(Pointcut)匹配哪些连接点哪些路口要设卡
通知(Advice)切点上执行的动作卡口的具体动作
织入(Weaving)把切面逻辑应用到目标把规则挂到路口上

通知类型

类型时机常见用途
@Before方法前参数校验、鉴权
@AfterReturning正常返回后记录结果
@AfterThrowing抛异常后异常告警
@After方法后(finally 语义)清理资源
@Around环绕,可控制是否 proceed事务、计时、缓存(能力最强)
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void logBefore(JoinPoint jp) {
log.info("调用: {}", jp.getSignature().getName());
}
}

Spring AOP 是方法级别的,不能像 AspectJ 编译期织入那样拦截字段赋值等所有连接点;对 Web 开发而言通常够用。

3. Spring AOP 工作原理:代理(核心)#

外部 Bean 调用 service.transfer()
→ 实际进入 Proxy
→ 事务 Advice:开启事务
→ 调用 Target.transfer() 执行业务
→ 正常则 commit,异常则 rollback
→ 返回给调用方

img
img

为什么 @Autowired 注入的 Service 上事务能生效?
因为注入的是容器里的 Proxy Bean;代理在 Bean 初始化后、由 BeanPostProcessor 创建(见 IoC 篇 生命周期第 8 节)。

为什么同类自调用事务失效?
this.transfer() 是目标对象内部直接调用,不经过 Proxy,事务拦截器根本不会执行。

@Service
public class AccountService {
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) { /* ... */ }
public void pay() {
this.transfer(); // ✗ 自调用,事务不生效
}
}

修复思路:把 transfer 抽到另一个 @Service;或注入自身接口代理;或 AopContext.currentProxy()(需开启 @EnableAspectJAutoProxy(exposeProxy = true))。

4. JDK 动态代理 vs CGLIB#

JDK 动态代理CGLIB
条件目标类实现接口可代理类(类、方法非 final
方式基于接口生成代理对象生成目标类的子类
性能创建快,调用略慢创建慢,调用略快
Spring 选择有接口时默认优先 JDK无接口或 proxyTargetClass=true 时用 CGLIB

Spring Boot 2.x 起默认倾向 CGLIBspring.aop.proxy-target-class=true),即使实现了接口也可能用子类代理。面试答「有接口优先 JDK,无接口 CGLIB」即可;实际以项目配置为准。

限制final 类/方法无法 CGLIB;JDK 代理只能代理接口方法。

5. 声明式事务 @Transactional#

@Service
public class AccountService {
@Transactional(rollbackFor = Exception.class)
public void transfer(Long from, Long to, BigDecimal amount) {
accountMapper.debit(from, amount);
accountMapper.credit(to, amount);
}
}

Spring 不直接在业务代码里写 begin/commit,而是由 TransactionInterceptor(一种 Advice)在代理方法前后管理事务,底层通过 PlatformTransactionManager(常见 DataSourceTransactionManager)绑定到 Connection

常用属性

属性含义
propagation传播行为(默认 REQUIRED
isolation隔离级别(默认 DEFAULT,跟数据库)
rollbackFor / noRollbackFor哪些异常回滚
timeout超时秒数
readOnly只读优化 hint

默认回滚规则:仅 RuntimeExceptionError 回滚;受检异常(Exception 子类但非 Runtime)默认不回滚,需显式 rollbackFor = Exception.class

6. 事务传播行为(常考)#

传播行为解决:一个事务方法调用另一个事务方法时,事务边界怎么合并或拆分

传播行为含义典型场景
REQUIRED(默认)有事务就加入,没有就新建日常业务,外层包内层
REQUIRES_NEW总是新建,挂起当前事务记审计日志,主事务回滚不影响日志
NESTED嵌套事务(保存点)部分失败只回滚内层(需 DB 支持)
SUPPORTS有就加入,没有就非事务执行查询可事务可非事务
NOT_SUPPORTED非事务执行,挂起当前强制不走事务
MANDATORY必须在事务内,否则抛异常强制被外层事务调用
NEVER禁止在事务内执行校验方法不能跑在事务里

记忆:先记三个就够 —— REQUIRED(默认合体)、REQUIRES_NEW(独立新开)、NESTED(嵌套保存点)。

外层 REQUIRED + 内层 REQUIRED → 同一个物理事务
外层 REQUIRED + 内层 REQUIRES_NEW → 两个独立事务,互不影响提交/回滚
外层 REQUIRED + 内层 NESTED → 同一连接上的保存点,内层 rollback 可只回滚到保存点

7. 事务隔离级别#

与 JDBC / 数据库一致,通过 @Transactional(isolation = ...) 指定:

级别脏读不可重复读幻读
读未提交可能可能可能
读已提交不会可能可能
可重复读不会不会可能
可串行化不会不会不会

8. @Transactional 失效常见原因#

原因说明
方法非 public代理无法拦截 protected/private
同类自调用this.xxx() 未走 Proxy
异常被 catch 未抛出代理层看不到异常,不会 rollback
受检异常未配置 rollbackFor默认不回滚
类未被 Spring 管理非 Bean,new 出来的无效
数据库引擎不支持事务如 MyISAM

常见陷阱与错误示例#

1. 在 private 方法上加 @Transactional#

Spring 默认用代理模式,private 方法无法被代理拦截,事务不生效。应标在对外 public 业务方法上。

2. 只捕获异常不抛出#

@Transactional
public void update() {
try {
mapper.step1();
mapper.step2();
} catch (Exception e) {
log.error("failed", e); // 异常被吞,事务仍 commit
}
}

需要回滚时必须重新抛出或配置 rollbackFor 且让异常传到代理层。

3. 只读方法误用写传播#

查询方法标 @Transactional(readOnly = true) 可避免脏检查、优化连接;写操作不要标 readOnly。

4. 多数据源未指定事务管理器#

存在多个 TransactionManager 时,需 @Transactional(transactionManager = "xxx") 指定。

面试高频问题#

更多速查见 Spring 面试题

Spring AOP 和 AspectJ 区别?
Spring AOP 运行时代理、方法级、与 Spring 容器集成好;AspectJ 支持编译期/加载期织入,能力更强但更复杂。Spring 事务、日志多用 Spring AOP 够用。

JDK 代理和 CGLIB 区别?
JDK 基于接口;CGLIB 基于子类。Spring 按配置与是否有接口选择;final 无法 CGLIB。

@Transactional 原理?
通过 AOP 代理在方法前获取连接并 begin,方法后 commit,异常时 rollback;本质是 TransactionInterceptor + PlatformTransactionManager

为什么自调用事务失效?
this 调用的是 Target 本身,不经过容器注入的 Proxy,Advice 未执行。

REQUIRED 和 REQUIRES_NEW 区别?
REQUIRED 加入当前事务;REQUIRES_NEW 挂起当前、新建独立事务,内外提交互不影响。

一句话总结#

AOP = 代理 + 切点 + 通知;事务 = @Transactional 经 Proxy 织入;记清「外部走代理、自调用 bypass」和三种传播行为,大部分坑都能避开。

文章目录

文章目录