Spring中通过@Transactional注解动态代理对目标方法的增强,可以很方便的回滚事务。但是,如果不熟悉使用@Transactional注解的话,却会有很多隐藏的坑不容易被发现,往往是在线上环境才出现问题,通过一番排查才找到问题所在,以下是本人实际工作中或是浏览其他相关博客模拟实现的场景,以此加深记忆和记录。
1.@Transactional注解标记的方法是private
2.@Transactional注解标记的方法不是Spring注入的bean调用
3.@Transactional注解没有显示声明rollbackFor属性
4.@Transactional注解标记的方法内,使用try...catch捕获异常
5.@Transactional注解使用默认的传播机制
打开@Transactional注解的内容
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";
    @AliasFor("value")
    String transactionManager() default "";
    Propagation propagation() default Propagation.REQUIRED;
    Isolation isolation() default Isolation.DEFAULT;
    int timeout() default -1;
    boolean readOnly() default false;
    Class<? extends Throwable>[] rollbackFor() default {};
    String[] rollbackForClassName() default {};
    Class<? extends Throwable>[] noRollbackFor() default {};
    String[] noRollbackForClassName() default {};
}

废话不多说,直接上案例!
以下的案例都是模拟新增用户的流程,为了简便,使用Spring Data JPA操作数据库。
User实体类
 @Entity
 public class User {
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Integer id;
     private String name;
    
    // 省略getter、setter
 }
UserDao类
@Repository
public interface UserDao extends JpaRepository<User,Integer> {
}
Controller类
@RestController
public class TransactionController {
    @Autowired
    private UserService userService;
    @GetMapping("test")
    public void test(){
        userService.createUser();
    }
}
1.@Transactional注解标记的方法是private
接下来看下Service实现类
@Service
public class UserService{
    @Autowired
    private UserDao userDao;
    /**
     * 创建用户
     */
    public void createUser() {
        insertUser();
    }
    @Transactional
    private void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new RuntimeException("错误");
    }
}
访问:http://localhost:8080/test后,可以发现控制台报错,证明有抛出异常,那么事务是否有回滚呢?查看一下数据库的User表,却发现有新增用户信息,就证明事务并没有回滚,事务回滚失效了!
这是为什么呢?这就需要知道@Transactional的原理,实际上就是Spring中的AOP,使用@Transactional注解,Spring就会通过动态代理的方式增强目标方法。所以private的方法是无法被代理,所以动态代理失效,无法回滚事务!
既然知道原因,那是不是将private方法改为public就行啦?
@Service
public class UserService{
    @Autowired
    private UserDao userDao;
    /**
     * 创建用户
     */
    public void createUser() {
        insertUser();
    }
    @Transactional
    public void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new RuntimeException("错误");
    }
}
再次访问http://localhost:8080/test,虽然控制台有输出报错信息,但还是没有回滚数据库的操作,这就纳闷了,不是使用@Transactional注解就可以了吗?
这就引申到下一个"坑"了
2.@Transactional注解标记的方法不是Spring注入的bean调用
有点拗口,其实简单理解为@Transactional注解标记的方法应该是Bean的调用,而不是方法内调用。例子中@Transactional注解标记的方法是由Bean内部方法的调用,所以将@Transactional注解放到例子中的createUser方法就可以了。
@Service
public class UserService{
    @Autowired
    private UserDao userDao;
    /**
     * 创建用户
     */
    @Transactional
    public void createUser() {
        insertUser();
    }
    public void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new RuntimeException("错误");
    }
}
访问http://localhost:8080/test,这次数据表就没有新增用户信息了,就证明事务回滚。
小结:使用@Transactional注解的方法,访问级别应该是public,而且应该是被Bean调用的方法
3.@Transactional注解没有显示声明rollbackFor属性
那我再对Service改一下,抛出的异常由原来的RuntimeException改为Exception
@Service
public class UserService{
    @Autowired
    private UserDao userDao;
    /**
     * 创建用户
     */
    @Transactional
    public void createUser() throws Exception {
        insertUser();
    }
    public void insertUser() throws Exception {
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new Exception("错误");
    }
}
访问http://localhost:8080/test,再次发现由新增用户信息。My God!这又是什么坑呀?
其实,这是由于不熟悉@Transactional注解的原因。
这是因为Spring框架的事务管理默认地只在发生不受控异常(RuntimeException和Error)时才进行事务回滚。也就是说,当事务方法抛出受控异常(Exception中除了RuntimeException及其子类以外的)时不会进行事务回滚。
而rollbackFor属性的默认值是 RuntimeException ,但是如果抛出的异常是 Exception 类型,@Transactional注解无法捕获异常,所以也就无法回滚事务。阿里巴巴规范建议使用@Transactional注解的时候显式地声明rollbackFor属性的值
// @Transactional注解 rollbackFor 属性默认值
@Transactional(rollbackFor = RuntimeException.class)
错误使用:
@Transactional
public void test(){}
正确使用:
@Transactional(rollbackFor = Exception.class)
public void test(){}
ps.强烈建议大家在Idea上安装阿里巴巴规范插件,插件扫描代码,发现有不规范的地方就回有提示,使咱们的代码更加规范、更加优雅!

将原本使用 @Transactional 改为 @Transactional(rollbackFor = Exception.class)后,重新启动访问http://localhost:8080/test后可以发现,用户信息没有新增,就证明事务回滚了!
小结:使用 @Transactional 注解的时候,为了避免隐藏的bug,一定要显式声明rollbackFor属性的值!
4.@Transactional注解标记的方法内,使用try...catch捕获异常
接下来,模拟另外一个坑,这也是一个十分常见的事务失效问题
改动使用 @Transactional 注解的方法,将原本throw异常改为try...catch捕获异常
    /**
     * 创建用户
     */
    @Transactional(rollbackFor = Exception.class)
    public void createUser(){
        try {
            insertUser();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
访问http://localhost:8080/test后可以发现,用户信息新增了,就证明事务并没有回滚!
这是因为异常信息在被@Transactional捕获之前被try...catch...捕获了,相对于try...catch..."吃"掉了异常,@Transactional就无法捕获异常,所以就无法回滚事务!
那我想通过使用try...catch...捕获异常并做出一些补偿机制,怎么办?其实也是可以的,加上一行:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    /**
     * 创建用户
     */
    @Transactional(rollbackFor = Exception.class)
    public void createUser(){
        try {
            insertUser();
        } catch (Exception e) {
            e.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            // 可以自定义出异常后的操作
        }
    }
小结:使用@Transactional注解的时候,要注意异常信息会不会被try...catch...捕获。
5.@Transactional注解使用默认的传播机制
@Transactional注解中,有个属性propagation,默认的传播级别为Propagation.REQUIRED
propagation属性的值有以下几种选择
- Propagation.REQUIRED(默认):如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务
 - Propagation.SUPPORTS:如果当前存在事务,则加入事务,没有则以非事务方式运行
 - Propagation.MANDATORY:当前存在事务,则加入事务,不存在事务则抛出异常
 - Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
 - Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
 - Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
 - Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED
 
但要根据实际的业务场景选择事务传播级别,不一定默认的传播级别适用!
假设现在的业务场景是,先创建用户信息,然后根据用户信息创建学生信息(Student表),但如果由于某些原因,创建学生信息失败,但不能影响用户信息的创建。所以创建用户信息和学生信息应该在不同的事务内,这样才不会相互影响,这样的话,使用@Transactional默认的传播级别就实现不了,但我们可以改变propagation属性值,改为Propagation.REQUIRES_NEW
Student实体类
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String name;
    private String classroom;
    
    // 省略getter、setter
}
StudentDao类
@Repository
public interface StudentDao extends JpaRepository<Student,Integer> {
}
StudentService实现类
@Service
public class StudentService {
    @Autowired
    private StudentDao studentDao;
    /**
     * 创建学生基本信息
     */
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void createStudentInfo() throws Exception {
        Student student = new Student();
        student.setName("MuggleLee");
        student.setClassroom("高一一班");
        studentDao.save(student);
        throw new Exception("错误");
    }
}
UserService实现类
@Service
public class UserService {
    @Autowired
    private UserDao userDao;
    @Autowired
    private StudentService studentService;
    /**
     * 创建用户
     */
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void createUser() {
        insertUser();
        try {
            studentService.createStudentInfo();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
    }
}
重启后访问http://localhost:8080/test,可以发现用户信息可以正常新增,但学生信息却没有新增,就证明学生新增信息被事务回滚,但不影响用户信息新增。
以上都是常见的事务失效的场景,希望能够诸位在开发的时候,多加注意!
如果觉得文章不错的话,麻烦点个赞哈,你的鼓励就是我的动力!对于文章有哪里不清楚或者有误的地方,欢迎在评论区留言~
参考资料:
极客时间——专栏:Java业务开发常见错误100例










