Java异常一种实践

理论

我们开发的每个系统都离不开各种异常,创建异常,抛出异常,捕获异常,每个团队每个项目的实践也不一样,对于异常处理的理解也不尽相同,以下我们提供一种我们的异常处理思路,供大家参考。以下所说跟特定团队,特定项目 综合考虑取舍而来,请自行甄别。
首先异常要解决什么问题,如果应用不出异常,讨论异常实践没有意义,如果应用出现异常,问题的关键就在于异常是否能提供足够的反馈,便于定位问题,并提供必要的上下文信息,最终开发能够快速解决问题。具体来说需要有What(什么东西出错),Where(哪出错了),Why(为什么出错),只要能提供这3W信息,个人认为就是有效的异常,至于是否优雅,是否高效,这些都是在正确有效的前提下来进一步表达的。我们再细化一下实践中的三个异常处理原理,具体明确,提早抛出,延迟捕获。
理论就说到这,以下是我们的工程实践,适用场景为,web应用系统,模块划分为web,service,rpc,manager,dao层。

关于异常的性能

我们来简单测试一下,异常到底消耗多少性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ExceptionTest {
public static void main(String[] args) {
ExceptionTest exceptionTest = new ExceptionTest();
int cnt = 1000*10000;
long start = System.currentTimeMillis();
for(int i=0; i<cnt; i++){
try{
exceptionTest.doTest();
}catch (Exception e){
//e.getStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println((end-start)/(cnt*1.0));
}
public void doTest(){
throw new RuntimeException("exception in doTest()");
}
}

我的笔记本(i5,8g,公司标配T450)测试结果是一次异常大概是在0.001毫秒,如果使用ex.getStackTrace(),性能大概会增大到6倍,大概是0.006毫秒,包含了try catch的时间。异常创建的主要性能在同步方法fillInStackTrace中,本测试方法只是简单测试,让大家有一个直观的感受,并发情况下的测试请自行试验。异常对性能的测试可以参考 Java异常及相关调用性能测试

关于性能,补充一下,程序不可能不抛异常,但是异常也不是常态,设计良好稳定运行的业务程序,异常还是少数。

我们的实践

定义错误码接口

1
2
3
4
5
6
7
8
public interface ErrorCode {
// 错误码编号
String getCode();
// 错误码描述
String getDescription();
String toString();

实现业务错误码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 业务错误码定义
*
* 0-9999 系统级别或通用异常
* 10000-20000 业务异常
*/
public enum BusinessErrorCode implements ErrorCode {
UNKNOWN_ERROR("0000", "未知异常"),
ILLEGAL_ARG_ERROR("0001", "请求参数错误."),
DIAGNOSE_ID_ERROR("10000", "问诊单号异常"),
;
private final String code;
private final String description;
BusinessErrorCode(String code, String description) {
this.code = code;
this.description = description;
}
@Override
public String getCode() {
return this.code;
}
@Override
public String getDescription() {
return this.description;
}
@Override
public String toString() {
return String.format("Code:[%s], Description:[%s]. ", this.code, this.description);
}
}

定义业务异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
*业务异常
*/
public class BusinessException extends RuntimeException {
private ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.toString());
this.errorCode = errorCode;
}
public BusinessException(ErrorCode errorCode, String errorMessage) {
super(errorCode.toString() + " - " + errorMessage);
this.errorCode = errorCode;
}
private BusinessException(ErrorCode errorCode, String errorMessage,
Throwable cause) {
super(errorCode.toString() + " - " + getMessage(errorMessage)
+ " - " + getMessage(cause), cause);
this.errorCode = errorCode;
}
public static BusinessException asBusinessException(ErrorCode errorCode) {
return new BusinessException(errorCode);
}
public static BusinessException asBusinessException(ErrorCode errorCode, String message) {
return new BusinessException(errorCode, message);
}
public static BusinessException asBusinessException(ErrorCode errorCode, String message, Throwable cause) {
if (cause instanceof BusinessException) {
return (BusinessException) cause;
}
return new BusinessException(errorCode, message, cause);
}
public static BusinessException asBusinessException(ErrorCode errorCode, Throwable cause) {
if (cause instanceof BusinessException) {
return (BusinessException) cause;
}
return new BusinessException(errorCode, null, cause);
}
public ErrorCode getErrorCode() {
return this.errorCode;
}
private static String getMessage(Object obj) {
if (obj == null) {
return "";
}
if (obj instanceof Throwable) {
return ((Throwable) obj).getMessage();
} else {
return obj.toString();
}
}
public static void main(String[] args) {
RuntimeException r = new RuntimeException("test001!!!");
BusinessException aa = BusinessException.asBusinessException(BusinessErrorCode.UNKNOWN_ERROR,r);
System.out.println(aa.getMessage());
}
}

使用异常

通过throw BusinessException.asBusinessException抛出异常

1
2
3
4
5
6
7
8
// 帐号冲突,取消订单
Long msgId = null;
try {
msgId = rxDiagnosisInfoManager.diagnoseConflictMsg(consultOrderId, mqMsg);
} catch (Exception e) {
wxLog.error("PartnerDiagnosisServiceImpl --> dealConflictMsg 入库失败 | consultOrderId={}, mqMsg={}", consultOrderId, FastjsonUtil.objectToJson(mqMsg), e);
throw BusinessException.asBusinessException(BusinessErrorCode.DIAG_CANCEL_ORDER_FAIL);
}

异常与返回错误码

1
2
3
4
5
6
7
8
public int doSth(){
if(..)
return 1;
else if(..)
return 2;
esle
return 3;
}
有些人喜欢用返回状态码来标识业务处理结果,C语言最喜欢这么干,至于做应用系统,调用某个业务方法弄一堆状态码是否合适,这个有待商榷,上层调用看到一大堆错误码大多数情况也是无能为力,其实简单粗暴点,如果要表达的是出错了,无法继续运行就抛异常,如果出错了还有补救措施,那就自由选择错误码或者特定的异常。提供jsf接口是需要详细定义错误码,但仅返回一个错误码也不太合适,至少得有个说明什么的。
结论就是,我们推荐使用异常来阻断业务继续。错误码的需求也可以在异常中表达。

异常使用规则

  1. 最外层一定要捕获异常,最外层是是action,controller,jsf服务提供者实现类。最外层如果不处理异常,会把错误信息传给最终用户。咱先不说业务中或者nginx中自定义错误页面的情况。
  2. dao,service层放心大胆抛异常,用于阻断程序继续执行。
  3. 不太推荐层层捕获异常,之后再嵌套包装后抛出来。注意,这里指的是系统中抛出的都是BusinessException这种自定义的运行时异常。业务上已经标转换成了BusinessException异常,没必要再做包装。
  4. 对于下层模块,异常是否要捕获不做强制要求,反正最外层一定会捕获的。如果我们要屏蔽业务实现的细节,可以捕获后再抛出带特定BusinessErrorCode的异常,这种情况我们做的是错误收敛,如对某些复杂的rpc调用的封装。处于中层的模块,捕获到BusinessException,如果不想做处理,直接上层抛。

结尾

套用老司机的一句话,不要以为自己见过的就是全世界,每一种方案都有其特定的场景,一切都是tradeoff。