一个特殊的异常处理

一个特殊的异常处理
文:袁光东

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

总结:
在实现业务方法,进行多种操作或调用外部接口时,一定要考虑事务原子性属性。有些时候是要你手动来保证的。
并于异常的处理,有时不是简单的抛出异常草草了事,能够提供给用户重试机会的,就需要给用户重试的机会。提高程序的稳定性和健壮性。







你可能感兴趣的:(框架,项目管理,脚本,SOA)