主题
AOP 与事务机制:代理、传播与失效
本文是 Spring 系统学习系列的 L2 核心篇。前置:Bean 生命周期与扩展点全家桶。学完可以配合面试题食用:Spring AOP 原理:JDK 动态代理 vs CGLIB、@Transactional 失效场景大全、Spring 事务传播机制
横切逻辑:AOP 要解决的问题
翻一遍订单服务的代码:创建订单、查询订单、取消订单,三个方法的开头都是"记录入参日志",结尾都是"捕获异常上报"。日志、事务、权限校验、接口耗时统计,这类逻辑散落在每个业务方法里,既重复又把业务代码搅得很乱。AOP(Aspect-Oriented Programming)的思路是:把这类横切多个业务方法的逻辑抽出来,在容器装配 Bean 时动态织入,业务代码只留业务。
上一篇讲 Bean 生命周期时留过一个口子:AOP 代理是在 BeanPostProcessor 的后置阶段生成的。这一篇把这个口子展开:代理怎么来的、切面怎么写、声明式事务怎么架在 AOP 之上,以及为什么事务会"加了注解却不生效"。
JDK 动态代理 vs CGLIB:两条生成代理的路
Spring 生成代理有两条路,AbstractAutoProxyCreator 会替你选:
- JDK 动态代理:基于接口。运行时用 JDK 自带的 Proxy.newProxyInstance 造一个实现了同样接口的新类,方法调用转发给 InvocationHandler。前提是目标类至少有一个接口。
- CGLIB:基于继承。运行时生成目标类的子类,重写非 final 的方法,调用经 MethodInterceptor 拦截。不要求接口,但 final 方法和 private 方法拦不住,final 类直接没法代理。
选择规则按 Spring 版本分两段记忆:
- 传统 Spring(proxyTargetClass=false 时):目标类实现了接口 -> JDK 动态代理;没有接口 -> CGLIB。
- Spring Boot 2.x 起,
spring.aop.proxy-target-class默认 true,统一走 CGLIB。原因很实际:如果一部分 Bean 用 JDK 代理、另一部分用 CGLIB,注入类型就得小心翼翼(JDK 代理只能按接口注入,代理对象无法转型为具体类);统一 CGLIB 后按具体类注入也成立,减少一类"BeanNotOfRequiredTypeException 启动报错。代价是继承 final 类或被 final 方法命中时无法代理。
一个常被忽略的细节:无论哪条路,代理对象持有目标对象的引用,业务逻辑仍在目标对象里执行,代理只负责"前后加菜"。
五种通知:用一个日志切面写全
AOP 的三个核心概念各一句话:Pointcut 回答"拦哪些方法",Advice 回答"拦住之后干什么",Aspect 是把两者组装起来的类。下面这个日志切面把五种 Advice 用全:
java
@Aspect
@Component
public class LoggingAspect {
// Pointcut:service 包下所有类的所有方法
@Pointcut("execution(* com.example.service..*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void beforeLog(JoinPoint jp) {
System.out.println("[before] 进入 " + jp.getSignature().toShortString());
}
@AfterReturning(pointcut = "serviceMethods()", returning = "ret")
public void afterReturn(JoinPoint jp, Object ret) {
System.out.println("[afterReturning] 正常返回: " + ret);
}
@AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
public void afterThrow(JoinPoint jp, Exception ex) {
System.out.println("[afterThrowing] 抛异常: " + ex.getMessage());
}
@After("serviceMethods()")
public void after(JoinPoint jp) {
System.out.println("[after] 相当于 finally,无论正常与否都执行");
}
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed(); // 放行目标方法
} finally {
System.out.println("[around] 耗时 " + (System.currentTimeMillis() - start) + "ms");
}
}
}执行顺序记两条就够:正常路径是 @Around 前半 -> @Before -> 目标方法 -> @AfterReturning -> @After -> @Around 后半;异常路径把 @AfterReturning 换成 @AfterThrowing,@After 依然执行。同一个方法同时被 @Around 和 @Before 增强时,@Around 在最外层。
事务:架在 AOP 之上的声明式方案
Spring 的事务抽象核心是 PlatformTransactionManager 接口,getTransaction/commit/rollback 三个方法。它的实现按数据访问技术分家:JdbcTemplate 用 DataSourceTransactionManager,JPA 用 JpaTransactionManager,MyBatis 底层走 JDBC,同样归 DataSourceTransactionManager 管。上层 API(@Transactional、TransactionTemplate)只对接接口,切换存储技术不碰业务代码。
@Transactional 的解析时机在 Bean 初始化完成后:InfrastructureAdvisorAutoProxyCreator(一个 BeanPostProcessor)扫描事务相关的 Advisor,给带注解的 Bean 生成代理,织入 TransactionInterceptor。调用一个 @Transactional 方法时,拦截器先向 TransactionManager 开事务,再反射调用目标方法,正常返回就 commit,抛出匹配的异常就 rollback。所以声明式事务的本质就是一次 AOP 增强,凡是用过代理的规则这里全部适用,凡是不走代理的路径这里全部失效。
mermaid
sequenceDiagram
participant C as 调用方
participant P as 代理对象
participant T as TransactionInterceptor
participant D as DataSource
C->>P: orderService.createOrder()
P->>T: 拦截,解析 @Transactional
T->>D: getConnection() + setAutoCommit(false)
T->>P: 反射调用目标方法
T->>D: commit 或 rollback
T-->>C: 返回结果传播行为:只精讲三个
事务传播行为回答"一个方法开在另一个事务里运行时,事务边界怎么划"。七种里常用的就三个:
- REQUIRED(默认):有事务就加入,没有就新建。适合绝大多数场景:订单服务调库存服务,两者同进一个事务,任一处抛异常整体回滚。
- REQUIRES_NEW:不管外层有没有事务,挂起它,自己新开一个,内外互不影响。典型场景是"主流程失败也要留痕":创建订单失败抛异常后仍要写一条失败记录,如果用默认传播,主事务回滚会把日志一起带走,REQUIRES_NEW 保证日志事务独立提交。
- NESTED:在外层事务里设一个保存点(savepoint),内层回滚只回到保存点,外层可以选择继续提交或整体回滚。适合批量处理:循环导入一万条数据,某条失败只回滚这一条,前面的成果保留。注意它依赖 JDBC savepoint,DataSourceTransactionManager 支持,JPA 的事务管理器不支持。
动手实操:自调用失效复现与修复
必含代码两块:先复现自调用失效,再给两种修复。
复现:同一个类里,非事务方法调用事务方法。
java
@Service
public class OrderService {
public void createOrder() {
// this 是原始对象,不是代理,事务拦截器根本没机会介入
this.saveOrder();
}
@Transactional
public void saveOrder() {
orderMapper.insert(order);
inventoryMapper.deduct(order.getSkuId());
// 若此处抛异常,insert 和 deduct 不会一起回滚——事务没开
}
}调用链拆开看:外部调 proxy.createOrder(),代理一看 createOrder 没有 @Transactional,直接放行到目标对象;方法体里 this.saveOrder() 的 this 指向目标对象,绕开了代理,TransactionInterceptor 没有介入机会,每条 SQL 各自 autoCommit。
修复一:注入自身代理。
java
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入的是代理对象
public void createOrder() {
self.saveOrder(); // 走代理,事务生效
}
@Transactional
public void saveOrder() { ... }
}修复二:AopContext。启动类加 @EnableAspectJAutoProxy(exposeProxy = true),把当前代理放进 ThreadLocal:
java
public void createOrder() {
((OrderService) AopContext.currentProxy()).saveOrder();
}两种修复各有代价:注入自身依赖三级缓存解循环依赖,可读性尚可;AopContext 需要显式开 exposeProxy 且强转,容易在运行时才暴露问题。更干净的办法是拆类——把 saveOrder 挪到独立的 Repository/Service,从根源上消灭自调用。
常见误区与小结
- CGLIB 能救自调用:不能。this 在 Java 语法层面就指向原始对象,换代理方式不影响 this 的指向。
- private 方法加 @Transactional 会生效:不会。CGLIB 靠重写方法来拦截,private/final 方法重写不了,注解静默失效。
- try-catch 吞掉异常事务就不回滚:拦截器是根据抛出到它那层的异常做决策的,异常在方法内部被吞,拦截器只看到正常返回,照常 commit。要么别吞,要么手动 setRollbackOnly。
- rollbackFor 默认值包打天下:默认只回滚 RuntimeException 和 Error,受检异常(如 IOException)默认提交。习惯写
@Transactional(rollbackFor = Exception.class)。 - 传播行为选 REQUIRES_NEW 一定更安全:不一定。它占两个物理连接,高并发下容易出现连接池耗尽,且内外事务操作同一行数据会互相等锁。
小结:这一篇把 AOP 代理的生成、切面的写法、事务拦截器的位置串成了一条线,所有失效场景最后都归到同一句话——没走代理。下一篇请求全链路:DispatcherServlet 到响应返回往 Web 层走,看一个 HTTP 请求在 Spring MVC 里经过哪些站。
参考
- Spring Framework 官方文档:Core - AOP(ProxyFactory、@AspectJ 支持)
- Spring Framework 官方文档:Data Access - Transaction Management
- 源码入口:AbstractAutoProxyCreator#postProcessAfterInitialization、TransactionInterceptor#invoke