文:袁光东
一、业务需求说明
前段时间接到一个关于援助卡的需求。这个需求比较特殊的是援助卡的卡号是由单证系统进行管理的。
可能用银行卡来说明比较清楚一些。
银行卡是由银行统一制作出来的,每一个银行卡号都是事先已经编制了的。银行有一个单证部门来管理这些印刷的银行卡。各个营业点,会向单证部门领取银行卡。此时单证系统的银行卡为领取状态。
当一个储户去银行开户时,银行职员会为储户选择一张银行卡。这张银行卡的卡号就成了储户的帐号。
计算机系统完成开户处理需要有三个步骤:
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方法。该方法是看一个异常是属于某个类型的异常。从当前开始比较,如果不匹配就拿该异常类的父类去比较。
这样问题就获得了圆满的解决。即保证了事务的原子性,又加强了程序的健壮性。
总结:
在实现业务方法,进行多种操作或调用外部接口时,一定要考虑事务原子性属性。有些时候是要你手动来保证的。
并于异常的处理,有时不是简单的抛出异常草草了事,能够提供给用户重试机会的,就需要给用户重试的机会。提高程序的稳定性和健壮性。