本专题导航
| 篇 | 主题 |
|---|---|
| 上一篇 | 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@Componentpublic 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 → 返回给调用方
为什么 @Autowired 注入的 Service 上事务能生效?
因为注入的是容器里的 Proxy Bean;代理在 Bean 初始化后、由 BeanPostProcessor 创建(见 IoC 篇 生命周期第 8 节)。
为什么同类自调用事务失效?
this.transfer() 是目标对象内部直接调用,不经过 Proxy,事务拦截器根本不会执行。
@Servicepublic 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 起默认倾向 CGLIB(spring.aop.proxy-target-class=true),即使实现了接口也可能用子类代理。面试答「有接口优先 JDK,无接口 CGLIB」即可;实际以项目配置为准。
限制:final 类/方法无法 CGLIB;JDK 代理只能代理接口方法。
5. 声明式事务 @Transactional
@Servicepublic 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 |
默认回滚规则:仅 RuntimeException 和 Error 回滚;受检异常(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. 只捕获异常不抛出
@Transactionalpublic 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」和三种传播行为,大部分坑都能避开。