代理模式:

静态代理(不推荐):

spring的aop框架就是对动态代理的简化
AOP面向切面编程思想:
aop是oop的补充和完善。
oop面向对象无法在子类继承父类的代码中局部修改某些代码,而aop就可以把冗余的重复性代码用动态代理模式提取到一个公共类中.
核心名词:




使用aop的步骤:
总体上分为两步,第一步就是正常编写业务代码:导入依赖,编写业务代码,编写ioc的配置类和文件,测试环境
第二步就是aop的部分:定义增强方法,编写增强类的配置,开启aop配置
初步实现过程举例:
第一步:
导入依赖:
<dependencies>
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.6</version>
</dependency>
<!--junit5测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<!-- spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.6</version>
</dependency>
</dependencies>
业务代码:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
@Component
public class CalculatorPureImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i+j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i-j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i*j;
return result;
}
@Override
public int div(int i, int j) {
int result = i/j;
return result;
}
}
注解配置类:
@Configuration
@ComponentScan(value = "com.atguigu")
@EnableAspectJAutoProxy //开启@Aspect注解
public class JavaConfig {
}
第二步:
/**
* ClassName:LogAdvice
* Package:com.atguigu.advice
* Description:
*增强类的内部存储增强代码
*
* 1.定义方法存储增强代码
* 具体定义几个方法,根据插入的位置决定
* 2.使用注解配置,插入指定目标方法的位置
* 前置 @Before
* 后置 @AfterReturning
* 异常 @AfterThrowing
* 最后 @After
* 环绕 @Around
* (环绕就是自定义位置)
* 3.配置切点表达式:
*
*4.补全注解
* 加入ioc容器 @Component
* 配置切面 @Aspect = 切点 + 增强方法
*
*
* 5.开启@Aspect注解类配置,在配置注解类中
* @Author 妄汐霜
* @Create 2026/3/22 19:01
* @Version 1.0
*/
@Component
@Aspect
public class LogAdvice {
@Before("execution(* com.atguigu.service.*.*(..))")
public void start(){
System.out.println("方法开始了");
}
@After("execution(* com.atguigu.service.*.*(..))")
public void after(){
System.out.println("方法结束了");
}
@AfterThrowing("execution(* com.atguigu.service.*.*(..))")
public void error(){
System.out.println("方法报错了");
}
}
使用:
@SpringJUnitConfig(value = JavaConfig.class)
public class SpringAopTest {
@Autowired
private Calculator calculator;
@Test
public void test(){
int add = calculator.add(1,2);
System.out.println(add);
}
@Test
public void test1(){
int add = calculator.add(1,2);
System.out.println(add);
}
}
获取切点详细信息的方式:
import java.lang.reflect.Modifier;
/**
* ClassName:MyAdvice
* Package:com.atguigu.advice
* Description:
*
* 定义四个增强方法,获取目标方法的信息 返回值 异常对象
*
* 1.定义方法
* 2.使用注解指定对应的位置
* 3.配置切点表达式选中方法
* 4.切面和ioc配置
* 5.开启aspectj注解的支持
*
* @Author 妄汐霜
* @Create 2026/3/22 19:31
* @Version 1.0
*
* 增强方法中要获取目标方法信息
* 1.全部增强方法中,获取目标方法的信息(方法名,参数,访问修饰符,所属的类的信息..)
* 想要获取这个的方法就是直接在形参列表中加(JoinPoint joinPoint),joinPoint中就包含目标方法的信息
* 2.返回的结果 - 只在@AfterReturning中有效
* (Object result) result接收返回结果
* @AfterReturning(value = "execution(* com..impl.*.*(..))",returning = "形参名即可")
*
* 3.异常的信息 - 只在@AfterThrowing中有效
* (Throwable t) t接收异常信息
* @AfterThrowing(value = "execution(* com..impl.*.*(..))",throwing = "形参名即可")
*
*/
@Aspect
@Component
public class MyAdvice {
@Before("execution(* com..impl.*.*(..))")
public void before(JoinPoint joinPoint) {
//1.获取方法属于的类的信息
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
//2.获取方法名称
int modifiers = joinPoint.getSignature().getModifiers();
String s = Modifier.toString(modifiers);
String name = joinPoint.getSignature().getName();//获取方法名
//3.获取参数列表
Object[] args = joinPoint.getArgs();//获取目标方法参数
}
@AfterReturning(value = "execution(* com..impl.*.*(..))",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result) {
}
@After("execution(* com..impl.*.*(..))")
public void after(JoinPoint joinPoint) {
}
@AfterThrowing("execution(* com..impl.*.*(..))")
public void afterThrowing(JoinPoint joinPoint) {
}
}
切点表达式语法:

语法细节
-
-
第一位:execution( ) 固定开头
-
第二位:方法访问修饰符
public private 直接描述对应修饰符即可
- 第三位:方法返回值
int String void 直接描述返回值类型
注意: 特殊情况 不考虑 访问修饰符和返回值 execution(* * ) 这是错误语法 execution(*) == 你只要考虑返回值 或者 不考虑访问修饰符 相当于全部不考虑了- 第四位:指定包的地址
固定的包: com.atguigu.api | service | dao
单层的任意命名: com.atguigu.* = com.atguigu.api com.atguigu.dao * = 任意一层的任意命名
任意层任意命名: com.. = com.atguigu.api.erdaye com.a.a.a.a.a.a.a ..任意层,任意命名 用在包上!
注意: ..不能用作包开头 public int .. 错误语法 com..
找到任何包下: *..- 第五位:指定类名称
固定名称: UserService
任意类名: *
部分任意: com..service.impl.*Impl
任意包任意类: ..- 第六位:指定方法名称
语法和类名一致
任意访问修饰符,任意类的任意方法: * ...*- 第七位:方法参数
第七位: 方法的参数描述
具体值: (String,int) != (int,String) 没有参数 ()
模糊值: 任意参数 有 或者 没有 (..) ..任意参数的意识
部分具体和模糊:
第一个参数是字符串的方法 (String..)
最后一个参数是字符串 (..String)
字符串开头,int结尾 (String..int)
包含int类型(..int..)
-
-
切点表达式案例
1.查询某包某类下,访问修饰符是公有,返回值是int的全部方法
2.查询某包下类中第一个参数是String的方法
3.查询全部包下,无参数的方法!
4.查询com包下,以int参数类型结尾的方法
5.查询指定包下,Service开头类的私有返回值int的无参数方法("execution(public int xx.xx.jj.j*(..))")
("execution(* xx.xx.jj.(String..))")
("execution( ...())")
("execution( com...(..int))")
("execution(private int xx.xx.Service*.*())")

同意切点管理:
@Aspect
@Component
public class MyAdvice {
/**
* 切点表达式的提取和复用:
* 1.当前类中提取
* 定义一个空方法
* 写注解(@Pointcut())
* 增强注解中引用切点表达式的方法
* 后面的注解如果要用就直接写方法名就可以
*
*
*/
@Pointcut("execution(* com..impl.*.*(..))")
public void pc(){}
@Before("pc()")
public void before(JoinPoint joinPoint) {
//1.获取方法属于的类的信息
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
//2.获取方法名称
int modifiers = joinPoint.getSignature().getModifiers();
String s = Modifier.toString(modifiers);
String name = joinPoint.getSignature().getName();//获取方法名
//3.获取参数列表
Object[] args = joinPoint.getArgs();//获取目标方法参数
}
@AfterReturning(value = "pc()",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result) {
}
@After("pc()")
public void after(JoinPoint joinPoint) {
}
@AfterThrowing("pc()")
public void afterThrowing(JoinPoint joinPoint) {
}
}
第二种:单独创建一个存储切点的类
/**
* ClassName:MyPointCut
* Package:com.atguigu
* Description:
*
* 存储切点的类
*
* @Author 妄汐霜
* @Create 2026/3/23 10:16
* @Version 1.0
*/
@Component
public class MyPointCut {
@Pointcut("execution(* com.atguigu.service.*.*(..))")
public void pc1(){}
@Pointcut("execution(* com.*.*(..))")
public void pc2(){}
//如果要引用这个类下的切点的话就要写:类的全限定符.方法名()
//比如:"com.atguigu.pointcut.MyPointCut.pc1()"
}
环绕通知(就是可以自定义位置的普通通知):
@Component
@Aspect
public class TxAroundAdvice {
@Around("com.atguigu.MyPointCut.pc1()")
public Object transaction(ProceedingJoinPoint joinPoint){
//保证目标方法被执行
Object[] args = joinPoint.getArgs();
Object result = null;
try {
System.out.println("开启事务");
result = joinPoint.proceed(args);
System.out.println("结束事务");
} catch (Throwable e) {
System.out.println("事物回滚");
throw new RuntimeException(e);
} finally {
}
return result;
}
}
切面物的优先级:

tx:
声明式事务:


添加事务的步骤:

测试:
-
准备项目
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.0.6</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.3.1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>6.0.6</version> <scope>test</scope> </dependency> <dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.25</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>6.0.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>6.0.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>6.0.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.0.6</version> </dependency> </dependencies> -
外部配置文件 jdbc.properties
atguigu.url=jdbc:mysql://localhost:3306/studb
atguigu.driver=com.mysql.cj.jdbc.Driver
atguigu.username=root
atguigu.password=root -
spring配置文件
@Configuration
@ComponentScan("com.atguigu")
@PropertySource("classpath:jdbc.properties")
public class JavaConfig {@Value("${atguigu.driver}") private String driver; @Value("${atguigu.url}") private String url; @Value("${atguigu.username}") private String username; @Value("${atguigu.password}") private String password; //druid连接池 @Bean public DataSource dataSource(){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } @Bean //jdbcTemplate public JdbcTemplate jdbcTemplate(DataSource dataSource){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; }}
-
准备dao/service层 dao
@Repository
public class StudentDao {@Autowired private JdbcTemplate jdbcTemplate; public void updateNameById(String name,Integer id){ String sql = "update students set name = ? where id = ? ;"; int rows = jdbcTemplate.update(sql, name, id); } public void updateAgeById(Integer age,Integer id){ String sql = "update students set age = ? where id = ? ;"; jdbcTemplate.update(sql,age,id); }}
service
@Service
public class StudentService {@Autowired private StudentDao studentDao; public void changeInfo(){ studentDao.updateAgeById(100,1); System.out.println("-----------"); studentDao.updateNameById("test1",1); }}
-
测试环境搭建
/**
-
projectName: com.atguigu.test
-
description:
*/
@SpringJUnitConfig(JavaConfig.class)
public class TxTest {@Autowired
private StudentService studentService;@Test
public void testTx(){
studentService.changeInfo();
}
}
-
添加事务
-
配置事务管理器 数据库相关的配置
/**
- projectName: com.atguigu.config
- description: 数据库和连接池配置类
*/
@Configuration
@ComponenScan("com.atguigu")
@PropertySource(value = "classpath:jdbc.properties")
@EnableTransactionManagement
public class DataSourceConfig {/** * 实例化dataSource加入到ioc容器 * @param url * @param driver * @param username * @param password * @return */ @Bean public DataSource dataSource(@Value("${atguigu.url}")String url, @Value("${atguigu.driver}")String driver, @Value("${atguigu.username}")String username, @Value("${atguigu.password}")String password){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } /** * 实例化JdbcTemplate对象,需要使用ioc中的DataSource * @param dataSource * @return */ @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } /** * 装配事务管理实现对象 * @param dataSource * @return */ @Bean public TransactionManager transactionManager(DataSource dataSource){ return new DataSourceTransactionManager(dataSource); }}
配置类使用注解:
@Configuration
@ComponentScan("com.atguigu")
@PropertySource("classpath:jdbc.properties")
//@EnableAspectJAutoProxy//这个注解是开启aspectj注解的支持
@EnableTransactionManagement//开启事务注解的支持
public class JavaConfig {
//druid连接池
@Value("${atguigu.driver}")
private String driver;
@Value("${atguigu.username}")
private String username;
@Value("${atguigu.password}")
private String password;
@Value("${atguigu.url}")
private String url;
@Bean
public DataSource dataSource() {
//实例化的是具体的实现类
DruidDataSource druidDataSource = new DruidDataSource();
//配置连接信息
druidDataSource.setDriverClassName(driver);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
//返回值是接口
return druidDataSource;
}
//jdbcTemplate
@Bean
public JdbcTemplate jdbcTemplate() {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource());
return jdbcTemplate;
}
@Bean
public TransactionManager tarangTransactionManager(DataSource dataSource) {
//内部要进行事务的操作,基于的连接池
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
//需要连接池对象
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
-
使用声明事务注解@Transactional
/**
- projectName: com.atguigu.service
*/
@Service
public class StudentService {@Autowired private StudentDao studentDao; @Transactional public void changeInfo(){ studentDao.updateAgeById(100,1); System.out.println("-----------"); int i = 1/0; studentDao.updateNameById("test1",1); }}
-
测试事务效果
/**
-
projectName: com.atguigu.test
-
description:
*/
//@SpringJUnitConfig(locations = "classpath:application.xml")
@SpringJUnitConfig(classes = DataSourceConfig.class)
public class TxTest {@Autowired
private StudentService studentService;@Test
public void testTx(){
studentService.changeInfo();
}
}
-
只读模式:

事务属性超时时间设置:

-
需求 事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。 概括来说就是一句话:超时回滚,释放资源。
-
设置超时时间
@Service
public class StudentService {@Autowired private StudentDao studentDao; /** * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间! */ @Transactional(readOnly = false,timeout = 3) public void changeInfo(){ studentDao.updateAgeById(100,1); //休眠4秒,等待方法超时! try { Thread.sleep(4000); } catch (InterruptedException e) { throw new RuntimeException(e); } studentDao.updateNameById("test1",1); }}
-
测试超时效果 执行抛出事务超时异常
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed May 24 09:10:43 IRKT 2023
at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:155)
at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInMillis(ResourceHolderSupport.java:144)
at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInSeconds(ResourceHolderSupport.java:128)
at org.springframework.jdbc.datasource.DataSourceUtils.applyTimeout(DataSourceUtils.java:341)
at org.springframework.jdbc.core.JdbcTemplate.applyStatementSettings(JdbcTemplate.java:1467)
事务属性:事务异常:
Spring 事务(主要指@Transactional注解实现的声明式事务)的本质是:Spring 通过 AOP 代理,在方法执行前开启事务,方法正常执行完提交事务,方法抛出指定异常时回滚事务。
这里有个新手必踩的 "生死线" 规则:
Spring 默认只对 运行时异常(RuntimeException) 和 Error 触发事务回滚;对 检查型异常(Checked Exception) (比如IOException、SQLException),默认不回滚!
- 运行时异常:不用手动
try/catch,抛出去程序直接崩(比如NullPointerException、IllegalArgumentException); - 检查型异常:必须手动
try/catch或throws声明(比如调用文件流的IOException、操作数据库的SQLException)。
一、最常见的事务异常:事务 "该回滚却没回滚"
这是新手遇到最多的事务异常场景(甚至不算 "报错",但结果不符合预期),比真正的异常更坑。
1. 核心原因(按踩坑频率排序)
原因 1:抛出的是 "检查型异常",Spring 默认不回滚
反例代码(坑!):
java
运行
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 加了事务注解,但抛出检查型异常,事务不会回滚!
@Transactional
public void transferMoney() throws SQLException {
// 扣减A账户余额
jdbcTemplate.update("update user set money = money - 100 where id = 1");
// 模拟数据库异常(检查型异常SQLException)
throw new SQLException("数据库操作失败");
// 新增B账户余额(这行不会执行,但扣减A的操作不会回滚!)
// jdbcTemplate.update("update user set money = money + 100 where id = 2");
}
}
执行结果:A 账户的钱被扣了,但 B 账户没加,数据不一致 ------ 因为SQLException是检查型异常,Spring 默认不回滚。
解决方案 :手动指定 "哪些异常要回滚"(推荐直接指定Exception.class,覆盖所有异常):
java
运行
// rollbackFor = Exception.class:所有异常都触发回滚
@Transactional(rollbackFor = Exception.class)
public void transferMoney() throws SQLException {
// 业务逻辑不变
}
也可以指定具体异常:@Transactional(rollbackFor = SQLException.class)。
原因 2:事务方法被 "内部调用"(Spring AOP 的坑)
反例代码(坑!):
java
运行
@Service
public class UserService {
// 非事务方法
public void outerMethod() {
// 内部调用事务方法
innerTransactionalMethod();
}
// 事务注解加在这里,但内部调用不会生效!
@Transactional(rollbackFor = Exception.class)
public void innerTransactionalMethod() {
jdbcTemplate.update("update user set money = money - 100 where id = 1");
throw new RuntimeException("模拟异常");
}
}
执行结果:事务没生效,数据不会回滚。为什么 :Spring 事务靠 AOP 代理实现 ------ 只有外部调用事务方法时,才会走代理类(开启事务);内部调用(同一个类里的方法调用)不会走代理,事务注解直接失效。
解决方案:
-
方案 1:把事务方法抽成独立的 Service 类(推荐);
-
方案 2:手动获取代理类调用(不推荐,代码不优雅):java运行
// 注入自身的代理对象(需要开启暴露代理:@EnableAspectJAutoProxy(exposeProxy = true))
@Autowired
private UserService self;public void outerMethod() {
// 调用代理类的事务方法
self.innerTransactionalMethod();
}
原因 3:@Transactional 加在 private 方法上
java
运行
@Service
public class UserService {
// 坑:private方法无法被AOP代理,事务注解完全无效!
@Transactional(rollbackFor = Exception.class)
private void transferMoney() {
// 业务逻辑
}
}
解决方案 :@Transactional只能加在public方法上(protected 也勉强可以,但几乎不用)。
原因 4:没配置事务管理器
如果你的项目是纯 Spring(非 SpringBoot),只加@Transactional不会生效 ------ 因为 Spring 不知道用哪个 "事务管理器" 来管理事务。
解决方案:在 JavaConfig 中配置事务管理器 Bean:
java
运行
@Configuration
public class JavaConfig {
@Autowired
private DataSource dataSource;
// 配置事务管理器(必须!)
@Bean
public PlatformTransactionManager transactionManager() {
// Druid/Hikari数据源都用这个
return new DataSourceTransactionManager(dataSource);
}
}
二、真正的 "报错型" 事务异常(带异常提示)
1. TransactionSystemException(事务系统异常)
- 通俗含义:Spring 事务底层出问题了(提交 / 回滚失败、连接断了、超时了);
- 常见原因:① 数据库连接断开(比如数据库服务停了);② 事务执行超时(比如方法执行太久,超过数据库 / 事务的超时时间);③ 数据源配置错误(比如用户名密码错了,连不上库);
- 解决方案 :✅ 检查数据库是否正常运行,连接信息是否正确;✅ 调整事务超时时间:
@Transactional(timeout = 30)(单位:秒,默认超时由数据库决定);✅ 查看异常堆栈的 "Caused by",定位具体原因(比如数据库锁等待超时)。
2. NoTransactionException(无事务异常)
-
通俗含义:代码想操作事务(比如手动回滚),但当前根本没有活跃的事务;
-
常见场景:java运行
@Service
public class UserService {
@Autowired
private TransactionStatus transactionStatus;// 无事务注解的方法,手动回滚会报错 public void test() { transactionStatus.setRollbackOnly(); // 抛出NoTransactionException }}
-
解决方案 :✅ 确保方法加了
@Transactional,且事务生效(public、外部调用);✅ 不要手动操作事务状态,除非明确知道当前有事务。
3. NestedTransactionNotSupportedException(嵌套事务不支持)
- 通俗含义:你想创建 "嵌套事务"(一个事务里包另一个事务),但数据库 / 连接池不支持;
- 常见原因 :Spring 事务传播行为设为
PROPAGATION_NESTED(嵌套事务),但 MySQL 默认不支持嵌套事务(InnoDB 只支持事务,不支持嵌套); - 解决方案 :✅ 改用默认的传播行为
PROPAGATION_REQUIRED(如果当前有事务就加入,没有就新建);✅ 不要滥用嵌套事务,用REQUIRED/REQUIRES_NEW即可满足 99% 的场景。
三、避坑总结(核心要点)
- 回滚规则 :
@Transactional默认只回滚运行时异常,检查型异常要手动加rollbackFor = Exception.class; - 事务生效前提 :注解加在
public方法上、外部调用(非内部调用)、配置了事务管理器; - 异常排查技巧:
-
- 事务没回滚:先看抛的是什么异常,再看方法是否是 public / 外部调用;
- 报 TransactionSystemException:先查数据库连接和超时时间;
- 嵌套事务异常:改传播行为为默认的 REQUIRED。
事务的隔离级别:

/**
- projectName: com.atguigu.service
*/
@Service
public class StudentService { @Autowired
private StudentDao studentDao; /**
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
//主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
-
- timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
- rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
- noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
- isolation = 设置事务的隔离级别,mysql默认是repeatable read!
*/
@Transactional(readOnly = false,
timeout = 3,
rollbackFor = Exception.class,
noRollbackFor = FileNotFoundException.class,
isolation = Isolation.REPEATABLE_READ)
}
隔离级别的设置是这个:
isolation = Isolation.REPEATABLE_READ)
最好是设置成读已提交的那一档,这样即避免了脏读,又保证了一定的效率
事务传播行为:
定义:事物之间调用,如何影响子事务
事务传播行为是 Spring 事务中最核心也最易混淆 的概念,它解决的核心问题是:当一个带有事务的方法调用另一个带有事务的方法时,两个方法的事务该如何协调(共用 / 新建 / 挂起等)。
先给一个通俗比喻:把事务 比作「会议室」,方法比作「开会的人」。传播行为就是「当 A 已经在会议室(有事务),叫 B 来开会时,B 该如何处理这个会议室」。
Spring 中所有传播行为都定义在 org.springframework.transaction.annotation.Propagation 枚举中,核心有 7 种,下面逐一拆解(重点讲最常用的 3 个)。
一、核心传播行为(按使用频率排序)
1. Propagation.REQUIRED(默认值)
- 核心逻辑:如果当前已有事务,就「加入」这个事务;如果没有,就「新建」一个事务。
- 比喻:A 有会议室,B 直接进来一起用;A 没会议室,B 自己开新会议室。
- 适用场景:绝大多数业务场景(如「下单 + 扣库存」,要求两个操作在同一个事务里,要么都成功,要么都失败)。
2. Propagation.REQUIRES_NEW
- 核心逻辑:不管当前有没有事务,都「强制新建」一个独立事务;如果当前有事务,先把原事务「挂起」,等新事务执行完再恢复原事务。
- 比喻:不管 A 有没有会议室,B 都新开一个独立会议室;A 的会议室先锁起来,等 B 开完会再解锁。
- 适用场景:需要独立事务的操作(如「记录操作日志」,不管主业务成功 / 失败,日志都要保存)。
3. Propagation.NESTED
- 核心逻辑:如果当前有事务,就在当前事务里「嵌套一个子事务」(依赖数据库保存点 Savepoint);如果没有,就新建事务。
-
- 主事务回滚 → 子事务必回滚;
- 子事务回滚(仅回滚子范围)→ 主事务可继续。
- 比喻:A 在大会议室,B 在大会议室里隔个小隔间开会;大会议室散会,小隔间也散;小隔间自己散会,大会议室还能继续。
- 注意:需数据库支持保存点(如 MySQL InnoDB)。
4. 其他辅助传播行为
表格
|---------------|--------------------|-----------------------|
| 传播行为 | 核心逻辑 | 比喻 / 场景 |
| SUPPORTS | 有事务就加入,无事务就非事务执行 | 有会议室就开,没有就站走廊开(查询类方法) |
| NOT_SUPPORTED | 非事务执行,有事务则挂起 | 只站走廊开,有会议室先锁起来(纯查询) |
| MANDATORY | 必须在已有事务中执行,无事务则抛异常 | 必须进别人的会议室,没会议室就报错 |
| NEVER | 必须非事务执行,有事务则抛异常 | 绝对不进会议室,有会议室就报错 |
二、代码实战(最常用的 REQUIRED + REQUIRES_NEW)
通过「下单 + 记录日志」的场景,直观演示传播行为的效果。
1. 前置准备
-
环境:Spring Boot 项目,引入依赖:xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tx</artifactId> </dependency> -
数据库表(MySQL):sql
-- 订单/用户表(模拟下单)
CREATE TABLEuser(
idint NOT NULL AUTO_INCREMENT,
namevarchar(255) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;-- 操作日志表
CREATE TABLEoperation_log(
idint NOT NULL AUTO_INCREMENT,
contentvarchar(255) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
2. 核心代码
java
运行
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
// 订单服务(主事务)
@Service
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private LogService logService;
// 默认传播行为:REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(String userName) {
try {
// 1. 插入用户(模拟下单)
jdbcTemplate.update("INSERT INTO user(name) VALUES (?)", userName);
// 2. 调用日志服务(独立事务)
logService.recordLog("创建订单,用户名:" + userName);
// 模拟业务异常,触发主事务回滚
if ("test".equals(userName)) {
throw new RuntimeException("下单失败,主事务回滚");
}
} catch (Exception e) {
throw new RuntimeException("创建订单失败", e);
}
}
}
// 日志服务(独立事务)
@Service
public class LogService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 传播行为:REQUIRES_NEW(强制新建独立事务)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordLog(String content) {
jdbcTemplate.update("INSERT INTO operation_log(content) VALUES (?)", content);
}
}
3. 测试代码 & 结果
java
运行
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class TransactionTest {
@Autowired
private OrderService orderService;
@Test
public void testPropagation() {
try {
// 传入test,触发主事务回滚
orderService.createOrder("test");
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果:
user表无「test」记录(主事务回滚);operation_log表有「创建订单,用户名:test」记录(REQUIRES_NEW 是独立事务,不受主事务影响)。
三、关键对比(易混淆的传播行为)
表格
|--------|--------------|------------------|------------|
| 场景 | REQUIRED | REQUIRES_NEW | NESTED |
| 主事务回滚 | 子方法也回滚 | 子方法不回滚(独立) | 子方法也回滚 |
| 子方法回滚 | 主事务一起回滚 | 主事务不受影响 | 主事务可继续执行 |
| 事务独立性 | 共用一个事务 | 完全独立的事务 | 嵌套在主事务中 |
总结
- REQUIRED:默认值,「共用 / 新建」事务,适用于绝大多数业务(如下单 + 扣库存);
- REQUIRES_NEW:「强制新建」独立事务,适用于日志、消息通知等不依赖主事务的场景;
- NESTED:「嵌套子事务」,依赖主事务但可独立回滚子范围,需数据库支持保存点。
核心记住:传播行为解决的是「多事务方法调用时的事务协调规则」,选择时只需明确「子方法是否需要和主方法共用事务」即可。
操作方法就是在 @Transactional加事务标签就行