Spring
为开发人员提供了声明式事务的使用方式,即在方法上标记@Transactional
注解来开启事务。大家在日常的开发中很多业务代码对数据进行操作的时候一定是希望有事务控制的。
比如电商卖东西业务,代码的逻辑是商家先生成一个订单(订单信息插入到数据库中),再将钱收入到自己的账户中(数据库中的money增加)。整个过程是要作为一个完整的事务来对待的,如果后面这个操作失败了,那么前者也一定不能插入成功,这时候就会用到事务的回滚。
经典的开发经验:
很多时候,在实际业务开发中,总希望接口能返回一个固定的类实例——这叫做统一返回结果。例如以Result
类作为统一返回结果。
于是为了方便就直接在Service
的方法中return一个Result
类对象,为了避免受异常的影响而无法返回该结果集,就会使用try-catch
语句,当业务代码出现错误而抛出异常时会捕获此异常,将异常信息写入Result的相关字段中,返回给调用者。
@Controller
@RestController
@Api( tags = "测试事务是否生效")
@RequestMapping("/test/transactionalTest")
@Slf4j
public class GoodsStockController {
@Autowired
private GoodsStockService goodsStockService;
/**
* create by: entropy
* description: 事务无法回滚的方法
* create time: 2022/1/25 21:38
*/
@GetMapping("/exception/first")
@ApiOperation(value = "关于异常的第一个方法,不能够回滚", notes = "因为异常未能被事务发现,所以没有回滚")
@ResponseBody
public Result firstFunctionAboutException(){
try{
return goodsStockService.firstFunctionAboutException();
}catch (Exception e){
return Result.server_error().Message("操作失败:"+e.getMessage());
}
}
}
其中的service中的方法
@Autowired
private GoodsStockMapper goodsStockMapper;
@Override
@Transactional
public Result firstFunctionAboutException() {
try{
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new RuntimeException();
return Result.ok();
}catch (Exception e){
log.info("减库存失败!" + e.getMessage());
return Result.server_error().Message("减库存失败!" + e.getMessage());
}
}
最终测试结果事务没有回滚。我们都知道当程序执行时出现错误而抛出异常时,事务才会回滚,这里虽然出现了异常但却被方法本身消化了(catch掉了),异常没有被事务所发现,所以这样子是不会出现回滚的。因此正确的姿势是去掉service
中的try-catch
语句即可:
@Override
@Transactional
public void secondFunctionAboutException() {
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new RuntimeException();
}
通过这种处理方式可以实现事务的回滚。另外异常怎么办呢?很简单,将异常放在Controller层去处理就行。
总结:当标记了@Transactional
注解的方法中出现异常时,如果该异常未传播到该方法外,则事务不会回滚;反之,只有异常传播到该方法之外,事务才会回滚。
@Transactional
所标注的方法,事务依然没有回滚@Override
@Transactional
public void thirdFunctionAboutException() throws Exception {
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new Exception();
}
眼尖的同学一眼就看到了问题:
Spring
的@Transactional
注解就是默认只有当抛出RuntimeException
运行时异常时,才会回滚。
Spring
通常采用RuntimeException
表示不可恢复的错误条件。也就是说对于其他异常,Spring
觉得无所谓所以就不回滚。
那么对应的解法也有2种,请看:
解法一:手动catch捕获Exception,然后抛出runtimeException
@Override
@Transactional
public void thirdFunctionAboutException1(){
try{
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new Exception();
}catch (Exception e){
log.info("出现异常"+e.getMessage());
throw new RuntimeException("手动抛出RuntimeException");
}
}
解法二:修改注解默认的@Transactional
回滚的异常范围
@Override
@Transactional(rollbackFor = Exception.class)
public void thirdFunctionAboutException2() throws Exception {
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new Exception();
}
假设service业务类中有这样的2个方法:
@Override
public void privateFunctionCaller (){
privateCallee();
}
@Transactional
private void privateCallee(){
goodsStockMapper.updateStock();
throw new RuntimeException();
}
service
的privateFunctionCaller
方法从而间接调用标注了@Transactional
注解的方法privateCallee
。执行代码后,发现事务并没有回滚。这是什么原因呢?
这就要提到@service注解的原理:spring是通过动态代理的方式来实现AOP的。也即AOP容器中的bean实际上都是代理对象。@Transactional注解的支持也正是通过AOP来实现的。Spring
会对原对象中的方法进行封装(即检查到标有该注解的方法时,就会为它加上事务).。这个行为就叫做为目标方法进行增强。要想铜鼓增强的方式使得事务生效,那么方法必然不能是private的,实际上如果在ieda编辑器里在私有的方法上使用了@Transactional注解的话,编译器是会报错的。
实际上修改为public后事务还是没有回滚。这是为什么呢?
@Override
public void publicFunctionCaller (){
publicCallee();
}
@Override
@Transactional
public void publicCallee(){
goodsStockMapper.updateStock();
throw new RuntimeException();
}
被注入的Service
对象是代理对象,当调用publicCallee
方法时,上面是没有@Transactional
注解的。故只是简单执行service.function()
,即在代理对象的方法publicFunctionCaller
中,先由Service的原对象来调用自己的publicFunctionCaller
方法,再由其调用自己的publicCallee
方法。不会走代理对象增强过(带有事务)的publicCallee
方法,事务也就不会回滚。
解决办法:显式的注入自己。缺点就是破坏了分层的结构,加大了代码耦合性
@Override
@Transactional
public void publicCallee(){
goodsStockMapper.updateStock();
throw new RuntimeException();
}
@Autowired
private GoodsStockService self;
@Override
public void aopSelfCaller (){
self.publicCallee();
}