一个特殊的异常处理
文:袁光东
一、业务需求说明
前段时间接到一个关于援助卡的需求。这个需求比较特殊的是援助卡的卡号是由单证系统进行管理的。
可能用银行卡来说明比较清楚一些。
银行卡是由银行统一制作出来的,每一个银行卡号都是事先已经编制了的。银行有一个单证部门来管理这些印刷的银行卡。各个营业点,会向单证部门领取银行卡。此时单证系统的银行卡为领取状态。
当一个储户去银行开户时,银行职员会为储户选择一张银行卡。这张银行卡的卡号就成了储户的帐号。
计算机系统完成开户处理需要有三个步骤:
1.在单证系统中检查卡号是否存在,并且卡号为已领取(要是印刷错误呢,呵呵)。这个过程称为回销检查。
2.然后把该卡号作为帐号和开户人的相关信息都写进帐户信息表去。
3.同时还需要调用单证系统,把该卡号的状态改为使用状态。这个过程称为回销。
援助卡的需求跟银行开户处理一样。
二、代码实现
public OperateResultDTO issueAssistanceCard(AssistanceCardDTO assistanceCardDTO)
throws BusinessServiceException {
…..省略
OperateResultDTO operateResultDTO;
operateResultDTO = service.writeOffCheck(
assistanceCardDTO.getCardNo());// 检查卡号是否可回销
if (!operateResultDTO.isSuccess()) {
….省略部分代码
}
service.issueAssistanceCard(assistanceCardDTO);//新发卡
operateResultDTO = service.writeOff(assistanceCardDTO
.getCardNo());// 将新卡回销
} catch (BusinessException ex) {
throw new BusinessServiceException(ex);
}
}
注意:回销检查和回销操作并没有调用外部接口,而是调用财务部门提供的一个PACAKGE方法。当然,这个procedure我们是没有权限看的。但是基本逻辑是知道的。
三、发生的问题
看上面的代码很简单。但是其中有一个非常严得的问题。就是事务的原子性。
在第二步,把援助卡信息写进了援助信息表。然后调用单证系统的回销接口,把卡号进行回销。如果此时回销失败。Prodedure返回的结果是N. 在java方法中返回的结果operateResultDTO.isSuccess为false;
如果就这样操作的话,虽然回销失败了,但是援助卡信息还是被成功的写进了数据库。这就违背了事务的原子性原则。
在一个事务里的操作,要么全部成功,要么全部失败。
四、解决方法一
要想达到事务的原子性,当回销失败时,就必须回滚它之前所有的操作。别要跟我说调用delete方法,把插入的数据删除掉哦。
最简单的办法就是回销操作失败时,抛出异常。
operateResultDTO = service.writeOff(regionCode, assistanceCardDTO
.getCardNo());// 将新卡回销
if(! operateResultDTO.isSuccess()){
throw new BusinessServiceException(“回销卡号失败,卡号:”+cardNo);
}
这样处理了之后,就解决了事务的原子性特性。
但是,当抛出了这个异常之后,程序也就当掉了。不可恢复了。但这张卡失败大不了给用户重新选一个卡号就完了嘛。但是还得重新录入一大堆客户资料信息吧。
五、解决方法二
为了让程序更加健壮,当回销操作失败后,应该再重新回到录入页面,让操作人员为客户重新选择一张卡。并且以前录入的信息也自动带出。并抛出一个异常是不够的。
可能你会说:那在controller 捕获BusinessServiceException异常不就完了嘛。如果有这个异常就返回到之前的页面。
在controller代码写上
try{
调用action
}catch(BusinessServiceException ex){
ModelAndView mav = new ModelAndView(“原来的录入页面”);
mav.addObject(“录入信息DTO”);//我简单的写一下伪代码
return mav;
}
这算是说到了关键点上了。但是这解决不了问题。因为你根本捕获不了BusinessServiceException.
框架对业务异常进行了处理。把业务异常转为了web异常。
所以应该写成这样。
catch (PafaWebException ex) {
if (ex.getCause() instanceof BusinessServiceException)
{
errorMap.put("reissueFail", "回销卡失败!");
mav.addObject("error", errorMap);
mav.addObject("assistanceCardDTO", assistanceCardDTO);
return mav;
} else {
throw ex;
}
似乎这样就已经解决了吧。事务的原子性得到了保证,程序的健壮性也得到了增强。
但是,如果某一天有业务方法的其它地方也抛出了一个BusinessServiceException时。而不是由回销失败起发的异常。你也让它重试,也提示他回销失败。那就摆了个大乌龙了。
解决方法三:
为了避免大乌龙的出现,必有抛出一个异常确认是由回销失败引起的。而且还需要把这个异常继承于BusinessServiceException.否则你就需要破坏原来接口的签名。
public static class WriteOffException extends BusinessServiceException {
public WriteOffException() {
super();
}
public WriteOffException(String msg) {
super(msg);
}
public WriteOffException(String msg, Throwable cause) {
super(msg, cause);
}
}
我这里是把它定义为一个内部的静态类。
然后业务方法不再是抛出BusinessServiceException.了。
if (operateResultDTO.isSuccess()) {
//回销成功
…..
} else {
throw new WriteOffException("回销卡号失败:"
+ assistanceCardDTO.getCardNo()
+ operateResultDTO.getMessage());
}
在controller里也只是稍加变化。
catch (PafaWebException ex) {
if (exceptionContainCause(ex, WriteOffException.class)) // 如果抛出的异常属于回销失败而抛出的
{
errorMap.put("reissueFail", "回销卡失败!");
mav.addObject("error", errorMap);
mav.addObject("assistanceCardDTO", assistanceCardDTO);
} else {
throw ex;
}
private boolean exceptionContainCause(Exception ex, Class exClass) {
if (null == exClass) {
return false;
}
if (null == ex) {
return false;
}
Throwable exSource = ex;
while (exSource != null) {
if (exClass.isInstance(exSource)) {
return true;
}
exSource = exSource.getCause();
}
return false;
}
需要注意的是:只有异常是由WriteOffException这种异常引发的时才处理。否则继续抛出。
另外要注意exceptionContainCause方法。该方法是看一个异常是属于某个类型的异常。从当前开始比较,如果不匹配就拿该异常类的父类去比较。
这样问题就获得了圆满的解决。即保证了事务的原子性,又加强了程序的健壮性。
总结:
在实现业务方法,进行多种操作或调用外部接口时,一定要考虑事务原子性属性。有些时候是要你手动来保证的。
并于异常的处理,有时不是简单的抛出异常草草了事,能够提供给用户重试机会的,就需要给用户重试的机会。提高程序的稳定性和健壮性。