Day618.Spring事务常见错误① -Spring编程常见错误

Spring事务常见错误①

Spring 事务管理包含两种配置方式

  • 第一种是使用XML 进行模糊匹配,绑定事务管理;
  • 第二种是使用注解,这种方式可以对每个需要进行事务处理的方法进行单独配置,你只需要添加上 @Transactional,然后在注解内添加属性配置即可。

在我们的错误案例示范中,我们统一使用更为方便的注解式方式


一、环境前置

我们还需要引入一些配置文件和类,简单列举一下。

  • 数据库配置文件 jdbc.properties,配置了数据连接信息。

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
    jdbc.username=root
    jdbc.password=achang
    
  • JDBC 的配置类,从上述 jdbc.properties 加载相关配置项,并创建 JdbcTemplate、DataSource、TransactionManager 相关的 Bean 等。

    public class JdbcConfig {
        @Value("${jdbc.driver}")
        private String driver;
    
        @Value("${jdbc.url}")
        private String url;
    
        @Value("${jdbc.username}")
        private String username;
    
        @Value("${jdbc.password}")
        private String password;
    
        @Bean(name = "jdbcTemplate")
        public JdbcTemplate createJdbcTemplate(DataSource dataSource) {
            return new JdbcTemplate(dataSource);
        }
    
        @Bean(name = "dataSource")
        public DataSource createDataSource() {
            DriverManagerDataSource ds = new DriverManagerDataSource();
            ds.setDriverClassName(driver);
            ds.setUrl(url);
            ds.setUsername(username);
            ds.setPassword(password);
            return ds;
        }
    
        @Bean(name = "transactionManager")
        public PlatformTransactionManager      createTransactionManager(DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }
    
  • Student 定义如下:

    public class Student implements Serializable {
        private Integer id;
        private String realname;
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public String getRealname() {
            return realname;
        }
        public void setRealname(String realname) {
            this.realname = realname;
        }
    }
    
    
  • Student 对应的 Mapper 类定义如下:

    @Mapper
    public interface StudentMapper {
        @Insert("INSERT INTO `student`(`realname`) VALUES (#{realname})")
        void saveStudent(Student student);
    }
    
    
  • 对应数据库表的 Schema 如下:

    CREATE TABLE `student` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `realname` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
  • 应用配置类,通过注解的方式,配置了数据源、MyBatis Mapper 的扫描路径以及事务等。

    @Configuration
    @ComponentScan
    @Import({JdbcConfig.class})
    @PropertySource("classpath:jdbc.properties")
    @MapperScan("com.spring.puzzle.others.transaction.example1")
    @EnableTransactionManagement
    @EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
    @EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
    public class AppConfig {
        public static void main(String[] args) throws Exception {
            ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        }
    }
    

二、默认异常不回滚问题

我们如下定义声明:

@Service
public class StudentService {
    @Autowired
    private StudentMapper studentMapper;

    @Transactional
    public void saveStudent(String realname) throws Exception {
        Student student = new Student();
        student.setRealname(realname);
        studentMapper.saveStudent(student);
        if (student.getRealname().equals("小明")) {
            throw new Exception("该学生已存在");
        }
    }
}

然后使用下面的代码来测试一下,保存一个叫小明的学生,看会不会触发事务的回滚。

public class AppConfig {
    public static void main(String[] args) throws Exception {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        StudentService studentService = (StudentService) context.getBean("studentService");
        studentService.saveStudent("小明");
    }
}

执行结果打印出了这样的信息:

Exception in thread “main” java.lang.Exception: 该学生已存在
at com.spring.puzzle.others.transaction.example1.StudentService.saveStudent(StudentService.java:23)

可以看到,异常确实被抛出来,但是检查数据库,你会发现数据库里插入了一条新的记录。

但是我们的常规思维可能是:在 Spring 里,抛出异常,就会导致事务回滚,而回滚以后,是不应该有数据存入数据库才对啊。而在这个案例中,异常也抛了,回滚却没有如期而至,这是什么原因呢?


通过 debug 沿着 saveStudent 继续往下跟,得到了一个这样的调用栈:
在这里插入图片描述

从这个调用栈中我们看到了熟悉的 CglibAopProxy,另外事务本质上也是一种特殊的切面,在创建的过程中,被 CglibAopProxy 代理。

事务处理的拦截器是 TransactionInterceptor,它支撑着整个事务功能的架构,我们来分析下这个拦截器是如何实现事务特性的。

首先,TransactionInterceptor 继承类 TransactionAspectSupport,实现了接口 MethodInterceptor

当执行代理类的目标方法时,会触发 invoke()

由于我们的关注重点是在异常处理上,所以直奔主题,跳到异常处理相关的部分。当它 catch 到异常时,会调用 completeTransactionAfterThrowing 方法做进一步处理。

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
      final InvocationCallback invocation) throws Throwable {
      //省略非关键代码
      Object retVal;
      try {
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         cleanupTransactionInfo(txInfo);
      }
      //省略非关键代码
}

在 completeTransactionAfterThrowing 的代码中,有这样一个方法 rollbackOn(),这是事务的回滚的关键判断条件

当这个条件满足时,会触发 rollback 操作,事务回滚。

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    //省略非关键代码
    //判断是否需要回滚
    if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
       try {
       //执行回滚
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
       }
       catch (TransactionSystemException ex2) {
          ex2.initApplicationException(ex);
          throw ex2;
       }
       catch (RuntimeException | Error ex2) {
          throw ex2;
       }
    }
    //省略非关键代码
}

rollbackOn() 其实包括了两个层级,具体可参考如下代码:

public boolean rollbackOn(Throwable ex) {
   // 层级 1:根据"rollbackRules"及当前捕获异常来判断是否需要回滚
   RollbackRuleAttribute winner = null;
   int deepest = Integer.MAX_VALUE;
   if (this.rollbackRules != null) {
      for (RollbackRuleAttribute rule : this.rollbackRules) {
         // 当前捕获的异常可能是回滚“异常”的继承体系中的“一员”
         int depth = rule.getDepth(ex);
         if (depth >= 0 && depth < deepest) {
            deepest = depth;
            winner = rule;
         }
      }
   }
   // 层级 2:调用父类的 rollbackOn 方法来决策是否需要 rollback
   if (winner == null) {
      return super.rollbackOn(ex);
   }
   return !(winner instanceof NoRollbackRuleAttribute);
}
  • RuleBasedTransactionAttribute 自身的 rollbackOn()当我们在 @Transactional 中配置了 rollbackFor,这个方法就会用捕获到的异常和 rollbackFor 中配置的异常做比较。如果捕获到的异常是 rollbackFor 配置的异常或其子类,就会直接 rollback。在我们的案例中,由于在事务的注解中没有加任何规则,所以这段逻辑处理其实找不到规则(即 winner == null),进而走到下一步。
  • RuleBasedTransactionAttribute 父类 DefaultTransactionAttribute 的 rollbackOn()如果没有在 @Transactional 中配置 rollback 属性,或是捕获到的异常和所配置异常的类型不一致,就会继续调用父类的 rollbackOn() 进行处理。而在父类的 rollbackOn() 中,我们发现了一个重要的线索,只有在异常类型为 RuntimeException 或者 Error 的时候才会返回 true,此时,会触发 completeTransactionAfterThrowing 方法中的 rollback 操作,事务被回滚。
    public boolean rollbackOn(Throwable ex) {
       return (ex instanceof RuntimeException || ex instanceof Error);
    }
    

Spring 处理事务的时候,如果没有在 @Transactional 中配置 rollback 属性,那么只有捕获到 RuntimeException 或者 Error 的时候才会触发回滚操作

而我们案例抛出的异常是 Exception,又没有指定与之匹配的回滚规则,所以我们不能触发回滚。


解决方式如下

通过rollbackFor 手动去指定他要需要回滚匹配的异常是什么

@Transactional(rollbackFor = Exception.class)

三、修饰private 方法添加事务失效问题

接下来,我们来优化一下逻辑,让学生的创建和保存逻辑分离,于是我就对代码做了一些重构,把 Student 的实例创建和保存逻辑拆到两个方法中分别进行。然后,把事务的注解 @Transactional 加在了保存数据库的方法上。

@Service
public class StudentService {
    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private StudentService studentService;

    public void saveStudent(String realname) throws Exception {
        Student student = new Student();
        student.setRealname(realname);
        studentService.doSaveStudent(student);
    }

	//通过private来修饰
    @Transactional
    private void doSaveStudent(Student student) throws Exception {
        studentMapper.saveStudent(student);
        if (student.getRealname().equals("小明")) {
            throw new RuntimeException("该用户已存在");
        }
    }
}

执行的时候,继续传入参数“小明”,看看执行结果是什么样子?

异常正常抛出,事务却没有回滚。

明明是在方法上加上了事务的注解啊,为什么没有生效呢?


通过 debug,我们一步步寻找到了问题的根源,得到了以下调用栈。

在这里插入图片描述

前一段是 Spring 创建 Bean 的过程。

当 Bean 初始化之后,开始尝试代理操作,这个过程是从 AbstractAutoProxyCreator 里的 postProcessAfterInitialization 方法开始处理的:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
   if (bean != null) {
      Object cacheKey = getCacheKey(bean.getClass(), beanName);
      if (this.earlyProxyReferences.remove(cacheKey) != bean) {
         return wrapIfNecessary(bean, beanName, cacheKey);
      }
   }
   return bean;
}

到了 AopUtils 的 canApply 方法。

这个方法就是针对切面定义里的条件,确定这个方法是否可以被应用创建成代理。其中有一段 methodMatcher.matches(method, targetClass) 是用来判断这个方法是否符合这样的条件:

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
   //省略非关键代码
   for (Class<?> clazz : classes) {
      Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
      for (Method method : methods) {
         if (introductionAwareMethodMatcher != null ?
               introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
               methodMatcher.matches(method, targetClass)) {
            return true;
         }
      }
   }
   return false;
}

从 matches() 调用到了 AbstractFallbackTransactionAttributeSource 的 getTransactionAttribute

public boolean matches(Method method, Class<?> targetClass) {
   //省略非关键代码
   TransactionAttributeSource tas = getTransactionAttributeSource();
   return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
}

其中,getTransactionAttribute 这个方法是用来获取注解中的事务属性,根据属性确定事务采用什么样的策略。

public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
      //省略非关键代码
      TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
      //省略非关键代码
   }
}

接着调用到 computeTransactionAttribute 这个方法,其主要功能是根据方法和类的类型确定是否返回事务属性,执行代码如下:

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
   //省略非关键代码
   if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
   }
   //省略非关键代码
}

这里有这样一个判断 allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers()) ,当这个判断结果为 true 的时候返回 null,也就意味着这个方法不会被代理,从而导致事务的注解不会生效。

那此处的判断值到底是不是 true 呢?我们可以分别看一下。

  • 条件 1:allowPublicMethodsOnly()allowPublicMethodsOnly 返回了 AnnotationTransactionAttributeSource 的 publicMethodsOnly 属性。

    protected boolean allowPublicMethodsOnly() {
       return this.publicMethodsOnly;
    }
    

    而这个 publicMethodsOnly 属性是通过 AnnotationTransactionAttributeSource 的构造方法初始化的,默认为 true。

    public AnnotationTransactionAttributeSource() {
       this(true);
    }
    
  • 条件 2:Modifier.isPublic()

    这个方法根据传入的 method.getModifiers() 获取方法的修饰符。该修饰符是 java.lang.reflect.Modifier 的静态属性,对应的几类修饰符分别是:PUBLIC: 1,PRIVATE: 2,PROTECTED: 4
    这里面做了一个位运算,只有当传入的方法修饰符是 public 类型的时候,才返回 true。

    public static boolean isPublic(int mod) {
        return (mod & PUBLIC) != 0;
    }
    

综合上述两个条件,你会发现,只有当注解为事务的方法被声明为 public 的时候,才会被 Spring 处理。


那问题的解决方式就不用说明了,就是直接将对应方法的修饰改为public

@Service
public class StudentService {
    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private StudentService studentService;

    public void saveStudent(String realname) throws Exception {
        Student student = new Student();
        student.setRealname(realname);
        studentService.doSaveStudent(student);
    }

    @Transactional
    public void doSaveStudent(Student student) throws Exception {
        studentMapper.saveStudent(student);
        if (student.getRealname().equals("小明")) {
            throw new RuntimeException("该学生已存在");
        }
    }
}

四、总结

  • Spring 支持声明式事务机制,它通过在方法上加上 @Transactional,表明该方法需要事务支持。于是,在加载的时候,根据 @Transactional 中的属性,决定对该事务采取什么样的策略;
  • @Transactional 对 private 方法不生效,所以我们应该把需要支持事务的方法声明为 public 类型;
  • Spring 处理事务的时候,默认只对 RuntimeException 和 Error 回滚,不会对 Exception 回滚,如果有特殊需要,需要额外声明,例如指明 Transactional 的属性 rollbackFor 为 Exception.class。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿昌喜欢吃黄桃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值