如何设计JAVA自定义异常

 

一、受检异常还是非受检异常?

       在绝大多数的情况下,系统报告了错误,我们都很难处理,最简单(有时甚至是唯一的办法)就是直接把错误报告给用户,这时我推荐使用非受检异常(即直接或间接继承RuntimeException)。比如:在下单时,发现没有库存了,这个错误是由于不满足业务规则。

PlaceOrderService.java

if (noInventory(productCode, quantity)) {
    throw new InsufficientInventoryException(productCode, quantity);
}

 InsufficientInventoryException.java

public class InsufficientInventoryException extends RuntimeException {
    public InsufficientInventoryException(String productCode, int quantity) {
        super("库存不足" + quantity + ",产品代码=" + productCode);
    }
}

       这样PlaceOrderService.java的客户端(可能是个MVC Controller),只需要简单地捕获了所有的Exception并报告用户即可了。

       另外,如果异常是由系统比较低层级的组件抛出的,如果选择受检异常,那么该组件的客户端及该客户端的客户端(可能会有很多嵌套)都必须在相关的方法中声明会抛出该异常,造成添加了一个底层组件,要改动大量的高层组件,使得添加新的异常非常麻烦,所以有时候干脆选择将一些系统底层(可能是框架)抛出的受检异常包裹成非受检异常,比如Spring framework就将受检异常SQLException包裹在自定义的非受检异常DataAccessException中。

 

二、异常中要包含哪些信息

       之前看过一位高人的文章,觉得总结的很到位,一定要包括描述和上下文。你是否遇到这样的情况:有个任务要分析日志,检查系统运行情况,结果你在日志看到一句:

Error!Cannot cancel order!”然后就没了

这个时候,真心郁闷啊,既不知道是哪张订单,也不知道为什么不能取消订单,只能查看代码:

throw new CannotCancelOrderException();

 如果当初是这样就好了:

throw CannotCancelOrderException.hasBeenLocked(orderId);
public CannotCancelOrderException(String orderId) {
    super("order["+ orderId + "] has been locked");
}

 

如果你的异常是由其他异常导致的,可别把cause落下了:

} catch (SupplierAccessException e) {
    throw new CannotCancelOrderException(orderId, e);
}

 

public CannotCancelOrderException(String orderId, Throwable t) {
      super("Cannot cancel order[" + orderId + "], due to " + t.getMessage(), t);
}

 

三、怎么让异常帮助报告错误

       当发生错误时,怎么告知用户是什么原因呢?比如在一个MVC Controller中,调用了下单服务:

String orderId = placeOrderService.placeOrder(productCode, quantity, price); 

由于使用了Spring framework来提供声明式事务,所以如果下单时如果发生错误,placeOrder()会抛出异常。为了不让用户看到500报错及满屏的错误堆栈,我们使用try/catch块捕获错误:

try {
    orderId = placeOrderService.placeOrder(productCode, quantity, price); 
} catch (Exception e) {
    model.addAttribute("statusCode", UNKNOWN);
}

但是如果捕获Exception的话,我们就不知道是什么原因导致的错误了,但如果去捕获每个异常的话catch篇幅会很大,而且如果要新增一个异常,就要回来修改catch,维护起来很麻烦:

try {
    orderId = placeOrderService.placeOrder(productCode, quantity, price); 
} catch (InsufficientInventoryException e) {
    model.addAttribute("statusCode", INSUFFICIENT_INVENTORY);
} catch (ExpriedPriceException e) {
    model.addAttribute("statusCode", EXPIRED_PRICE);
} catch (NoSuchProductException e) {
    model.addAttribute("statusCode", NO_SUCH_PRODUCT);
} catch (Exception e) {
    model.addAttribute("statusCode", UNKNOWN);
}

 由于这些异常都是自定义的,我们可以将其设计成一个体系(Hierachy):

try {
    orderId = placeOrderService.placeOrder(productCode, quantity, price); 
} catch (UncheckedApplicationException e) {
    model.addAttribute("statusCode", e.getStatusCode);
} catch (Exception e) {
    model.addAttribute("statusCode", UNKNOWN);
}

 要新增异常的话,只需要让其继承UncheckedApplicationException并实现getStatusCode()即可。同样的,如果除了要告知statusCode,还需要返回本地化的提示信息,还可以加上getI18nCode()、getArgs()这样的方法:

try {
    orderId = placeOrderService.placeOrder(productCode, quantity, price); 
} catch (UncheckedApplicationException e) {
    model.addAttribute("statusCode", e.getStatusCode);
    model.addAttribute("message", messageSource.getMessage(e, locale));
} catch (Exception e) {
    model.addAttribute("statusCode", UNKNOWN);
    model.addAttribute("message", messageSource.getUnknownError(locale));
}

messageSource:

public String getMessage(UncheckedApplicationException e, Locale locale) {
    return getMessage(e.getI18nCode(), e.getArgs(), locale);
}

 

四、tell,don't ask

    合理的异常构造函数签名可以帮助简化message的拼装,比如在第二部分的

 

public CannotCancelOrderException(String orderId, Throwable cause) {
      super("Cannot cancel order[" + orderId + "], due to " + t.getMessage(), cause);
}
就是个不错的例子,它的客户端只需要传入orderId及cause即可,如果orderId能使用领域对象就更好了(可以防止误把其他String对象当成orderId传入),比如:

 

 

public CannotCancelOrderException(OrderIdentifier orderId, Throwable cause) {
而这个签名如果是这样的话,要抛出它的人就辛苦了些:

 

 

public CannotCancelOrderException(String message, Throwable cause) {
    super(message, cause);
}
Client:

 

 

throw new CannotCancelOrderException("Cannot cancel order[" + orderId + "] due to " + e.getMessage(), e);
如果有多个地方会抛出这个异常,那么除了拼装message麻烦外,不免还会有代码重复。此外对于单元测试来说,也不得不添加琐碎的代码,比如下面一段使用JMock做单元测试的代码:

 

 

final String message = "[" + username + "] login [failure] for ["
				+ cause + "]";
context.checking(new Expectations() {
    {
        oneOf(loginService).login(username, password);
        will(throwException(new CannotLoginException(message, cause)));
    }
});
 
好了,就介绍到这里,如果你有更好的想法,请务必更贴哦

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值