异常数据入库不被@Transactional注解捕获回退
1.业务背景
一个简单的业务操作,对入参分析,分别入主表master和副表follw表,因为是两个入库操作,所以我们用@Transactional来修饰方法,保证两个表数据的一致性。突然老板说,在校验入参的时候,要是有非法入参,我们也得记录主表数据(记录入参和错误原因),说是方便后期维护查问题。好,咱们就针对入参错误问题做了入库操作,后来发现日志中打印了错误后的入参sql,但是数据库中没有数据,突然想到@Transactional这个事务开着,导致入参错误的落库被回退了,真的是坑。。。
2.代码复现
代码主要逻辑有校验参数、解析入参入主表数据、解析入参入副表数据、最后返回主表id给调用方。显然为了保持主副标的数据一致性,我们使用@Transactional注解开始事务是合理的。数据库操作是用mybatis-plus,不熟悉的朋友也不用深究,理解为入库操作就行。
@Transactional(rollbackFor = Exception.class)
public String save(SaveDTO saveDto) {
//校验入参
validInfo(saveDto);
//任务入库
TaxMaster taxMaster = .......;
Assert.isTrue(iTaxMasterService.save(taxMaster), "failed insert master db");
int masterId = taxMaster.getId();
//任务分解 生成副表数据
List<TaxFollow> taxFollows = .......;
Assert.isTrue(iTaxFollowService.saveBatch(taxFollows), "failed insert follow db");
return String.valueOf(masterId);
}
然后在校验入参里面需要抛出错误原因,并且把记录错误入参及错误描述,代码如下:
private void validInfo(SaveDTO saveDto) throws IllegalArgumentException {
try {
//获取企业信息
AllCompanyInfoDTO allCompanyInfoDTO = .......;
Assert.notNull(allCompanyInfoDTO, "获取企业信息为空");
Assert.notNull(allCompanyInfoDTO.getCompanyInfoDTO(), "校验企业信息不通过");
Assert.notNull(allCompanyInfoDTO.getQyGsxxDTO(), "校验企业信息不通过");
} catch (Exception e) {
String errorMessage = (e instanceof IllegalArgumentException) ? ExceptionUtils.getMessage(e) : "校验信息异常";
TaxMaster taxMaster = .....;
taxMaster.setCollectStatus(FAIL_DEAL_STATUS);
taxMaster.setFailDesc(errorMessage);
iTaxMasterService.save(taxMaster);
throw new IllegalArgumentException(errorMessage);
}
}
使用断言的Assert来检测参数,不满足则抛出IllegalArgumentException异常,我们捕获异常后需要做入库操作,然后再把异常往外抛,终止程序;
3.代码分析
上述代码,笔者写完看了一遍没啥问题,入参错误后记录错误原因后抛出异常,主副表入库操作用事物囊括,看上去业务逻辑都实现了。
但当我们程序运行测试时候,发现入参错误的确是执行入库sql语句,但是数据库中没有相应的数据,自己检查代码才发现入参校验异常后,我们捕获了异常后进行入库,再抛出。再抛出到外层方法save的时候被@Transactional注解捕获,进行了回退操作。。。。
4.优化方案
先看@Transactional注解,我们一般都是使用@Transactional(rollbackFor = Exception.class),表示方法中发生Exception异常都会被捕获,进行回退,而IllegalArgumentException继承RuntimeException继承Exception,所以我们之前的错误入库代码会被回退。
继续看@Transactional注解,它提供了一个noRollbackFor的属性,顾名思义表示遇到这种异常不做回退操作,因此我们有一个应对的优化思路,就是当入参错误后,我们捕获异常在抛出一个自定义异常,那么@Transactional注解就可以针对这个自定义异常做忽略处理。下面是优化后的代码:
//外部方法save
@Transactional(rollbackFor = Exception.class, noRollbackFor = CustomException.class)
public String save(SaveDTO saveDto) {
........
}
//入参校验方法
private void validInfo(SaveDTO saveDto) throws CustomException{
try {
........
} catch (Exception e) {
........
throw new CustomException(errorMessage);
}
}
//自定义异常类CustomException
public class CustomException extends RuntimeException {
private static final long serialVersionUID = 1L;
private Integer code;
private String message;
public CustomException(String message) {
this.message = message;
}
public CustomException(String message, Integer code) {
this.message = message;
this.code = code;
}
public CustomException(String message, Throwable e) {
super(message, e);
this.message = message;
}
@Override
public String getMessage() {
return message;
}
public Integer getCode() {
return code;
}
}
5.小结
针对这类问题,只需要自定义异常类,内部逻辑捕获我们需要的异常,操作后(入库),再抛出自定义异常,然后外部方法过滤自定义异常的回滚操作,以上。