手写一遍 Spring 事务,你就再也不会忘了
手写一遍 Spring 事务,你就再也不会忘了
全文围绕一个名为
aop-demo的小项目展开,六个 demo 从最朴素的"自己写代理"一路演进到 Spring Boot 全自动事务。读完之后,你应该能对面试官讲出来:Spring 事务的本质是 AOP 代理 + ThreadLocal + PlatformTransactionManager,而且每一句都举得出例子。项目代码全部贴在文中,可以直接复制粘贴跑起来。
写在最前
刚学 Spring 的时候,我对事务的理解大概是这样的:
“在方法上加个
@Transactional,出异常了它就帮我回滚。”
这种理解能干活,但只要面试官追一句"那它怎么知道要回滚?",我就开始磕巴了。后来真正搞懂这件事
- demo1:不依赖 Spring 任何高级特性,自己写一个最小可用的事务工具(JDK 动态代理 + ThreadLocal)。
- demo2:把 demo1 中"自己造代理"这一步,交给 Spring AOP。
- demo3:把 AOP 的五种通知一次说清楚。
- demo4:把 JDBC 升级成 JdbcTemplate,顺便复现"无事务导致钱凭空消失"。
- demo5:用 XML 声明式事务把 demo4 的问题修好。
- demo6:把 XML 改成
@Transactional+ JavaConfig,这就是你日常写的代码。
最后再回过头看 Spring Boot 的自动配置,你会发现 demo6 里那个 SpringConfig,Spring Boot 全帮你写好了。
项目准备
先把脚手架搭起来,后面六个 demo 全部跑在这一套配置上。
目录结构
aop-demo/
├── pom.xml
├── README.md
├── src/
│ ├── main/
│ │ ├── java/com/qcbyjy/
│ │ │ ├── AopDemoApplication.java
│ │ │ ├── demo1/ (JDK 动态代理 + 手写事务)
│ │ │ ├── demo2/ (Spring AOP 入门)
│ │ │ ├── demo3/ (AOP 五种通知)
│ │ │ ├── demo4/ (JdbcTemplate 转账案例)
│ │ │ ├── demo5/ (XML 声明式事务)
│ │ │ └── demo6/ (注解事务 + JavaConfig)
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── applicationContext_demo5.xml
│ │ └── sql/init.sql
│ └── test/
│ └── java/com/qcbyjy/test/
│ ├── Demo1Test.java
│ ├── Demo2Test.java
│ ├── Demo3Test.java
│ ├── Demo4Test.java
│ ├── Demo5Test.java
│ └── Demo6Test.java
pom.xml
技术栈是 Spring Boot 2.7.18 + JDK 11 + MySQL 8 + Druid。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.qcbyjy</groupId>
<artifactId>aop-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 8080
spring:
application:
name: aop-demo
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
username: test123
password: test123
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
logging:
level:
root: info
com.qcbyjy: debug
org.springframework.aop: debug
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
spring.aop:
auto: true
proxy-target-class: false
sql/init.sql
USE test;
DROP TABLE IF EXISTS account;
CREATE TABLE account (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
money DECIMAL(10, 2) NOT NULL
);
INSERT INTO account(name, money) VALUES ('熊大', 1000.00);
INSERT INTO account(name, money) VALUES ('熊二', 1000.00);
启动类
注意这里 exclude = DataSourceAutoConfiguration.class,因为 demo1 要自己管理数据源,如果不排除,Spring Boot 启动时会去连数据库,demo1 的演示就不纯粹了。后面 demo4 ~ demo6 用 @SpringBootTest(classes=...) 或 @SpringJUnitConfig 单独装载配置,跟启动类没关系。
package com.qcbyjy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class AopDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AopDemoApplication.class, args);
}
}
开始之前先想清楚一件事:事务到底是什么
我把每一节的"为什么"放在前面,代码贴在后面。读起来可能慢,但你不会迷路。
account 表里有两个账户,熊大和熊二,各 1000 元。"熊大转账给熊二 100 元"这件小事,需要执行两条 SQL:
UPDATE account SET money = money - 100 WHERE name = '熊大';
UPDATE account SET money = money + 100 WHERE name = '熊二';
这两条必须同时成功,或同时失败。否则就会出现一种很灵异的场景:钱从熊大账户扣了,但熊二没收到 —— 100 元凭空消失。
所以事务的本质用一句话讲:
同一个 Connection 上的一系列操作,要么全部 commit,要么全部 rollback。
听起来挺朴素的吧?但里面藏着一个特别容易被忽略的前提:两条 SQL 必须用同一个 Connection。
我们看下面这段错误代码,它有事务的"形",但没有事务的"魂":
public void transfer() {
Connection connA = dataSource.getConnection();
PreparedStatement ps1 = connA.prepareStatement("UPDATE ... money - 100");
ps1.executeUpdate();
connA.close();
Connection connB = dataSource.getConnection();
PreparedStatement ps2 = connB.prepareStatement("UPDATE ... money + 100");
ps2.executeUpdate();
connB.close();
}
JDBC 默认 autoCommit = true,SQL 一发出去就立刻 commit 了。哪怕你后面 connA.rollback(),熊二在 connB 上的入账也早就落库,根本回滚不了 —— 因为它根本不知道 connA 想干嘛。
要让事务成立,必须做到三件事:
- 关掉
autoCommit,SQL 不再自动提交; - 所有 SQL 共用同一个 Connection;
- 调用链结束时统一决定 commit 还是 rollback。
第二点是整个 demo1 的核心矛盾,也是 ThreadLocal 登场的地方。
下面正式开始。
demo1:自己写一遍事务,从根上理解 ThreadLocal
1.1 这个 demo 想说什么
demo1 不依赖 Spring 任何"高级特性",从零写一份事务工具。我希望你写完之后能脱口而出:
“Spring 事务的本质就是 ThreadLocal 绑连接 + 代理拦截方法。”
我们做四件事:
- 用 Druid 当连接池;
- 用
ThreadLocal<Connection>绑定当前线程的连接; - 写一个
JdkProxy,在方法前后开/提/回事务; - 让 Service / DAO 完全不关心事务。
1.2 完整代码
Account.java
实体类,没什么好说的。
package com.qcbyjy.demo1;
public class Account {
private Integer id;
private String name;
private Double money;
public Account() {}
public Account(String name, Double money) {
this.name = name;
this.money = money;
}
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getMoney() { return money; }
public void setMoney(Double money) { this.money = money; }
@Override
public String toString() {
return "Account{id=" + id + ", name='" + name + "', money=" + money + '}';
}
}
AccountDao.java
package com.qcbyjy.demo1;
import java.sql.SQLException;
public interface AccountDao {
void save(Account account) throws SQLException;
}
AccountDaoImpl.java
package com.qcbyjy.demo1;
import org.springframework.stereotype.Repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
@Repository
public class AccountDaoImpl implements AccountDao {
@Override
public void save(Account account) throws SQLException {
Connection conn = TxUtils.getConnection(); // 注意:不是 dataSource.getConnection()
String sql = "INSERT INTO account (name, money) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, account.getName());
ps.setDouble(2, account.getMoney());
ps.executeUpdate();
}
// 注意:这里不关闭 Connection,事务还没结束
}
}
DAO 只写 SQL,不创建连接,不关闭连接。这一点非常关键,后面解释为什么。
AccountService.java
package com.qcbyjy.demo1;
import java.sql.SQLException;
public interface AccountService {
void saveAll(Account account1, Account account2) throws SQLException;
}
AccountServiceImpl.java
业务方法里夹了一个 1 / 0,故意制造异常,用来触发回滚。
package com.qcbyjy.demo1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.sql.SQLException;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void saveAll(Account account1, Account account2) throws SQLException {
accountDao.save(account1);
int i = 1 / 0; // 故意制造异常
accountDao.save(account2);
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
}
TxUtils.java(整个 demo1 的灵魂)
package com.qcbyjy.demo1;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.sql.Connection;
import java.sql.SQLException;
@Component
public class TxUtils {
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
private static TxUtils INSTANCE;
private static DruidDataSource dataSource;
private static boolean initialized = false;
/** 关键:把 Connection 绑定到当前线程 */
private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<>();
@PostConstruct
public void init() {
INSTANCE = this;
}
private synchronized void initDataSource() {
if (initialized) return;
try {
dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setInitialSize(5);
dataSource.setMinIdle(5);
dataSource.setMaxActive(20);
dataSource.init();
initialized = true;
} catch (SQLException e) {
throw new RuntimeException("Druid 初始化失败", e);
}
}
private void ensureInitialized() {
if (!initialized) initDataSource();
}
public static Connection getConnection() throws SQLException {
if (INSTANCE == null) throw new IllegalStateException("TxUtils 还没被 Spring 初始化");
INSTANCE.ensureInitialized();
Connection conn = CONNECTION_HOLDER.get();
if (conn == null) {
conn = dataSource.getConnection(); // 从连接池借一根
CONNECTION_HOLDER.set(conn); // 绑到当前线程
}
return conn;
}
public static void startTransaction() throws SQLException {
getConnection().setAutoCommit(false);
}
public static void commit() throws SQLException {
Connection conn = CONNECTION_HOLDER.get();
if (conn != null) conn.commit();
}
public static void rollback() throws SQLException {
Connection conn = CONNECTION_HOLDER.get();
if (conn != null) conn.rollback();
}
public static void close() throws SQLException {
Connection conn = CONNECTION_HOLDER.get();
if (conn != null) {
conn.close();
CONNECTION_HOLDER.remove();
}
}
}
这个类我建议你多看几遍。它干的事情非常少,但每一行都精准对应事务的一个关键步骤:
CONNECTION_HOLDER:把 Connection 绑到线程上;getConnection():有就用,没有就借;startTransaction():把autoCommit关掉;commit / rollback / close:统一管理生命周期。
这就是后面 Spring 里 TransactionSynchronizationManager 的雏形,只是少了几百个 if-else。
JdkProxy.java(事务织入器)
package com.qcbyjy.demo1;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkProxy {
public static AccountService createProxy(AccountService target) {
return (AccountService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TransactionInvocationHandler(target)
);
}
private static class TransactionInvocationHandler implements InvocationHandler {
private final AccountService target;
TransactionInvocationHandler(AccountService target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result;
try {
System.out.println("[Proxy] 开始事务");
TxUtils.startTransaction();
result = method.invoke(target, args);
TxUtils.commit();
System.out.println("[Proxy] 提交事务");
} catch (Exception e) {
Throwable real = (e.getCause() != null) ? e.getCause() : e;
System.out.println("[Proxy] 异常,准备回滚:" + real.getMessage());
TxUtils.rollback();
throw real;
} finally {
TxUtils.close();
}
return result;
}
}
}
Demo1Test.java
package com.qcbyjy.test;
import com.qcbyjy.demo1.Account;
import com.qcbyjy.demo1.AccountService;
import com.qcbyjy.demo1.JdkProxy;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Demo1Test {
@Autowired
private AccountService accountService;
@Test
public void testSaveAllCommit() throws Exception {
Account a1 = new Account("张三", 1000.0);
Account a2 = new Account("李四", 2000.0);
AccountService proxy = JdkProxy.createProxy(accountService);
proxy.saveAll(a1, a2);
}
@Test
public void testSaveAllRollback() {
Account a1 = new Account("王五", 3000.0);
Account a2 = new Account("赵六", 4000.0);
AccountService proxy = JdkProxy.createProxy(accountService);
try {
proxy.saveAll(a1, a2);
} catch (Exception e) {
System.out.println("捕获异常:" + e.getMessage());
}
}
}
1.3 重点解析:这块面试爱问
ThreadLocal 为什么能实现事务
很多同学第一次看到 ThreadLocal 都有点懵。我用一个特别朴素的角度讲:
一次 Web 请求 = 一个线程 = 一次完整调用链(Controller → Service → DAO)。
线程是 Java 中天然的"调用链单位"。ThreadLocal 给你提供了一种"把变量塞进线程"的能力 —— 同一个线程里,无论你嵌套调用多少层,塞进去的东西都拿得回来。
事务需要"多次 SQL 共用一个 Connection",而"多次 SQL"必然在同一个线程内执行。所以 ThreadLocal<Connection> 简直是为事务量身定做的。
如果你不用 ThreadLocal,会怎样?你只能这么写:
public void saveAll(Connection conn, Account a1, Account a2) {
accountDao.save(conn, a1);
accountDao.save(conn, a2);
}
把 Connection 一路当参数往下传。这种写法叫"穿透式参数",代码会立刻变得难看。ThreadLocal 的本质是:用一个隐式的"线程上下文"替代显式的参数传递。
为什么必须使用同一个 Connection
事务的提交、回滚是 Connection 上的操作。如果你扣款用了 connA,加款用了 connB,JDBC 默认的 autoCommit=true 会让每条 SQL 立刻落库。等你想 rollback,connB 上那条已经回不来了。
简单一句:事务边界 = Connection 生命周期边界。
手写事务有什么缺点
写完 demo1 你应该能感受到:
- 侵入性大:每个要事务的对象都要被
JdkProxy.createProxy()包一次。 - 重复造轮子:每个项目都要写一份 TxUtils。
- 能力有限:没有传播行为,没有隔离级别,没有只读优化,没有超时控制。
- JDK 代理只能基于接口:目标类必须实现接口。
这些缺点会在 demo5、demo6 中被 Spring 一一治好。
面试常考:JDK 代理 vs CGLIB
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 代理对象类型 | 实现目标的所有接口 | 继承目标类 |
| 要求 | 目标类必须实现接口 | 目标类不能是 final |
| 性能 | 创建快,调用稍慢 | 创建慢,调用快 |
| Spring 默认 | 有接口走 JDK | 没接口走 CGLIB |
强制走 CGLIB 的方式:spring.aop.proxy-target-class=true,或 @EnableTransactionManagement(proxyTargetClass = true)。
高频追问 Q&A
Q1: ThreadLocal 在 Tomcat 线程池场景下会内存泄漏吗?
会。Tomcat 复用线程,如果你 set 了但没 remove,旧的 Value 会一直挂在
Thread.threadLocals里(Entry 的 Key 是弱引用,Value 是强引用,Key 被 GC 后 Value 仍然在),下一个请求复用同一个线程时还能"看到"上一次请求遗留的东西。所以 demo1 的TxUtils.close()一定要remove(),这是规范操作。后果不止是脏数据 —— Connection 不归还连接池,池子撑爆。
Q2:
Proxy.newProxyInstance拿到的代理对象,toString/equals/hashCode 也会被拦截吗?会。所有接口方法以及 Object 的三个方法都会进
invoke()。生产代码必须在 invoke 入口加一句if (method.getDeclaringClass() == Object.class) return method.invoke(target, args);直接放行,不然accountService.toString()也会触发"开事务",日志一打满。
Q3: demo1 的 TxUtils 能正确处理嵌套事务吗?
不能。我现在的实现是"拿连接时 setAutoCommit(false),关连接时 commit"。如果 saveAllA 调 saveAllB,B 走完会先 commit,A 还没结束,事务就提前关了。Spring 通过
ConnectionHolder.referenceCount解决这个 —— 每嵌套一次就 +1,关一次就 -1,只有归零才真 commit。
Q4: invoke 里 method.invoke(target, args) 抛 InvocationTargetException 怎么办?
必须 unwrap。目标方法抛的真实异常会被反射包成
InvocationTargetException,你直接抛会让上层 catch 业务异常的代码失效。规范写法:try { ... } catch (InvocationTargetException e) { throw e.getTargetException(); }。demo1 没特意写,但生产代码必须处理,否则你的 Service 抛BizException,Controller 永远 catch 不到。
demo2:让 Spring 替你写代理
写完 demo1 你可能会有一个朴素的想法:
“我 Service 一多,每个都要自己 createProxy 一次,太烦了。能不能让 Spring 看到
@Service就自动给我包一下?”
能。这就是 demo2 要演示的事情。
2.1 完整代码
UserService.java
package com.qcbyjy.demo2;
public interface UserService {
void save();
}
UserServiceImpl.java
package com.qcbyjy.demo2;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public void save() {
System.out.println("业务层:保存用户");
}
}
MyXmlAspect.java
package com.qcbyjy.demo2;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect // 告诉 Spring:这是切面
@Component // 告诉 Spring:把它注册到容器
public class MyXmlAspect {
@Before("execution(* com.qcbyjy.demo2.UserServiceImpl.save(..))")
public void log() {
System.out.println("[MyXmlAspect] 前置增强:记录日志");
}
}
类名虽然叫 MyXmlAspect,实际用的是注解。这是为了对应原来 XML 风格的写法,后面 demo3 才是纯注解的标准姿势。
Demo2Test.java
package com.qcbyjy.test;
import com.qcbyjy.demo2.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Demo2Test {
@Autowired
private UserService userService;
@Test
public void testXmlAop() {
userService.save();
}
}
2.2 跟 demo1 比起来,Spring 干了什么
启动时:
- Spring 看到
@Aspect类,知道这是个切面; - 解析
@Before里的 execution 表达式; - 找到所有匹配的 Bean(这里是
UserServiceImpl); - 给它们造一个代理对象,代理逻辑里嵌入
@Before方法; - 当你
@Autowired UserService时,注入的不是 UserServiceImpl 的真实实例,而是这个代理对象。
也就是说,demo1 中的 Proxy.newProxyInstance 这一步,被 Spring AOP 框架默默替你做了。
2.3 execution 表达式拆解(必须会背)
execution(* com.qcbyjy.demo2.UserServiceImpl.save(..))
│ │ │ │ │
│ │ │ │ └── (..) 任意参数(0 个或多个,任意类型)
│ │ │ └── save 方法名
│ │ └── com.qcbyjy.demo2.UserServiceImpl 全限定类名
│ └── * 返回值类型(任意)
└── execution 匹配方法执行
几个常见变形:
| 表达式 | 含义 |
|---|---|
execution(public * *(..)) | 所有 public 方法 |
execution(* com.qcbyjy..*.*(..)) | demo 包及子包所有方法 |
execution(* com.qcbyjy.demo3.*Service.*(..)) | demo3 下所有以 Service 结尾的类的所有方法 |
execution(* save*(..)) | 方法名以 save 开头的所有方法 |
execution(* *(java.lang.String, ..)) | 第一个参数是 String 的方法 |
记住 .. 这个符号,它在不同位置含义不同:在包名里表示"包及子包",在参数里表示"任意数量任意类型参数"。
2.4 demo2 vs demo1
| 维度 | demo1 | demo2 |
|---|---|---|
| 谁创建代理 | 自己写 JdkProxy | Spring 自动 |
| 织入方式 | 编程式 | 声明式(注解) |
| 切面定义 | InvocationHandler | @Aspect 类 |
| 灵活性 | 一次只代理一个对象 | 一个表达式可以命中无数方法 |
2.5 高频追问 Q&A
Q1:
@Aspect不加@Component会怎样?切面不会被 Spring 容器扫描到,自然也不会织入。
@Aspect只是告诉 AspectJ"这是切面定义",但容器认不认它取决于@Component(或@Configuration里@Bean显式注册)。Spring Boot 工程里这两个注解几乎成了固定搭配。
Q2: 同一个方法被两个
@Aspect命中,谁先执行?默认看 Bean 创建顺序,不可靠。要明确顺序就给切面加
@Order(1)、@Order(2)—— 数字小的外层先 Before、最后 After,典型的洋葱结构:@Order(1) Before → @Order(2) Before → 目标方法 → @Order(2) After → @Order(1) After这是面试官常用的"埋雷题"。
Q3: AOP 织入是在 Bean 生命周期的哪一步发生的?
BeanPostProcessor#postProcessAfterInitialization。具体实现是AnnotationAwareAspectJAutoProxyCreator,它在 Bean 初始化完成后判断要不要包代理,要包就返回代理对象替换原 Bean。所以你@Autowired拿到的永远是代理,这一点是 Spring AOP 的根基。
Q4: 我的目标类没实现接口,Spring 还能给我做 AOP 吗?
能。Spring AOP 默认有接口走 JDK 动态代理,没接口自动切换 CGLIB。Spring Boot 2.0 之后默认就是
proxy-target-class=true,统一用 CGLIB,这是为了避免"接口里没声明 / 但实现类加了@Transactional"这种隐性失效。
demo3:五种通知,执行顺序面试最爱问
demo2 只演示了 @Before。真正面试,面试官最爱问的是"五种通知执行顺序"。demo3 就把这个问题彻底说透。
3.1 完整代码
ProductService.java
package com.qcbyjy.demo3;
public interface ProductService {
void save();
void update();
void delete();
String findById(Integer id);
}
ProductServiceImpl.java
package com.qcbyjy.demo3;
import org.springframework.stereotype.Service;
@Service
public class ProductServiceImpl implements ProductService {
@Override
public void save() {
System.out.println("[Product] 保存产品");
}
@Override
public void update() {
System.out.println("[Product] 更新产品");
}
@Override
public void delete() {
System.out.println("[Product] 删除产品");
int i = 1 / 0; // 模拟异常,用来触发 @AfterThrowing
}
@Override
public String findById(Integer id) {
System.out.println("[Product] 查询产品 id=" + id);
return "产品-" + id;
}
}
MyAnnotationAspect.java
package com.qcbyjy.demo3;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
public class MyAnnotationAspect {
@Pointcut("execution(* com.qcbyjy.demo3.ProductServiceImpl.*(..))")
public void pointcut() {}
@Before("pointcut()")
public void beforeAdvice(JoinPoint jp) {
System.out.println("【前置通知】方法:" + jp.getSignature().getName()
+ ",参数:" + Arrays.toString(jp.getArgs()));
}
@AfterReturning(value = "pointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint jp, Object result) {
System.out.println("【后置通知】方法:" + jp.getSignature().getName()
+ ",返回值:" + result);
}
@AfterThrowing(value = "pointcut()", throwing = "ex")
public void afterThrowingAdvice(JoinPoint jp, Throwable ex) {
System.out.println("【异常通知】方法:" + jp.getSignature().getName()
+ ",异常:" + ex.getMessage());
}
@After("pointcut()")
public void afterAdvice(JoinPoint jp) {
System.out.println("【最终通知】方法:" + jp.getSignature().getName());
}
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("【环绕通知-前】" + pjp.getSignature().getName());
Object result;
try {
result = pjp.proceed(); // 不调 proceed(),目标方法不会被执行
long cost = System.currentTimeMillis() - start;
System.out.println("【环绕通知-后】耗时 " + cost + "ms,返回值=" + result);
} catch (Throwable ex) {
System.out.println("【环绕通知-异常】" + ex.getMessage());
throw ex; // 继续抛出去,让 @AfterThrowing 也能收到
}
return result;
}
}
Demo3Test.java
package com.qcbyjy.test;
import com.qcbyjy.demo3.ProductService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Demo3Test {
@Autowired
private ProductService productService;
@Test
public void testSave() { productService.save(); }
@Test
public void testUpdate() { productService.update(); }
@Test
public void testFindById() {
String r = productService.findById(100);
System.out.println("返回值:" + r);
}
@Test
public void testDelete() {
try { productService.delete(); }
catch (Exception e) { System.out.println("捕获异常(预期):" + e.getMessage()); }
}
}
3.2 五种通知的差异
| 注解 | 执行时机 | 拿到什么 | 典型用途 |
|---|---|---|---|
@Before | 方法前 | JoinPoint(方法名、参数) | 权限校验、参数日志 |
@AfterReturning | 方法正常返回后 | returning 拿返回值 | 记录返回值、结果加工 |
@AfterThrowing | 方法抛异常后 | throwing 拿异常 | 异常告警、日志归档 |
@After | 方法结束后(无论成败) | JoinPoint | 释放资源 |
@Around | 包裹整个方法 | ProceedingJoinPoint | 性能统计、事务、缓存 |
3.3 执行顺序(面试一定会问)
正常返回
【环绕-前】
【前置】
目标方法
【后置(AfterReturning)】
【最终(After)】
【环绕-后】
抛异常
【环绕-前】
【前置】
目标方法 → 抛异常
【异常(AfterThrowing)】 // 替代后置
【最终(After)】 // finally 一定执行
【环绕-异常分支】
面试坑:
@After和@AfterReturning的区别?一个是 finally(异常也跑),一个是 try 结束后(只在正常返回时跑)。
@AfterThrowing触发后,为什么@AfterReturning不执行?因为方法抛了异常,根本没有"返回值",
returning拿不到东西,这个时机自然不存在。
@Around 不调 proceed() 会怎样
目标方法不会执行。这是它和其他通知最大的区别:@Around 决定了"目标方法到底跑不跑"。
Spring 事务的 TransactionInterceptor 内部就是个 @Around:在 proceed 前 getTransaction,在 proceed 后 commit,catch 里 rollback。
3.4 高频追问 Q&A
Q1:
@Around里 catch 了 proceed 抛的异常但没 rethrow,@AfterThrowing会触发吗?不会。
@AfterThrowing触发的前提是异常穿过整个切面链。@Around在最外层,如果它把异常吞了,后面所有的@AfterThrowing都收不到。这也是 Spring 事务用@Around而非@AfterThrowing的原因之一 —— 事务对异常控制要求绝对精准,绝不能让别的切面把异常吞了导致漏回滚。
Q2: 切面自己抛异常会怎样?
取决于切面类型:
@Before抛异常 → 目标方法不会执行,异常直接抛给调用方;@After抛异常 → 会覆盖目标方法的返回/异常,这是大坑;@AfterReturning抛异常 → 会变成异常返回,从正常分支切到异常分支。所以切面里要么不抛,要么用 try-catch 兜住。
Q3:
JoinPoint和ProceedingJoinPoint区别?为什么只有@Around能用ProceedingJoinPoint?
JoinPoint只能拿到方法签名、参数、目标对象;ProceedingJoinPoint多一个proceed()方法。前置/后置通知的"目标方法执不执行"是框架决定的 —— 你拿 ProceedingJoinPoint 也没意义。只有@Around把"执不执行目标方法"的决定权交给你,所以才会给ProceedingJoinPoint。
Q4: 同类方法 a 调 b,b 上的切面会触发吗?
不会。
a()内部this.b()走的是原对象,绕过了代理,b 上的切面完全不会被织入。@Transactional失效的"自调用"问题就是这个,demo6 会专门讲。这个问题面试出现率接近 100%。
Q5:
@Pointcut表达式可以复用吗?可以。在切面里写一个空方法标
@Pointcut,通知里直接用方法名引用:@Before("myPointcut()")。多个切面之间也可以引用,前提是 public,引用时写全限定名:@Before("com.qcbyjy.aspect.MyAspect.myPointcut()")。
demo4:把 JDBC 升级成 JdbcTemplate,顺便复现一次"钱凭空消失"
demo1 ~ demo3 还停留在"AOP 概念"的层面。demo4 开始进入实战:用 JdbcTemplate 做转账。
4.1 完整代码
Account.java
用了 Lombok,自动生成 getter/setter。
package com.qcbyjy.demo4;
import lombok.Data;
@Data
public class Account {
private Integer id;
private String name;
private Double money;
}
AccountDao.java
package com.qcbyjy.demo4;
import java.util.List;
public interface AccountDao {
void save(Account account);
void update(Account account);
void deleteById(Integer id);
Account findById(Integer id);
List<Account> findAll();
void outMoney(String outName, Double money);
void inMoney(String inName, Double money);
}
AccountDaoImpl.java
package com.qcbyjy.demo4;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository("accountDaoImpl4")
public class AccountDaoImpl implements AccountDao {
private final JdbcTemplate jdbcTemplate;
public AccountDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void save(Account account) {
jdbcTemplate.update(
"insert into account(name, money) values (?, ?)",
account.getName(), account.getMoney());
}
@Override
public void update(Account account) {
jdbcTemplate.update(
"update account set name = ?, money = ? where id = ?",
account.getName(), account.getMoney(), account.getId());
}
@Override
public void deleteById(Integer id) {
jdbcTemplate.update("delete from account where id = ?", id);
}
@Override
public Account findById(Integer id) {
return jdbcTemplate.queryForObject(
"select id, name, money from account where id = ?",
new BeanPropertyRowMapper<>(Account.class), id);
}
@Override
public List<Account> findAll() {
return jdbcTemplate.query(
"select id, name, money from account",
new BeanPropertyRowMapper<>(Account.class));
}
@Override
public void outMoney(String outName, Double money) {
jdbcTemplate.update(
"update account set money = money - ? where name = ?",
money, outName);
}
@Override
public void inMoney(String inName, Double money) {
jdbcTemplate.update(
"update account set money = money + ? where name = ?",
money, inName);
}
}
AccountService.java
package com.qcbyjy.demo4;
import java.util.List;
public interface AccountService {
void save(Account account);
void update(Account account);
void deleteById(Integer id);
Account findById(Integer id);
List<Account> findAll();
void pay(String outName, String inName, Double money);
}
AccountServiceImpl.java
注意 pay 方法上有 @Transactional。我下面会专门解释这一点。
package com.qcbyjy.demo4;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("accountServiceImpl4")
public class AccountServiceImpl implements AccountService {
private final AccountDao accountDao;
public AccountServiceImpl(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override public void save(Account account) { accountDao.save(account); }
@Override public void update(Account account) { accountDao.update(account); }
@Override public void deleteById(Integer id) { accountDao.deleteById(id); }
@Override public Account findById(Integer id) { return accountDao.findById(id); }
@Override public List<Account> findAll() { return accountDao.findAll(); }
@Override
@Transactional
public void pay(String outName, String inName, Double money) {
accountDao.outMoney(outName, money);
int error = 1 / 0; // 故意制造异常
accountDao.inMoney(inName, money);
}
}
Demo4Test.java
package com.qcbyjy.test;
import com.qcbyjy.demo4.Account;
import com.qcbyjy.demo4.AccountService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class Demo4Test {
@Autowired
private AccountService accountService;
@Test
public void testFindAll() {
List<Account> accounts = accountService.findAll();
accounts.forEach(System.out::println);
}
@Test
public void testSave() {
Account a = new Account();
a.setName("光头强");
a.setMoney(500.0);
accountService.save(a);
}
@Test
public void testPayWithoutTransaction() {
accountService.pay("熊大", "熊二", 100.0);
}
}
4.2 JdbcTemplate vs 原生 JDBC
| 操作 | 原生 JDBC | JdbcTemplate |
|---|---|---|
| 获取连接 | DriverManager.getConnection(...) | 框架自动 |
| 创建 Statement | conn.prepareStatement(sql) | 框架自动 |
| 设置参数 | ps.setString(1, ...) 一个一个设 | update(sql, args...) |
| 关闭资源 | try-finally | 框架自动 |
| 处理异常 | throws SQLException | 转成 DataAccessException(运行时异常) |
| 结果映射 | while (rs.next()) { ... } | BeanPropertyRowMapper |
一句话:JdbcTemplate 替你处理了 90% 的样板代码。
4.3 三个核心方法
update():增删改
int rows = jdbcTemplate.update(
"insert into account(name, money) values (?, ?)",
"光头强", 500.0);
返回受影响的行数。INSERT、UPDATE、DELETE 都用它。
queryForObject():查询单个对象
Account a = jdbcTemplate.queryForObject(
"select id, name, money from account where id = ?",
new BeanPropertyRowMapper<>(Account.class), id);
注意:结果必须刚好 1 行。
- 0 行 →
EmptyResultDataAccessException; - 多行 →
IncorrectResultSizeDataAccessException。
很多人这里踩坑,以为查 0 行返回 null,实际是抛异常。
query():查询列表
List<Account> list = jdbcTemplate.query(
"select id, name, money from account",
new BeanPropertyRowMapper<>(Account.class));
返回 List<Account>,空集合不报错。
4.4 为什么没有事务会导致数据不一致
public void pay(String outName, String inName, Double money) {
accountDao.outMoney(outName, money); // 熊大 -100,JDBC 默认 autoCommit,直接 commit
int error = 1 / 0; // 抛异常
accountDao.inMoney(inName, money); // 永远不会执行
}
如果没有事务保护,扣款那一句 SQL 一发出去就 commit 了,后面抛异常压根管不住它。结果:熊大 -100,熊二 +0,数据库少了 100 块。
4.5 一个容易被忽略的细节
你可能注意到了,我在 pay() 上写了 @Transactional。它实际上是生效的。
为什么?因为我们用了 spring-boot-starter-jdbc,Spring Boot 会自动配置 DataSourceTransactionManager 并隐式启用 @EnableTransactionManagement。所以哪怕你这一节明面上叫"无事务版本",事务也已经在帮你兜底了。
如果你想真的复现"钱消失"的灾难,有两种办法:
- 把
@Transactional注释掉再跑一次testPayWithoutTransaction; - 或者保留
@Transactional,但把异常吞掉:try { ... } catch (Exception ignored) {},这样 Spring 看不到异常,事务照样 commit。
第 2 种是真实项目里最常见的事务失效场景,后面会专门讲。
4.6 高频追问 Q&A
Q1: JdbcTemplate 是线程安全的吗?
**是。**它是无状态对象 —— 内部不持有 Connection,Connection 在每次 SQL 调用时通过
DataSourceUtils.getConnection(dataSource)临时获取、用完归还。所以全局单例没问题,这也是 Spring 把它做成单例 Bean 的底气。
Q2:
queryForObject查 0 行和多行分别抛什么异常?0 行:
EmptyResultDataAccessException;多行:IncorrectResultSizeDataAccessException。两者都是运行时异常,继承DataAccessException。所以用 queryForObject 一定要保证查询条件唯一,否则推荐query()+ 手动取list.size()判断。
Q3: JdbcTemplate.update() 执行完,Connection 会怎样?
看是不是事务里。没事务时用
DataSourceUtils.releaseConnection归还连接池;有事务时(TransactionSynchronizationManager里能找到当前 DataSource 的 ConnectionHolder),它根本不会真关,只把referenceCount--,等事务最后才真关。源码后面那一节会展开。
Q4: 为什么 Spring Boot 工程里我啥都没写就能
@Autowired JdbcTemplate?因为
JdbcTemplateAutoConfiguration已经在你引入spring-boot-starter-jdbc时帮你配好了。它的条件是"classpath 有 JdbcTemplate.class 且容器里有 DataSource Bean"。所以你只要写好spring.datasource.url那一组配置,JdbcTemplate 自动就在容器里。番外那节会再深入。
Q5: 我把
@Transactional注释掉再跑,数据真的会丢吗?会。MySQL InnoDB 默认每条 SQL 一个隐式事务(autoCommit=true),扣款 SQL 一执行就 commit,后面 1/0 抛了异常也回不来。这就是没事务保护的真实代价 —— 你看到的"数据不一致",本质是"两条 SQL 处于两个独立事务",这跟 demo1 开篇讲的事务定义直接闭环。
demo5:用 XML 把事务声明出来
demo4 暴露了"裸跑无事务"的危险,demo5 用 XML 把事务正式接管过来。注意,这是一种"老但好理解"的写法,真实项目里你可能不会用 XML,但学这个的目的是让你看清楚事务每一个组件长什么样。
5.1 完整代码
Account.java
package com.qcbyjy.demo5;
import lombok.Data;
@Data
public class Account {
private Integer id;
private String name;
private Double money;
}
AccountDao.java
package com.qcbyjy.demo5;
import java.util.List;
public interface AccountDao {
List<Account> findAll();
void outMoney(String outName, Double money);
void inMoney(String inName, Double money);
}
AccountDaoImpl.java
package com.qcbyjy.demo5;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class AccountDaoImpl implements AccountDao {
private final JdbcTemplate jdbcTemplate;
public AccountDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public List<Account> findAll() {
return jdbcTemplate.query("select id, name, money from account",
new BeanPropertyRowMapper<>(Account.class));
}
@Override
public void outMoney(String outName, Double money) {
jdbcTemplate.update("update account set money = money - ? where name = ?",
money, outName);
}
@Override
public void inMoney(String inName, Double money) {
jdbcTemplate.update("update account set money = money + ? where name = ?",
money, inName);
}
}
AccountService.java / AccountServiceImpl.java
注意:Service 实现类完全不写事务代码。
package com.qcbyjy.demo5;
import java.util.List;
public interface AccountService {
List<Account> findAll();
void pay(String outName, String inName, Double money);
}
package com.qcbyjy.demo5;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AccountServiceImpl implements AccountService {
private final AccountDao accountDao;
public AccountServiceImpl(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public List<Account> findAll() {
return accountDao.findAll();
}
@Override
public void pay(String outName, String inName, Double money) {
accountDao.outMoney(outName, money);
int error = 1 / 0;
accountDao.inMoney(inName, money);
}
}
applicationContext_demo5.xml
这是整个 demo5 的核心,我把每个 Bean 的角色都注释清楚。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- ① 扫描 demo5 包,@Service / @Repository 自动注册 -->
<context:component-scan base-package="com.qcbyjy.demo5"/>
<!-- ② 数据源(连接池) -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false"/>
<property name="username" value="test123"/>
<property name="password" value="test123"/>
<property name="initialSize" value="5"/>
<property name="minIdle" value="5"/>
<property name="maxActive" value="20"/>
<property name="maxWait" value="60000"/>
</bean>
<!-- ③ JdbcTemplate(必须显式声明,且和事务管理器使用同一个 dataSource) -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- ④ 事务管理器:把"事务"翻译成 JDBC 操作 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- ⑤ 事务增强:哪些方法用什么事务规则 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="pay*" propagation="REQUIRED"/>
<tx:method name="find*" read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- ⑥ AOP 配置:把事务增强织入 ServiceImpl 的方法 -->
<aop:config>
<aop:pointcut id="servicePointcut"
expression="execution(* com.qcbyjy.demo5.*ServiceImpl.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="servicePointcut"/>
</aop:config>
</beans>
Demo5Test.java
@ImportResource 把 XML 加载进来。
package com.qcbyjy.test;
import com.qcbyjy.demo5.Account;
import com.qcbyjy.demo5.AccountService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ImportResource;
import java.util.List;
@SpringBootTest(classes = Demo5Test.TestConfig.class)
public class Demo5Test {
@EnableAutoConfiguration
@ImportResource("classpath:applicationContext_demo5.xml")
static class TestConfig {}
@Autowired
private AccountService accountService;
@Test
public void testFindAll() {
List<Account> list = accountService.findAll();
list.forEach(System.out::println);
}
@Test
public void testPayWithXmlTransaction() {
accountService.pay("熊大", "熊二", 100.0);
}
}
5.2 一个个把 XML 看懂
XML 看着挺吓人,真理解起来一点也不复杂。我们逐个 Bean 拆开看。
transactionManager 是干嘛的
Spring 把"事务"抽象成接口 PlatformTransactionManager:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}
JDBC 实现叫 DataSourceTransactionManager,内部干的事情大致是:
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
TransactionSynchronizationManager.bindResource(dataSource, conn);
这就是 demo1 中你手写的 TxUtils.startTransaction()。Spring 用 TransactionSynchronizationManager(本质还是一个 ThreadLocal Map)替代了你的 CONNECTION_HOLDER。
tx:advice 是什么
它是一份规则表:哪些方法用什么事务规则。
<tx:method name="pay*" propagation="REQUIRED"/>
含义:方法名以 pay 开头的,使用 REQUIRED 传播行为。
aop:advisor 是什么
advisor = pointcut + advice,翻译成大白话:
把
txAdvice这套规则,套用在servicePointcut匹配到的所有方法上。
整体流程
test 调 accountService.pay(...)
↓
accountService 是代理对象(Spring 用 CGLIB/JDK 包过)
↓
事务拦截器(由 tx:advice 生成)介入:
① getTransaction() → 关闭 autoCommit,把 Connection 绑到线程
② method.invoke(target) → 调真实 pay()
└─ outMoney 用 ThreadLocal 中的 Connection
└─ 1/0 抛 RuntimeException
③ catch → rollback()
④ finally → 解绑、归还连接
跟 demo1 的 JdkProxy.invoke 结构几乎一致,只是 Spring 替你写了。
5.3 七种传播行为(REQUIRED 是默认)
REQUIRED(默认):当前没事务就新建,有事务就加入。
@Transactional // 默认 REQUIRED
public void A() {
B(); // B 也是 REQUIRED → A 和 B 在同一个事务里
}
| 传播行为 | 含义 |
|---|---|
| REQUIRED(默认) | 有就加入,没有就新建 |
| REQUIRES_NEW | 始终新建,挂起当前事务 |
| SUPPORTS | 有就加入,没有就以非事务方式运行 |
| NOT_SUPPORTED | 以非事务方式运行,挂起当前事务 |
| NEVER | 以非事务方式运行;有事务就抛异常 |
| MANDATORY | 必须有事务,没有就抛异常 |
| NESTED | 在当前事务中嵌套保存点 |
面试坑:REQUIRED 与 REQUIRES_NEW 的区别
A 用 REQUIRED,B 用 REQUIRES_NEW:A 出错回滚时,B 不受影响(B 已独立提交)。
经典场景:主流程出错了要回滚,但日志记录必须落库。把日志方法标
REQUIRES_NEW。
5.4 高频追问 Q&A
Q1:
tx:advice和aop:advisor必须一起出现吗?在 XML 风格里缺一不可。
tx:advice描述"做什么"(开/提交/回滚的策略),aop:advisor描述"在哪做"(pointcut)。两条线由advisor-ref关联起来,Spring 才知道"要把这一段 advice 织到这一组方法上"。
Q2:
read-only="true"真的会拒绝写操作吗?不会主动拒绝,但底层会"半拒绝"。它给底层一个 hint —— MySQL 8 JDBC 驱动会向服务端发
SET SESSION TRANSACTION READ ONLY,Hibernate 会关闭脏检查,Oracle 会切换到 read-only 事务。如果你硬写,大概率会触发数据库报错(MySQL 错误码 1792),Spring 的SQLExceptionTranslator没给这个码具体映射,默认就会包成UncategorizedSQLException(DataAccessException的子类)抛出。它的本质是性能优化 + hint,不是权限控制,具体是不是抛异常还取决于驱动版本和数据库实现。
Q3:
<tx:method name="pay*"/>这种通配能匹配payMoney,能匹配doPay吗?匹配
payMoney(以 pay 开头),不匹配doPay(没 pay 前缀)。Spring 用的是简单字符串通配,*在头/尾/中间都行,但不是正则。所以工程里约定方法命名很重要,常见save*find*update*delete*这种全前缀通配。
Q4: XML 里
<tx:method/>没写 propagation,默认是什么?
REQUIRED。这跟@Transactional默认值完全一致 —— 有就加入,没有就新建,工程里 95% 的场景都用它。
Q5: XML 风格的事务和注解风格能混用吗?
能,但不要。XML 配的是面切入(execution 表达式批量),注解配的是点切入(单方法精确)。两套规则同时生效会让你彻底搞不清"这个方法到底带不带事务"。要么全用 XML,要么全
@Transactional,二选一。
demo6:把 XML 改成 @Transactional,这就是你日常写的代码
6.1 完整代码
Account.java
package com.qcbyjy.demo6;
import lombok.Getter;
@Getter
public class Account {
private Integer id;
private String name;
private Double money;
public void setId(Integer id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setMoney(Double money) { this.money = money; }
@Override
public String toString() {
return "Account{id=" + id + ", name='" + name + "', money=" + money + "}";
}
}
AccountDao.java
package com.qcbyjy.demo6;
import java.util.List;
public interface AccountDao {
void outMoney(String out, double money);
void inMoney(String in, double money);
List<Account> findAll();
}
AccountDaoImpl.java
package com.qcbyjy.demo6;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.List;
@Repository("AccountDaoImplDemo6")
public class AccountDaoImpl implements AccountDao {
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public void outMoney(String out, double money) {
jdbcTemplate.update(
"update account set money = money - ? where name = ?", money, out);
}
@Override
public void inMoney(String in, double money) {
jdbcTemplate.update(
"update account set money = money + ? where name = ?", money, in);
}
@Override
public List<Account> findAll() {
return jdbcTemplate.query(
"select id, name, money from account",
new BeanPropertyRowMapper<>(Account.class));
}
}
AccountService.java / AccountServiceImpl.java
package com.qcbyjy.demo6;
import java.util.List;
public interface AccountService {
void pay(String out, String in, double money);
List<Account> findAll();
}
package com.qcbyjy.demo6;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Service("AccountServiceImplDemo6")
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;
@Transactional
@Override
public void pay(String out, String in, double money) {
accountDao.outMoney(out, money);
int a = 1 / 0; // 模拟异常
accountDao.inMoney(in, money);
}
@Override
public List<Account> findAll() {
return accountDao.findAll();
}
}
SpringConfig.java
这就是 demo5 那一堆 XML 翻译成 Java 的样子。
package com.qcbyjy.demo6;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@ComponentScan(basePackages = "com.qcbyjy.demo6") // 等价 <context:component-scan>
@EnableTransactionManagement // 等价 <tx:annotation-driven>
public class SpringConfig {
@Bean(name = "dataSource")
public DataSource createDataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
ds.setUsername("test123");
ds.setPassword("test123");
return ds;
}
@Bean(name = "jdbcTemplate")
public JdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean(name = "transactionManager")
public PlatformTransactionManager createTransactionManager(DataSource ds) {
return new DataSourceTransactionManager(ds);
}
}
Demo6Test.java
package com.qcbyjy.test;
import com.qcbyjy.demo6.Account;
import com.qcbyjy.demo6.AccountService;
import com.qcbyjy.demo6.SpringConfig;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import javax.annotation.Resource;
import java.util.List;
@SpringJUnitConfig(classes = SpringConfig.class)
public class Demo6Test {
@Resource
private AccountService accountService;
@Test
public void testFindAll() {
List<Account> list = accountService.findAll();
list.forEach(System.out::println);
}
@Test
public void testPay() {
accountService.pay("熊大", "熊二", 100);
}
}
6.2 @Transactional 到底怎么生效的
把 demo6 拆开看,Spring 干了下面几件事。
启动时
@EnableTransactionManagement注册了一个BeanPostProcessor(底层是InfrastructureAdvisorAutoProxyCreator);- 后处理器扫描每个 Bean,判断是否有
@Transactional方法; - 有的话,给它造一个代理对象,代理里嵌入
TransactionInterceptor(本质就是@Around); - 容器中替换为代理对象,后续
@Resource注入到测试类的就是这个代理。
调用时
accountService.pay(...) // 看起来调的是真实对象
↓ 实际调代理
TransactionInterceptor.invoke()
① getTransaction() // 开事务,绑线程
② method.invoke(target) // 调真实 pay()
- outMoney → 用绑定线程的 Connection
- 1/0 抛 RuntimeException
③ catch → rollback()
④ throw 出去
是不是和 demo1 的 JdkProxy.invoke 长得一模一样?对,就是同一回事。
6.3 commit 和 rollback 的触发时机
这是面试特别爱挖的坑,记住:
方法正常返回 → commit()
抛 RuntimeException 或 Error → rollback()
抛 checked Exception → 默认 commit(注意!)
为什么默认只回滚 RuntimeException?
Spring 把检查异常(IOException 这种)看成"业务可预期的异常",认为开发者会自己处理;运行时异常被认为是"代码错了",才需要回滚。
想让检查异常也回滚?写成
@Transactional(rollbackFor = Exception.class)。
6.4 高频追问 Q&A
Q1: 为什么
@Transactional加在 private 方法上不生效?(经典)Spring AOP 是基于代理的。代理对象只能拦截外部对它的调用,而:
- JDK 代理:只能拦截接口里声明的方法,private 根本不在接口上;
- CGLIB 代理:虽然能继承目标类,但 Java 语言规则规定 private 方法不能被子类重写,代理插不进去。
所以
@Transactional必须加在public方法上。protected/package-private 也不行,只有 public 才稳。
Q2: 为什么同类内 a 方法调 b 方法,b 上的
@Transactional不生效?(经典)因为
this.b()走的是原对象引用,绕过了代理。代理对象拦截的是"外部调用",一旦进入 a 方法体,this就是原对象,后面调 b 就是普通的方法调用,事务织入完全失效。三种解法:
- 把 b 抽到另一个 Service;
- 注入自己的代理
@Autowired private XxxService self;然后用self.b();((XxxService) AopContext.currentProxy()).b()(需要@EnableAspectJAutoProxy(exposeProxy = true))。
Q3:
@Transactional加在接口方法上 vs 实现类方法上,区别是什么?加在接口方法上,只有 JDK 代理认。如果切到了 CGLIB 代理(类没接口),注解就丢失了,事务失效。Spring 官方建议加在实现类的方法上,这样不管哪种代理都生效,迁移时不会埋雷。
Q4: 抛了
IOException没回滚,为什么?怎么修?默认只回滚
RuntimeException和Error。Spring 的设计哲学是:检查异常代表"业务可预期(开发者必须显式 try-catch 或抛出)“,运行时异常代表"代码错了”。改:@Transactional(rollbackFor = Exception.class)让所有异常都回滚,或针对性地rollbackFor = IOException.class。源码就一行判断,看后面RuleBasedTransactionAttribute#rollbackOn。
Q5:
@EnableTransactionManagement(proxyTargetClass = true)解决什么问题?强制使用 CGLIB 代理。两个真实场景:
- 类没实现接口,JDK 代理用不了;
- 类实现了接口但你
@Autowired UserServiceImpl impl(具体类注入),JDK 代理转不成 impl,会抛 ClassCastException。Spring Boot 2.x 之后默认就是
true,所以你大多数时候不用关心,但面试官爱挖。
番外:Spring Boot 为什么不需要手写 transactionManager
到 demo6 你会发现一件事:demo6 这堆 SpringConfig,在 Spring Boot 的世界里你压根不用写。原因是 自动配置(AutoConfiguration)。
自动配置怎么干活
@SpringBootApplication 包含 @EnableAutoConfiguration,后者会:
- 读取
META-INF/spring.factories(2.x)或META-INF/spring/...AutoConfiguration.imports(3.x); - 拿到一堆
XxxAutoConfiguration类; - 按条件注册它们(
@ConditionalOnClass、@ConditionalOnMissingBean等)。
跟事务相关的有三个核心自动配置:
DataSourceAutoConfiguration
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, ... })
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration { ... }
读取 application.yml 里 spring.datasource.*,自动创建 DataSource。引入 Druid Starter 就是 Druid,引入 HikariCP(Spring Boot 默认)就是 Hikari。
DataSourceTransactionManagerAutoConfiguration
@AutoConfiguration
@ConditionalOnClass({ JdbcTemplate.class, TransactionManager.class })
@ConditionalOnSingleCandidate(DataSource.class)
public class DataSourceTransactionManagerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
DataSourceTransactionManager transactionManager(...) { ... }
}
干两件事:
- 自动创建
DataSourceTransactionManager; - 隐式启用
@EnableTransactionManagement,让@Transactional直接生效。
JdbcTemplateAutoConfiguration
自动创建 JdbcTemplate。
一句话总结
application.yml(spring.datasource.*)
↓
DataSourceAutoConfiguration → 创建 DataSource
JdbcTemplateAutoConfiguration → 创建 JdbcTemplate
DataSourceTxAutoConfiguration → 创建 TransactionManager + 启用 @Transactional
↓
你只要写 @Transactional,完事
这也是为什么 demo4 中你看到 @Transactional 不配置任何 manager 也能生效:Spring Boot 全替你做了。
几个非常实用的对比表
DriverManagerDataSource vs Druid / HikariCP
| 维度 | DriverManagerDataSource | Druid / HikariCP |
|---|---|---|
| 类型 | 每次新建连接(不是连接池) | 连接池 |
| 性能 | 极差(每次 TCP 三次握手 + 鉴权) | 极好(连接复用) |
| 用途 | 仅供测试(demo6 用了它) | 生产必备 |
| 配置 | url / user / pwd | 还能配最大连接数、空闲时间、SQL 监控 |
| 监控 | 无 | Druid 自带 SQL 监控页面 |
demo5 的 XML 用了 Druid,demo6 的 JavaConfig 用了 DriverManagerDataSource。前者更接近生产,后者纯粹是为了演示简洁。生产代码一律用 Druid 或 HikariCP。
手写事务 vs Spring 事务
| 维度 | demo1 手写 | demo5 / demo6 Spring |
|---|---|---|
| 连接绑定 | 自己写 ThreadLocal | TransactionSynchronizationManager(本质也是 ThreadLocal) |
| 开/提/回 | 手动 conn.commit / rollback | PlatformTransactionManager 抽象 |
| 织入方式 | 手写 JDK 代理 | Spring AOP 自动代理 |
| 传播行为 | 自己实现要上百行 | 配置一行搞定 |
| 隔离级别 | 自己 conn.setTransactionIsolation | @Transactional(isolation = ...) |
| 异常回滚规则 | 自己 if-else | rollbackFor = Xxx.class |
XML 事务 vs 注解事务
| 维度 | XML(demo5) | 注解(demo6) |
|---|---|---|
| 配置位置 | 集中 | 分散在每个方法 |
| 可读性 | 事务规则一眼看完 | 在用的地方就能看到 |
| 灵活性 | 全局规则统一改 | 单方法精细控制 |
| 现代项目 | 老项目 | 强烈推荐 |
| 命中范围 | 一个 pointcut 一刀切 | 一个一个加 @Transactional |
JdbcTemplate vs 原生 JDBC
详见 demo4。核心是"框架替你处理 90% 样板代码"。
必踩之坑:@Transactional 失效的几种情况
这一节是面试和实战双高频,把这几个坑理解透,简历加分。
坑 1:this.xxx() 自调用失效(最坑)
@Service
public class A {
public void m1() {
this.m2(); // 直接走对象内部,绕过代理,@Transactional 失效
}
@Transactional
public void m2() { ... }
}
为什么:@Transactional 靠代理对象生效;this 指向的是真实对象,根本没经过代理。
怎么修:
@Service
public class A {
@Autowired
private A self; // 注入自己
public void m1() {
self.m2(); // 走代理,生效
}
@Transactional
public void m2() { ... }
}
坑 2:private 方法不生效
@Service
public class A {
@Transactional
private void m() { ... } // 代理无法拦截 private
}
为什么:JDK 代理只能代理接口方法(必须 public);CGLIB 也无法代理 private / final / static。
怎么修:改成 public。
坑 3:不是 Spring Bean
A a = new A(); // 自己 new 的
a.m(); // 不会触发事务
为什么:Spring 只代理它管理的 Bean。
怎么修:@Autowired 注入。
坑 4:异常被吞了(实战中最常见)
@Transactional
public void pay() {
try {
accountDao.outMoney(...);
int a = 1 / 0;
} catch (Exception e) {
log.error("出错了", e); // 吞掉了!Spring 看不到异常,以为成功,会 commit
}
}
怎么修:在 catch 里把异常抛出去,或包装成 RuntimeException 重抛。
坑 5:抛了检查异常但没配 rollbackFor
@Transactional // 默认只回滚 RuntimeException
public void pay() throws IOException {
throw new IOException("..."); // 不会回滚
}
怎么修:@Transactional(rollbackFor = Exception.class)。
进阶:Connection 怎么绑到线程的
底层是 TransactionSynchronizationManager,本质上就是一组 ThreadLocal<Map<DataSource, ConnectionHolder>>:
当前线程 → Map
├─ DataSourceA → ConnectionHolderA
└─ DataSourceB → ConnectionHolderB
事务开启时,Spring 把 Connection 包成 ConnectionHolder 塞进去;JdbcTemplate.update() 执行时,先从 ThreadLocal 找,有就直接用,没有才向 DataSource 借。这就是 demo1 中 CONNECTION_HOLDER 的工业级版本。
这也解释了一个很容易踩的坑:多数据源时,如果 Service 上 @Transactional 用的是 DataSource A,但 DAO 中 JdbcTemplate 用的是 DataSource B,二者在 ThreadLocal 里是两个独立的 Key,事务自然管不住 DAO 的 SQL。
源码深入:从 @Transactional 到一行 SQL,Spring 走过了什么
行号以 Spring 5.3.x 主线为参考。不同小版本会有较大偏移(轻则 ±10 行,某些方法跨版本能差 ±30 行以上),这里给的是大致方位,不是精确坐标。要看自己版本的精确位置,IDEA 里 Cmd / Ctrl + 点进去就行。
讲到这里,你已经会用 @Transactional、知道传播行为、能避开自调用大坑。最后这一节做一件事:把代理打开,看看里面的发条。理解了这一段,事务这块基本就到顶了。
1. 入口:TransactionInterceptor#invoke
文件:org.springframework.transaction.interceptor.TransactionInterceptor
// 大约 line 119
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// line 121: 拿到目标类(注意:从 invocation.getThis() 拿,而不是 getClass(),因为 this 才是被代理的真实对象)
Class<?> targetClass = (invocation.getThis() != null
? AopUtils.getTargetClass(invocation.getThis()) : null);
// line 124: 把控制权交给父类 TransactionAspectSupport 的核心方法
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
这一段没什么逻辑,就两件事:找到目标类,把控制权委托给父类。invocation::proceed 是个方法引用,代表"如果你决定继续,调它就会进入真正的业务方法"。
TransactionInterceptor 实现了 MethodInterceptor(AOP 联盟标准接口),所以它本质就是 AOP 的一个普通拦截器,完全等价于一个 @Around 通知。
2. 核心:TransactionAspectSupport#invokeWithinTransaction
文件:org.springframework.transaction.interceptor.TransactionAspectSupport,大约 line 327~410。这是整个事务最核心的方法,我把它去掉一些校验代码后简化如下:
// 大约 line 327
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// line 333: 解析 @Transactional 注解或 XML 配置,得到属性(propagation/isolation/rollbackFor 等)
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null
? tas.getTransactionAttribute(method, targetClass) : null);
// line 336: 选 PlatformTransactionManager(支持多 TM 场景;省略了 Reactive 分支)
final TransactionManager tm = determineTransactionManager(txAttr);
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// line 367: 关键!按属性开启事务(若需要),返回 TransactionInfo
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// line 380: 执行业务方法 —— 这里就是你的 pay() 方法体
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// line 384: 捕获异常,根据 rollbackFor 决定回滚还是提交
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// line 388: 清理 ThreadLocal,把事务信息从线程上剥下来
cleanupTransactionInfo(txInfo);
}
// line 397: 正常返回 → 提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
把它翻译成一句话:
开事务 → try { 业务方法 } catch { 回滚 } finally { 清理 } → 提交
跟 demo3 里 @Around 的写法完全一致。Spring 事务本质就是个写死了的 @Around 切面,这个方法就是 proceed() 那一刻发生的所有事。
面试坑:
completeTransactionAfterThrowing怎么决定回滚?进去会看到它调
txInfo.transactionAttribute.rollbackOn(ex),而RuleBasedTransactionAttribute#rollbackOn(Throwable ex)的逻辑大致是:// 大约 line 130 public boolean rollbackOn(Throwable ex) { RollbackRuleAttribute winner = null; int deepest = Integer.MAX_VALUE; for (RollbackRuleAttribute rule : rollbackRules) { int depth = rule.getDepth(ex); if (depth >= 0 && depth < deepest) { deepest = depth; winner = rule; } } // 没匹配规则就走默认:RuntimeException 和 Error 才回滚 if (winner == null) { return super.rollbackOn(ex); } return !(winner instanceof NoRollbackRuleAttribute); }而父类
DefaultTransactionAttribute#rollbackOn就一行:return (ex instanceof RuntimeException || ex instanceof Error);。这就是"检查异常默认不回滚"的源头,一行代码而已。
3. ThreadLocal 工厂:TransactionSynchronizationManager
文件:org.springframework.transaction.support.TransactionSynchronizationManager
打开这个类,你会看到一组核心 ThreadLocal 字段:
// 大约 line 65~82
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
第一行 resources 就是 demo1 里 CONNECTION_HOLDER 的工业级版本。区别在于它的 Value 不是单个 Connection,而是 Map<DataSource, ConnectionHolder> —— 支持多数据源。Key 是 DataSource,Value 是包了 Connection 的 holder。
绑定 Connection 的方法:
// 大约 line 153
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
// line 156: 拿到当前线程的 Map
Map<Object, Object> map = resources.get();
if (map == null) {
map = new HashMap<>();
resources.set(map);
}
// line 161: 用 DataSource 当 Key,塞 ConnectionHolder
Object oldValue = map.put(actualKey, value);
if (oldValue != null) {
throw new IllegalStateException(/* 已经绑过了 */);
}
}
取 Connection:
// 大约 line 109
@Nullable
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
return doGetResource(actualKey);
}
// 大约 line 117
@Nullable
private static Object doGetResource(Object actualKey) {
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
return map.get(actualKey);
}
逻辑非常朴素:当前线程 → Map → 按 DataSource 取。Spring 的"事务上下文"全靠这一组 ThreadLocal 撑着,一切的传播、嵌套、隔离都建立在它之上。
4. JdbcTemplate 怎么"复用"事务里的 Connection
文件:org.springframework.jdbc.datasource.DataSourceUtils
JdbcTemplate.update() 内部最终会走到这里:
// 大约 line 80
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
return doGetConnection(dataSource);
}
// 大约 line 100
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
// line 105: 先从 ThreadLocal 拿 holder(关键!)
ConnectionHolder conHolder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
// line 108: 找到了,引用计数 +1,返回老 Connection
conHolder.requested();
if (!conHolder.hasConnection()) {
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
// line 120: 没找到,从 DataSource 拿一个新的
Connection con = fetchConnection(dataSource);
// line 124: 如果当前线程开了事务,把这个新 Connection 也绑到 ThreadLocal
if (TransactionSynchronizationManager.isSynchronizationActive()) {
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
} else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(/* ... */);
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
return con;
}
这一段是整个魔法的关键。它做的事很朴素,却解决了 demo1 里所有的痛点:
- 先看 ThreadLocal 里有没有当前 DataSource 的 holder(也就是 Service 上那个
@Transactional在方法开始时绑进去的那个); - 有就用老的,没有就拿新的;
- 如果有事务,新拿到的也注册进 ThreadLocal,后续 SQL 继续复用。
这就解释了为什么"Service 上一个 @Transactional,DAO 里十几条 SQL 全在同一个事务里" —— 它们都用了同一个 Connection,因为 getConnection 永远先看 ThreadLocal。
releaseConnection 也是同样逻辑的反过程:有事务就只 referenceCount--,真正归还连接池要等事务最后才执行。
5. 把整条链路串起来
你的代码: accountService.pay(...)
↓ (调到代理对象)
CglibAopProxy.intercept()
↓ (拦截器链)
TransactionInterceptor.invoke() line 119
↓
TransactionAspectSupport.invokeWithinTransaction() line 327
├─ createTransactionIfNecessary() → 开事务
│ └─ DataSourceTransactionManager.doBegin()
│ └─ con.setAutoCommit(false)
│ └─ TransactionSynchronizationManager.bindResource(ds, ch) line 153
├─ proceedWithInvocation() → 进入业务方法
│ └─ accountDao.outMoney(...)
│ └─ JdbcTemplate.update(...)
│ └─ DataSourceUtils.getConnection(dataSource) line 100
│ └─ TransactionSynchronizationManager.getResource(ds) line 109 ← 命中!复用同一个 Connection
├─ commitTransactionAfterReturning() / completeTransactionAfterThrowing()
│ └─ DataSourceTransactionManager.doCommit() / doRollback()
│ └─ con.commit() / con.rollback()
│ └─ TransactionSynchronizationManager.unbindResource(ds)
└─ cleanupTransactionInfo() → 清理 ThreadLocal,防内存泄漏
这条链你能从头到尾讲一遍,事务这块的源码深度就到位了。下次面试官问"@Transactional 怎么实现的",你不用说"代理 + ThreadLocal"这种笼统答案,直接画这张图,效果完全不一样。
一个收尾的小思考
demo1 自己写的那套
TxUtils + ThreadLocal<Connection> + JdkProxy,跟 Spring 的真实源码,架构完全一致:
demo1 自己写的 Spring 的对应物 JdkProxy.createProxy()AnnotationAwareAspectJAutoProxyCreatorTxUtils.startTransaction()DataSourceTransactionManager.doBegin()ThreadLocal<Connection>TransactionSynchronizationManager.resourcesTxUtils.commit / rollback / closecommitTransactionAfterReturning等invoke()里的 try-catch-finallyinvokeWithinTransaction的 try-catch-finally一句话:Spring 事务不是黑魔法,是把 demo1 这个玩具放大到工业级,加了传播行为、隔离级别、多数据源、事件回调、嵌套保存点。看到这,这一整篇文章就闭环了。
自测题:能答上来,事务这块就稳了
读完后用下面这些问题自检,我把面试官常问的角度都列上。能流畅讲出来,就基本不会被卡了。
- 为什么手写事务必须用 ThreadLocal?
- JDK 动态代理为什么必须有接口?CGLIB 怎么解决?Spring 怎么选?
execution(* com.qcbyjy.demo3.*ServiceImpl.*(..))五个部分各是什么?- AOP 五种通知在正常 / 异常情况下分别怎么执行?
@Around不调proceed()会怎样?它和@Before + @AfterReturning组合有什么本质区别?- JdbcTemplate 比原生 JDBC 省了什么?它把检查异常转成了什么?
update / query / queryForObject的差别?queryForObject 查不到数据会怎样?- XML 中
tx:advice与aop:advisor是什么关系?事务怎么织入的? - Spring 事务的本质是什么?(代理 + ThreadLocal + PlatformTransactionManager)
@Transactional默认只回滚什么异常?为什么?怎么改?- Spring Boot 自动配了哪三个跟事务相关的 Bean?分别来自哪些 AutoConfiguration?
@Transactional失效的常见 5 种场景?每种怎么修?- REQUIRED 和 REQUIRES_NEW 的区别?各举一个真实场景。
- 多数据源下事务为什么容易出问题?
写在最后
如果让我把整篇文章压成一句话,大概是:
Spring AOP 是对 JDK 动态代理的封装;Spring 事务是基于 AOP 的一种特殊增强;Spring Boot 是对 Spring 事务体系的自动装配。
从 demo1 的 TxUtils + JdkProxy,一路到 demo6 的 @Transactional,本质都是同一套思路:代理拦截方法,ThreadLocal 绑连接,统一 commit / rollback。
理解了 demo1,后面五个 demo 都只是给同一个核心思想换了不同的"语法糖"。
最后一个建议:别只看,把代码抄一遍跑一遍。手写 demo1 的过程,会让你后面读源码、面试答题、排查事务失效的速度翻倍。
如果你看完之后想再深入一层,可以读这两段源码:
org.springframework.transaction.interceptor.TransactionInterceptor#invoke(Spring 事务的"@Around")org.springframework.transaction.support.TransactionSynchronizationManager(那个工业级 ThreadLocal)
读完之后,你会发现 Spring 在做的事,你 demo1 已经做过一遍了 —— 只是它的实现更工程化、更通用而已。
更多推荐


所有评论(0)