手写一遍 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 想干嘛。

要让事务成立,必须做到三件事:

  1. 关掉 autoCommit,SQL 不再自动提交;
  2. 所有 SQL 共用同一个 Connection;
  3. 调用链结束时统一决定 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 你应该能感受到:

  1. 侵入性大:每个要事务的对象都要被 JdkProxy.createProxy() 包一次。
  2. 重复造轮子:每个项目都要写一份 TxUtils。
  3. 能力有限:没有传播行为,没有隔离级别,没有只读优化,没有超时控制。
  4. 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 干了什么

启动时:

  1. Spring 看到 @Aspect 类,知道这是个切面;
  2. 解析 @Before 里的 execution 表达式;
  3. 找到所有匹配的 Bean(这里是 UserServiceImpl);
  4. 给它们造一个代理对象,代理逻辑里嵌入 @Before 方法;
  5. 当你 @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

维度demo1demo2
谁创建代理自己写 JdkProxySpring 自动
织入方式编程式声明式(注解)
切面定义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

操作原生 JDBCJdbcTemplate
获取连接DriverManager.getConnection(...)框架自动
创建 Statementconn.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。所以哪怕你这一节明面上叫"无事务版本",事务也已经在帮你兜底了。

如果你想真的复现"钱消失"的灾难,有两种办法:

  1. 把 @Transactional 注释掉再跑一次 testPayWithoutTransaction;
  2. 或者保留 @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&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;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 干了下面几件事。

启动时
  1. @EnableTransactionManagement 注册了一个 BeanPostProcessor(底层是 InfrastructureAdvisorAutoProxyCreator);
  2. 后处理器扫描每个 Bean,判断是否有 @Transactional 方法;
  3. 有的话,给它造一个代理对象,代理里嵌入 TransactionInterceptor(本质就是 @Around);
  4. 容器中替换为代理对象,后续 @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,后者会:

  1. 读取 META-INF/spring.factories(2.x)或 META-INF/spring/...AutoConfiguration.imports(3.x);
  2. 拿到一堆 XxxAutoConfiguration 类;
  3. 按条件注册它们(@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

维度DriverManagerDataSourceDruid / HikariCP
类型每次新建连接(不是连接池)连接池
性能极差(每次 TCP 三次握手 + 鉴权)极好(连接复用)
用途仅供测试(demo6 用了它)生产必备
配置url / user / pwd还能配最大连接数、空闲时间、SQL 监控
监控无Druid 自带 SQL 监控页面

demo5 的 XML 用了 Druid,demo6 的 JavaConfig 用了 DriverManagerDataSource。前者更接近生产,后者纯粹是为了演示简洁。生产代码一律用 Druid 或 HikariCP。

手写事务 vs Spring 事务

维度demo1 手写demo5 / demo6 Spring
连接绑定自己写 ThreadLocalTransactionSynchronizationManager(本质也是 ThreadLocal)
开/提/回手动 conn.commit / rollbackPlatformTransactionManager 抽象
织入方式手写 JDK 代理Spring AOP 自动代理
传播行为自己实现要上百行配置一行搞定
隔离级别自己 conn.setTransactionIsolation@Transactional(isolation = ...)
异常回滚规则自己 if-elserollbackFor = 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 里所有的痛点:

  1. 先看 ThreadLocal 里有没有当前 DataSource 的 holder(也就是 Service 上那个 @Transactional 在方法开始时绑进去的那个);
  2. 有就用老的,没有就拿新的;
  3. 如果有事务,新拿到的也注册进 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()AnnotationAwareAspectJAutoProxyCreator
TxUtils.startTransaction()DataSourceTransactionManager.doBegin()
ThreadLocal<Connection>TransactionSynchronizationManager.resources
TxUtils.commit / rollback / closecommitTransactionAfterReturning 等
invoke() 里的 try-catch-finallyinvokeWithinTransaction 的 try-catch-finally

一句话:Spring 事务不是黑魔法,是把 demo1 这个玩具放大到工业级,加了传播行为、隔离级别、多数据源、事件回调、嵌套保存点。看到这,这一整篇文章就闭环了。


自测题:能答上来,事务这块就稳了

读完后用下面这些问题自检,我把面试官常问的角度都列上。能流畅讲出来,就基本不会被卡了。

  1. 为什么手写事务必须用 ThreadLocal?
  2. JDK 动态代理为什么必须有接口?CGLIB 怎么解决?Spring 怎么选?
  3. execution(* com.qcbyjy.demo3.*ServiceImpl.*(..)) 五个部分各是什么?
  4. AOP 五种通知在正常 / 异常情况下分别怎么执行?
  5. @Around 不调 proceed() 会怎样?它和 @Before + @AfterReturning 组合有什么本质区别?
  6. JdbcTemplate 比原生 JDBC 省了什么?它把检查异常转成了什么?
  7. update / query / queryForObject 的差别?queryForObject 查不到数据会怎样?
  8. XML 中 tx:advice 与 aop:advisor 是什么关系?事务怎么织入的?
  9. Spring 事务的本质是什么?(代理 + ThreadLocal + PlatformTransactionManager)
  10. @Transactional 默认只回滚什么异常?为什么?怎么改?
  11. Spring Boot 自动配了哪三个跟事务相关的 Bean?分别来自哪些 AutoConfiguration?
  12. @Transactional 失效的常见 5 种场景?每种怎么修?
  13. REQUIRED 和 REQUIRES_NEW 的区别?各举一个真实场景。
  14. 多数据源下事务为什么容易出问题?

写在最后

如果让我把整篇文章压成一句话,大概是:

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 已经做过一遍了 —— 只是它的实现更工程化、更通用而已。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐