SPRING AOP · @TRANSACTIONAL · JDK 8 / 17
你调用的,真是它本人吗?
Spring 递给你的 Bean,很多时候是个替身。日志、权限、缓存、事务,都是这个替身顺手替你做的。
Proxy站在真身前面。所有调用先到他这儿,他照着规矩加点料,再转给真身。
Spring 启动时生成 · CGLIB 或 JDK 代理Aspect只写两件事:管哪些方法(切点),什么时候做什么(通知)。经纪人照着办。
一个 @Aspect 类就是一本@Transactional方法里的改动先写在同一张草稿上。全部成功一起生效,出错整张撕掉。
Spring 自带的一条规矩AOP 的六个词
听着玄。换成大白话:经纪人拿着规矩本,在合适的时刻插一手。
Aspect规矩本。一个 @Aspect 类就是一本。
Pointcut名单:管哪些方法。execution(* com.shop.service..*.*(..))
Advice什么时候做什么。一共五种,见右图。
JoinPoint能插手的时刻。Spring AOP 只认“方法被调用”这一种。
Proxy经纪人。真正接电话的那个对象。
Weaving把规矩缝到经纪人身上。Spring 在启动时做,AspectJ 在编译时做。
@Around包住全程,还能决定要不要叫醒真身(不调 proceed() 就不调)@Before进门前@AfterReturning正常返回后@AfterThrowing抛异常后@After不管成败都执行,像 finally跟着一次调用走一趟
选一个剧情,点「下一步」。留意数据库里那几行:是草稿、已提交、已撕掉,还是不该留下却留下了。
ControllerProxy@Order(1) 外层@TransactionalOrderService同一个连接- 还没写入
orderService.placeOrder(cmd)替身什么时候套上
Bean 出生的流水线上,第 4 站给它穿上外套。之后谁来要这个 Bean,拿到的都是替身。
- 1实例化
new OrderService() - 2属性注入
@Autowired - 3初始化
@PostConstruct - 4套上替身有规矩点名就生成代理
BeanPostProcessor - 5进容器单例池里存的是替身
- 6被注入别人拿到的是替身
- 7销毁
@PreDestroy
@PostConstruct 里开不了事务
第 3 站时还没穿外套,而且是 this 调用。启动后要做事,改用 ApplicationRunner,从另一个 Bean 里调。
怎么确认拿到的是替身
打印 orderService.getClass(),看到 OrderService$$SpringCGLIB$$0 就是;或者用 AopUtils.isAopProxy(orderService)。
一次 @Transactional 调用里,草稿本的一生
setAutoCommit(false)
绑到当前线程
执行方法
commit()或rollback()
解绑,还连接
提交后回调
连接绑在线程上(ThreadLocal)。同一线程里的 Mapper、Repository 自动拿到同一个连接;换个线程就不是了。想在提交成功后再发消息,用 @TransactionalEventListener,它默认在提交后才触发。
事务的两个旋钮
第一个旋钮管“草稿本怎么共用”,第二个管“能看到别人多少草稿”。
传播行为 propagation:A 开着事务,去调另一个 Bean 的 B
REQUIRED默认
B 写在 A 的纸上。B 出错,整张撕掉。
A 就算 catch 住 B 的异常,提交时也会收到 UnexpectedRollbackException。
REQUIRES_NEW
B 拿一张新纸,自己提交或回滚,再把 A 的纸拿回来。
适合“失败也要留下”的记录。会多占一个连接。
NESTED
同一张纸,先夹一枚书签(savepoint)。B 出错只撕到书签处。
JDBC、MyBatis 支持;JPA(Hibernate)不支持。
SUPPORTS有纸就一起写,没有就不用纸NOT_SUPPORTED把纸收起来,不用事务MANDATORY必须已经有纸,否则报错NEVER有纸就报错隔离级别 isolation:能看到别人多少草稿
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
READ_UNCOMMITTED | 会 | 会 | 会 |
READ_COMMITTED | 防住 | 会 | 会 |
REPEATABLE_READ | 防住 | 防住 | 会(InnoDB 靠 MVCC 和间隙锁挡住了大部分) |
SERIALIZABLE | 防住 | 防住 | 防住 |
默认 Isolation.DEFAULT 就是用数据库自己的:MySQL InnoDB 是 REPEATABLE_READ,PostgreSQL 和 Oracle 是 READ_COMMITTED。其他小旋钮:readOnly = true、timeout = 5(秒)、rollbackFor / noRollbackFor。
三种替身做法
Spring AOP 只会前两种。AspectJ 不找替身,直接改你的类。
| 对比项 | JDK 动态代理 | CGLIB 代理 | AspectJ 织入 |
|---|---|---|---|
| 怎么做 | 造一个实现同样接口的替身 | 造一个子类当替身 | 直接改你类的字节码 |
| 前提 | 必须有接口 | 类和方法不能是 final | 用 ajc 编译,或加 -javaagent |
| 什么时候做 | 启动时 | 启动时 | 编译期或类加载期 |
| 谁默认用它 | 纯 Spring,且类有接口时 | Spring Boot 2.0 起默认;类没有接口时 | 要手动开启 |
| 自己调自己 | 拦不到 | 拦不到 | 拦得到 |
| private、final、static | 拦不到 | 拦不到 | 拦得到 |
| 按实现类注入 | 不行,只能按接口 | 可以 | 可以 |
你天天在用的替身
这些注解自己什么也不做,干活的都是替身。
@Transactional开事务,提交或回滚
@Cacheable先查缓存,命中就不叫醒真身
@Async丢进线程池,马上返回
@PreAuthorize调用前查权限(Spring Security)
@Validated放在类上,检查方法参数
@Retryable失败自动重试(Spring Retry)
你的 @Aspect操作日志、限流、分布式锁、数据权限
@Configuration(例外)它本身就是 CGLIB 子类,背后没有另一个真身,所以 @Bean 方法互相调用也拦得到。
// ① 规矩本:给 service 包里的方法计时
@Slf4j
@Aspect
@Component
@Order(1) // 数字小 → 在外层
public class TimingAspect {
@Pointcut("execution(* com.shop.service..*.*(..))") // 切点:管哪些方法
void serviceMethods() {}
@Around("serviceMethods()") // 通知:怎么管
public Object time(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed(); // 交给下一层,最终到真身
} finally {
log.info("{} 耗时 {}ms", pjp.getSignature().toShortString(),
System.currentTimeMillis() - start);
}
}
}
// ② 草稿本:事务也是替身身上的一条规矩
@Service
public class OrderService {
private final OrderMapper orderMapper;
private final StockMapper stockMapper;
// 构造器注入省略
@Transactional(rollbackFor = Exception.class) // 受检异常也回滚
public Long placeOrder(OrderCmd cmd) {
orderMapper.insert(cmd.toOrder()); // 同一个连接
stockMapper.deduct(cmd.getSkuId(), cmd.getCount()); // 同一个连接
return cmd.getOrderId();
}
}
JDK 8 和 JDK 17:原理不变,细节变了
JDK 8 对应 Spring 5 / Boot 2,JDK 17 对应 Spring 6+ / Boot 3+。替身还是那个替身。
Spring 5 · Boot 2
JDK 8+@Transactional只认 public 方法,加在其他方法上静默失效- 代理类名长这样:
OrderService$$EnhancerBySpringCGLIB$$9f8e7d6c @PostConstruct在javax.annotation包。JDK 11 起它被移出 JDK,Boot 2 已帮你带上依赖- 5.2.7 起,同一个切面里的
@After改到@AfterReturning/@AfterThrowing之后执行
Spring 6+ · Boot 3+
JDK 17+- protected、包可见的方法也能加
@Transactional(CGLIB 代理时);private 依然不行 - 代理类名变短:
OrderService$$SpringCGLIB$$0 - 包名换成
jakarta.annotation.PostConstruct;JTA 的事务注解变成jakarta.transaction.Transactional,日常仍推荐 Spring 自己的@Transactional - 打 GraalVM 原生镜像时,代理类在构建期提前生成(AOT)
@Transactional 失效的 10 种情况
大部分都能归到一句话:调用没经过替身,或者替身不知道出了错。
this.placeOrder() 不经过替身。把方法挪到另一个 Bean,或注入自己(加 @Lazy)再通过它调用。
替身拦不到。Spring 5 里非 public 的方法也不行。
自己 new 出来的对象没有替身。
替身以为一切正常,照样提交。要么继续往外抛,要么调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
默认照样提交。写上 rollbackFor = Exception.class。
两层都是 REQUIRED 时,草稿已被标成“只能撕”,外层提交时抛 UnexpectedRollbackException。
@Async、new Thread、parallelStream 里写库,拿到的不是同一个连接,也就不在同一个事务里。
解锁早于提交,别人会读到旧数据。给锁切面加更小的 @Order,让它包在事务外面。
连接被长时间占着;消息发出去了,事务却回滚了。用 @TransactionalEventListener 等提交后再发。
MySQL 的 MyISAM 表没有事务,换成 InnoDB。