Q:受检还是非受检,这是一个问题?
A:这是一个好问题。不过强烈推荐将自定义的异常设计为非受检的,原因如下:
1.如果期望调用者能够适当地恢复/处理错误,对于这种情况就应该使用受检异常,但在实际情况中,这个原则被大量的误用,在绝大多数的情况下,系统报告了错误,我们都很难处理,最简单(有时甚至是唯一的办法)就是直接把错误报告给用户。
2.受检异常要求客户端一定要继续抛出或是处理异常,由于第一个原因,常常导致客户端选择继续抛出异常,如果这个异常是系统较低层的组件抛出的,你经常会看到如下场景:
Listing-1 nested throws
public class TopService {
private MiddleService mService ;
public void doBusiness () throws ACustomeCheckedException {
mService . doBusiness ();
}
}
public class MiddleService {
private Dao dao ;
public void doBusiness () throws ACustomeCheckedException {
dao . save ();
}
}
public class Dao {
public void doBusiness () throws ACustomeCheckedException {
//omitted persistence logic
}
}
由于底层的Dao声明抛出一个受检异常,导致所有直接/间接调用它的组件也不得不声明抛出该异常,情况更糟的是,如果此时Dao由于实现变化又需要抛出另一个受检异常,这时你不得不修改所有直接/间接调用它的组件,让它们也抛出这个新的异常。这造成添加了一个底层异常,要改动大量的高层组件,使得添加新的异常非常麻烦,所以有时候干脆选择将一些系统底层(可能是框架)抛出的受检异常包裹成非受检异常,比如Spring framework就将受检异常SQLException包裹在自定义的非受检异常DataAccessException中。
基于以上两个原因,我推荐绝大多数(如果不是所有)情况下,让你的自定义异常继承 RuntimeException ,使其成为非受检异常。
Q:异常中要包含哪些信息?
A:简单地说,一定要包括描述和上下文。你是否遇到这样的情况:有个任务要分析日志,检查系统运行情况,结果你在日志看到一句:
“ Error ! Cannot cancel order ! ”
然后就没了,这个时候,真心郁闷啊,既不知道是哪张订单,也不知道为什么不能取消订单,只能查看代码:
Listing-2 a mysterious error
public class Order {
public void cancel () {
if ( order . isLocked ()) {
throw CannotCancelOrderException . hasBeenLocked ( orderId );
}
if ( order . isCanceled ()) {
throw new CannotCancelOrderException ();
}
//omitted code
}
}
如果重构一下,修改为这样就好多了:
Listing-3 exception with context and description
public class Order {
public void cancel () {
if ( order . isLocked ()) {
throw CannotCancelOrderException . hasBeenLocked ( orderId );
}
if ( order . isCanceled ()) {
throw CannotCancelOrderException . hasBeenCanceled ( orderId );
}
//omitted code
}
}
public class CannotCancelOrderException extends RuntimeException {
public static CannotCancelOrderException hasBeenLocked ( String orderId ) {
return new CannotCancelOrderException ( "order[" + orderId + "] has been locked" );
}
public static CannotCancelOrderException hasBeenCanceled ( String orderId ) {
return new CannotCancelOrderException ( "order[" + orderId + "] has already been canceled" );
}
public CannotCancelOrderException ( String message ) {
super ( message );
}
}
如果你的异常是由其他异常导致的,可别把cause落下了:
Listing-4 wrapped exception
public class Order {
public void cancel () {
//omitted code
} catch ( SupplierAccessException e ) {
throw CannotCancelOrderException . wrapped ( orderId , e );
}
}
}
public class CannotCancelOrderException extends RuntimeException {
//omitted factory methods and constructors
public static CannotCancelOrderException wrapped ( String orderId , Throwable t ) {
return new CannotCancelOrderException ( "Cannot cancel order[" + orderId + "] due to " + t . getMessage (), t );
}
public CannotCancelOrderException ( String message , Throwable t ) {
super ( message , t );
}
}
Q:如果没人能够处理异常,就让异常直接抛出吗?或是总要有个组件来捕获它?
A: 比如在一个MVC Controller中,调用了下单服务:
Listing-5
String orderId = placeOrderService . placeOrder ( productCode , quantity , price );
由于使用了Spring framework来提供声明式事务,所以如果下单时如果发生错误,我们会让placeOrder()会抛出异常从而触发回滚。为了不让用户看到500报错及满屏的错误堆栈,一般会使用try/catch块捕获错误:
Listing-6
try {
orderId = placeOrderService . placeOrder ( productCode , quantity , price );
} catch ( Exception e ) {
model . addAttribute ( "error" , e . getMessage ());
//omitted logging code if placeOrderService doesn't log on it own
}
由于大多数情况下,我们只是要报告错误,一般推荐在最靠近的用户的“层”中捕获异常并根据UI接口填充错误报告(对于View可能是将错误报告放入HttpServletRequest对象待渲染,对于远程访问接口可能是翻译并返回错误码)。
Q:我们的WebService接口需要以错误码的方式报告错误,这时怎样利用异常呢?
A: 如果要返回错误码而不是直接抛出异常,那么总会有一个组件会捕获所有的异常并进行翻译。最简单的解决方案是硬编码:
Listing-7 hard coded status code translation
try {
orderId = placeOrderService . placeOrder ( productCode , quantity , price );
} catch ( InsufficientInventoryException e ) {
statusCode = INSUFFICIENT_INVENTORY ;
} catch ( ExpriedPriceException e ) {
statusCode = EXPIRED_PRICE ;
} catch ( NoSuchProductException e ) {
statusCode = NO_SUCH_PRODUCT ;
} catch ( Exception e ) {
statusCode = UNKNOWN ;
}
但是如果异常很多,这里的翻译篇幅就会比较大,而且如果要新增一个异常,就要回来修改catch,维护起来很麻烦。由于这些异常都是自定义的,我们可以将其设计成一个体系(Hierachy):
Listing-8 exception hierachy
public class PlaceOrderServiceFacadeImpl implements PlaceOrderServiceFacade {
public PlaceOrderRs handle ( PlaceOrderRq rq ) {
//omitted code
try {
orderId = placeOrderService . placeOrder ( productCode , quantity , price );
statusCode = SUCCESS ;
} catch ( UncheckedApplicationException e ) {
statusCode = e . getStatusCode ();
} catch ( Exception e ) {
statusCode = UNKNOWN ;
}
return new PlaceOrderRs ( statusCode , order );
}
}
public abstract class UncheckedApplicationException {
//omitted factory methods and constructors
public abstract String getStatusCode ();
}
要新增异常的话,只需要让其继承UncheckedApplicationException并实现getStatusCode()即可。同样的,如果除了要告知statusCode,如果还需要返回本地化的提示信息,还可以加上getI18nCode()、getI18nArgs()这样的方法:
Listing-9 i18 supported exception hierachy
public class PlaceOrderServiceFacadeImpl implements PlaceOrderServiceFacade {
public PlaceOrderRs handle ( PlaceOrderRq rq ) {
//omitted code
try {
orderId = placeOrderService . placeOrder ( productCode , quantity , price );
statusCode = SUCCESS ;
message = messageSource . getSuccess ( locale ));
} catch ( UncheckedApplicationException e ) {
statusCode = e . getStatusCode ();
message = messageSource . getMessage ( e , locale ));
} catch ( Exception e ) {
statusCode = UNKNOWN ;
message = messageSource . getUnknownError ( e , locale ));
}
return new PlaceOrderRs ( statusCode , order , message );
}
}
public abstract class UncheckedApplicationException {
//omitted factory methods and constructors
public abstract String getStatusCode ();
public abstract String getI18nCode ();
public abstract String [] getI18nArgs ();
}
public class DelegateToSpringMessageSourceFacadeImpl implments MessageSourceFacade {
public String getMessage ( UncheckedApplicationException e , Locale locale ) {
return delegate . getMessage ( e . getI18nCode (), e . getI18nArgs (), locale ); //delegate to Spring MessageSource
}
}
另外,因为往往一个WebService的实现中有多个方法,如果使用之前的方案,那么每个方法都要编写一次try/catch块,但是内容是一样的,对于这类情况,我推荐使用AOP的方式来捕获异常:
Listing-10 a web service impl example
public class PlaceOrderServiceFacadeImpl implements PlaceOrderServiceFacade {
public QuoteRs handle ( QuoteRq rq ) {
//omitted code
try {
//omitted quoting code
statusCode = SUCCESS ;
message = messageSource . getSuccess ( locale ));
} catch ( UncheckedApplicationException e ) {
statusCode = e . getStatusCode ();
message = messageSource . getMessage ( e , locale ));
} catch ( Exception e ) {
statusCode = UNKNOWN ;
message = messageSource . getUnknownError ( e , locale ));
}
return new QuoteRs ( statusCode , quotes , message );
}
public PlaceOrderRs handle ( PlaceOrderRq rq ) {
//omitted code
try {
orderId = placeOrderService . placeOrder ( productCode , quantity , price );
statusCode = SUCCESS ;
message = messageSource . getSuccess ( locale ));
} catch ( UncheckedApplicationException e ) {
statusCode = e . getStatusCode ();
message = messageSource . getMessage ( e , locale ));
} catch ( Exception e ) {
statusCode = UNKNOWN ;
message = messageSource . getUnknownError ( e , locale ));
}
return new PlaceOrderRs ( statusCode , order , message );
}
}
Listing-10 an around advice catching exception
public class LogAndReturnHandler implements MethodInterceptor {
@Setter
private LoggingSupport loggingSupport ;
@Override
public Object invoke ( MethodInvocation invocation ) throws Throwable {
//omitted logging code
try {
//omitted logging code
Object returning = invocation . proceed ();
//omitted logging code
return returning ;
} catch ( UncheckedApplicationException e ) {
//omitted logging code
return populateRs ( invocation , e . getStatusCode (), e . getMessage ());
} catch ( Throwable t ) {
//omitted logging code
return populateRs ( invocation , StatusCode . UNKNOWN_ERROR ,
t . getMessage ());
}
}
private Class <?> returnTypeOf ( MethodInvocation invocation ) {
return invocation . getMethod (). getReturnType ();
}
private GenericRs aGenericRsWith ( String statusCode , String message ) {
return new GenericRs ( statusCode , message );
}
private Object populateRs ( MethodInvocation invocation , String statusCode ,
String message ) {
final GenericRs aGenericRs = aGenericRsWith ( statusCode , message );
Object object = mapper ( invocation , aGenericRs );
return object ;
}
private Object mapper ( MethodInvocation invocation ,
final GenericRs aGenericRs ) {
ModelMapper mapper = new ModelMapper ();
Object object = mapper . map ( aGenericRs , returnTypeOf ( invocation ));
return object ;
}
}
public class PlaceOrderServiceFacadeImpl implements PlaceOrderServiceFacade {
public QuoteRs handle ( QuoteRq rq ) {
//omitted quoting code
return new QuoteRs ( SUCCESS , quotes , successMessageFor ( locale ));
}
public PlaceOrderRs handle ( PlaceOrderRq rq ) {
//omitted code
orderId = placeOrderService . placeOrder ( productCode , quantity , price );
return new PlaceOrderRs ( SUCCESS , order , successMessageFor ( locale ));
}
private String successMessageFor ( Locale locale ) {
return messageSource . getSuccess ( locale ));
}
}
spring xml
< bean id = "irs.MemberBookingServiceFacadeImpl" class =" org . springframework . aop . framework . ProxyFactoryBean ">
<property name=" target " ref=" irs . MemberBookingServiceFacadeImplTarget " />
<property name=" interceptorNames " >
< list >
< value > irs . LogAndReturnHandler </ value >
</ list >
</ property >
</ bean >
这里的要点在于虽然每个方法都会catch异常,但是每个方法返回类型是不一致的(比如QuoteRs/PlaceOrderRs),需要找到一种方法能根据返回类型自动实例化并填充statusCode和message的方法(一般发生错误时,没有业务数据需要返回),我这里使用的是一个开源的library:ModelMapper,只要所有的Rs(比如QuoteRs/PlaceOrderRs)都和GenericRs一样有statusCode、message两个属性,ModelMapper会自动完成映射Rs的任务,非常方便(可以让所有的Rs都继承GenericRs降低属性名称不一致的风险)。
Q:自定义异常的粒度到什么程度比较好?
A: 这主要看应用程序准备怎么使用这个自定义的异常体系了。理论上来讲,即使是为了翻译statusCode,整个异常体系也只需要一个自定义异常就可以满足使用:
Listing-10 GenericApplicationException
public class GenericApplicationException extends RuntimeException {
@Getter
private String statusCode ;
@Getter
private String i18nCode ;
@Getter
private String [] i18nArgs ;
public GenericApplicationException ( String statusCode , String message , String i18nCode , String [] i18nArgs , Throwable cause ) {
super ( message , cause );
this . statusCode = statusCode ;
this . i18nCode = i18nCode ;
this . i18nArgs = i18nArgs ;
}
}
这个异常就非常通用,但是不易用,因为抛出它的组件需要自己负责输入statusCode, message, i18nCode, i18nArgs, cause。这样来看的话,你可以根据错误的来源设计一些特定的异常以简化异常的抛出代码,比如:
Listing-10 CannotCancelOrderException
public class CannotCancelOrderException extends UncheckedApplicationException {
private String orderId ;
private String i18nCode ;
public static CannotCancelOrderException hasBeenLocked ( String orderId ) {
return new CannotCancelOrderException ( "order[" + orderId + "] has been locked" , orderId , "i18n.order.orderHasBeenLocked" );
}
public static CannotCancelOrderException hasBeenCanceled ( String orderId ) {
return new CannotCancelOrderException ( "order[" + orderId + "] has already been canceled" , orderId , "i18n.order.orderHasBeenCanceled" );;
}
public CannotCancelOrderException ( String message , String orderId , String i18nCode ) {
super ( message );
this . orderId = orderId ;
this . i18nCode = i18nCode ;
}
@Override
public String getStatusCode () {
return CANNOT_CANCEL_ORDER ;
}
@Override
public String getI18nCode () {
return i18nCode ;
}
@Override
public String [] getI18nArgs () {
return new String [] { orderId };
}
}
i18n - zh - CN . properties
i18n . order . orderHasBeenCanceled = 订单 { 0 } 已被取消
i18n . order . orderHasBeenLocked = 订单 { 0 } 已被锁定
CannotCancelOrderException将statusCode、i18n、message的拼装隐藏了起来,这样抛出CannotCancelOrderException的组件就轻松多了,只需要输入orderId即可。如果编写单元测试,这种情况就更明显了,如果设计得当,抛出异常的组件在测试中也不用关心异常的信息细节,异常的信息细节可以在异常的单元测试中验证。
Listing-11 unit test for exception
public class CannotChangeOrderExceptionUnitTests {
private CannotChangeOrderException target ;
@Test
public void tellsOrderIsCanceled () throws Exception {
final String orderId = "1" ;
target = CannotChangeOrderException . orderIsCanceled ( orderId );
Assert . assertEquals ( "订单[1]已被取消" , target . getMessage ());
Assert . assertEquals ( StatusCode . CANNOT_CHANGE_ORDER ,
target . getStatusCode ());
Assert . assertEquals (
"com.springtour.irs.application.exception.CannotChangeOrderException.orderIsCanceled.message" ,
target . getI18nCode ());
}
@Test
public void tellsOrderIsHolding () throws Exception {
final String orderId = "1" ;
target = CannotChangeOrderException . orderIsHolding ( orderId );
Assert . assertEquals ( "订单[1]预占中" , target . getMessage ());
Assert . assertEquals ( StatusCode . CANNOT_CHANGE_ORDER ,
target . getStatusCode ());
Assert . assertEquals (
"com.springtour.irs.application.exception.CannotChangeOrderException.orderIsHolding.message" ,
target . getI18nCode ());
}
}