小白学习笔记(spring框架的aop和tx)

代理模式:

静态代理(不推荐):

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. 切点表达式案例

    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:

声明式事务:

添加事务的步骤:

测试:

  1. 准备项目

    <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>
  2. 外部配置文件 jdbc.properties

    atguigu.url=jdbc:mysql://localhost:3306/studb
    atguigu.driver=com.mysql.cj.jdbc.Driver
    atguigu.username=root
    atguigu.password=root

  3. 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;
     }

    }

  4. 准备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);
     }

    }

  5. 测试环境搭建

    /**

    • projectName: com.atguigu.test

    • description:
      */
      @SpringJUnitConfig(JavaConfig.class)
      public class TxTest {

      @Autowired
      private StudentService studentService;

      @Test
      public void testTx(){
      studentService.changeInfo();
      }
      }

添加事务

  1. 配置事务管理器 数据库相关的配置

    /**

    • 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;
    }

}
  1. 使用声明事务注解@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);
     }

    }

  2. 测试事务效果

    /**

    • 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();
      }
      }

只读模式:

事务属性超时时间设置:

  1. 需求 事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。 概括来说就是一句话:超时回滚,释放资源。

  2. 设置超时时间

    @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);
     }

    }

  3. 测试超时效果 执行抛出事务超时异常

    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) (比如IOExceptionSQLException),默认不回滚

  • 运行时异常:不用手动try/catch,抛出去程序直接崩(比如NullPointerExceptionIllegalArgumentException);
  • 检查型异常:必须手动try/catchthrows声明(比如调用文件流的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% 的场景。

三、避坑总结(核心要点)

  1. 回滚规则@Transactional默认只回滚运行时异常,检查型异常要手动加rollbackFor = Exception.class
  2. 事务生效前提 :注解加在public方法上、外部调用(非内部调用)、配置了事务管理器;
  3. 异常排查技巧
    • 事务没回滚:先看抛的是什么异常,再看方法是否是 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 TABLE user (
    id int NOT NULL AUTO_INCREMENT,
    name varchar(255) DEFAULT NULL,
    PRIMARY KEY (id)
    ) ENGINE=InnoDB;

    -- 操作日志表
    CREATE TABLE operation_log (
    id int NOT NULL AUTO_INCREMENT,
    content varchar(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 |
| 主事务回滚 | 子方法也回滚 | 子方法不回滚(独立) | 子方法也回滚 |
| 子方法回滚 | 主事务一起回滚 | 主事务不受影响 | 主事务可继续执行 |
| 事务独立性 | 共用一个事务 | 完全独立的事务 | 嵌套在主事务中 |


总结

  1. REQUIRED:默认值,「共用 / 新建」事务,适用于绝大多数业务(如下单 + 扣库存);
  2. REQUIRES_NEW:「强制新建」独立事务,适用于日志、消息通知等不依赖主事务的场景;
  3. NESTED:「嵌套子事务」,依赖主事务但可独立回滚子范围,需数据库支持保存点。

核心记住:传播行为解决的是「多事务方法调用时的事务协调规则」,选择时只需明确「子方法是否需要和主方法共用事务」即可。

操作方法就是在 @Transactional加事务标签就行

相关推荐
sheeta19983 小时前
LeetCode 每日一题笔记 日期:2025.03.24 题目:2906.构造乘积矩阵
笔记·leetcode·矩阵
椎4953 小时前
JSONUtil工具包大致学习使用
学习
leiming64 小时前
CAN 通信协议学习讲义(带图文 + C 语言代码)
c语言·开发语言·学习
Heartache boy4 小时前
野火STM32_HAL库版课程笔记-ADC多通道采集热敏、光敏、反射传感器(轮询)
笔记·stm32·单片机
yoothey5 小时前
Java字节流与字符流核心笔记(问答+考点复盘)
java·开发语言·笔记
星空5 小时前
RAG学习第一节
学习
知识分享小能手5 小时前
MongoDB入门学习教程,从入门到精通,MongoDB入门指南 —— 知识点详解(2)
数据库·学习·mongodb
老师好,我是刘同学5 小时前
force与deposit在SystemVerilog中的区别详解
笔记
炽烈小老头5 小时前
【 每天学习一点算法 2026/03/24】寻找峰值
学习·算法