怎么写代码才舒服--合理使用异常

今天讲点干货,来点思想上的碰撞,代码风格的讨论,我们来探讨一下怎么合理使用异常,来使代码流程简洁易懂,使写代码写得舒服。

异常类型

先说一下java的异常类型有:

image-20181219202750294

检查性异常(checked exceptions) 是必须在在方法的throws子句中声明的异常。它们扩展了异常,旨在成为一种“在你面前”的异常类型。JAVA希望你能够处理它们,因为它们以某种方式依赖于程序之外的外部因素。检查的异常表示在正常系统操作期间可能发生的预期问题。 当你尝试通过网络或文件系统使用外部系统时,通常会发生这些异常。 大多数情况下,对检查性异常的正确响应应该是稍后重试,或者提示用户修改其输入。
非检查性异常(unchecked Exceptions) 是不需要在throws子句中声明的异常。 由于程序错误,JVM并不会强制你处理它们,因为它们大多数是在运行时生成的。 它们扩展了RuntimeException。 最常见的例子是NullPointerException [相当可怕..是不是?]。 未经检查的异常可能不应该重试,正确的操作通常应该是什么都不做,并让它从你的方法和执行堆栈中出来。 在高层次的执行中,应该记录这种类型的异常。
错误(errors) 是严重的运行时系统问题,几乎肯定无法恢复。 例如OutOfMemoryErrorLinkageErrorStackOverflowError, 它们通常会让程序崩溃或程序的一部分。 只有良好的日志练习才能帮助你确定错误的确切原因。

补充完以上的概念后,我们再来探讨一下这三种异常怎么用,什么时候用,该用哪种。Error是系统异常就不说了,我们直接讲Exception和RuntimeException。

怎么用

直接通过场景对比来剖析异常什么时候用比较好,该用哪种类型异常。

场景分析

用户在系统中输入证件号查询用户信息,如果成功返回{"code":200, "result":{用户信息对象}},验证不通过则返回{"code":-100, "error":"系统验证错误"},那么这里的验证不通过,怎么处理才比较好。

方案一

return的方式,定义通用结果对象CommonResult:

1
2
3
4
5
6
7
8
9
10
public class CommonResult {

private int code;

private UseInfo result;

private String error;

//===== 忽略getset ====
}

如果只是单层方法里面的验证逻辑,可能代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public CommonResult handle(String phone) {

// 验证逻辑1

// 验证逻辑2
if ("123".equals(phone)) {
return new CommonResult(-100, null, "验证逻辑2不通过");
}

// 验证逻辑3

//用户信息填充
UseInfo userinfo = new UseInfo();

return new CommonResult(200, userinfo, null);
}

但是如果是两层嵌套方法,那么我就需要将嵌在里面的方法也返回这种通用的CommonResult对象,不然只通过return的方法无法处理里面方法的校验不通过情况,最后的结果是,所有方法都必须返回通用结果对象。

造成这种现象的本质原因是,我们将正确的结果返回和错误的结果返回混在一起了,就会造成极其混乱的结果返回逻辑。我觉得正确的、舒服的代码结构方式,应该是我只需要舒服、专注地处理正确的业务流程就好,不正确的流程都throw异常出去。
(曾经听过一些老一辈的程序员觉得抛异常会影响性能,所以会采用上面那种处理方式,我觉得完全是多虑和没有必要的,先不讨论影响性能多少,以现在的机器配置,这点消耗是完全可以忽略的)

以上,我觉得就该用异常来处理了,那么第二个问题就是,我是该抛Exception还是RuntimeException呢?

方案二

抛出Exception的方案,先定义通用CommonException如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class CommonException extends Exception {

private int code;

private String error;

public CommonException(int code, String error) {
this.code = code;
this.error = error;
}
//==== 其他构造方法 ====
}

最后方法里面就变成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public UserInfo handle2(String phone) throws CommonException {

// 验证逻辑1

// 验证逻辑2
if ("123".equals(phone)) {
throw new CommonException(-100, "验证逻辑2不通过");
}

// 验证逻辑3

//用户信息填充
UserInfo userinfo = new UserInfo();

return userinfo;
}

变成这种结构之后,则需要在controller或者全局过滤器里面,将结果和异常捕获,在封装成CommonResult序列化返回,整个流程暂时也是很舒服的。
但是如果有多层嵌套的方法,则需要每个方法都抛出这种通用异常CommonException,但是这种异常并不是由调用方来处理的,而是最外层的controller或者全局过滤器统一处理的,这个是不合理也不舒服的地方。

造成这种现象的本质原因是,没有搞清楚Exception和RuntimeException的分别使用场景,我觉得Exception的设计考虑是,需要调用方处理这种异常情况,并且需要调用方针对不同的Exception做不同的处理,影响的是业务流转方向,不会中断业务流程。

RuntimeException的场景则是,某个校验不通过,整个流程其实都跑不下去的,需要中断整个流程。

方案三

定义通用RuntimeException如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class CommonRuntimeException extends RuntimeException {

private int code;

private String error;

public CommonRuntimeException(int code, String error) {
this.code = code;
this.error = error;
}
//==== 其他构造方法 ====
}

方法逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public UserInfo handle3(String phone) {

// 验证逻辑1

// 验证逻辑2
if ("123".equals(phone)) {
throw new CommonRuntimeException(-100, "验证逻辑2不通过");
}

// 验证逻辑3

//用户信息填充
UserInfo userinfo = new UserInfo();

return userinfo;
}

这种的话,无论几层的方法嵌套,我都不需要显式的抛出异常,我只需要舒服地专注于写正常流程的代码即可,异常的、校验不通过的,并且流程跑不下去的情况,我只需抛CommonRuntimeException出来即可,其他就不需要管了。

补充个全局过滤器的实现:

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
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest hreq = (HttpServletRequest) request;
HttpServletResponse hres = (HttpServletResponse) response;
try {
before(hreq, hres);
chain.doFilter(request, response);
} catch (Exception e) {
error(hreq, hres, e);
} finally {
after(hreq, hres);
}
}

public void error(HttpServletRequest hreq, HttpServletResponse hres, Exception e) throws IOException {
if (e instanceof CommonRuntimeException) {
CommonRuntimeException ex = (CommonRuntimeException) e;
ResponseUtil.fail(hreq, hres, ex.getCode(), ex.getError());
return;
} else if (e.getCause() instanceof CommonRuntimeException) {
//处理spring框架包了一层的情况
CommonRuntimeException ex = (CommonRuntimeException) e.getCause();
ResponseUtil.fail(hreq, hres, ex.getCode(), ex.getError());
return;
}
}

//其他非通用异常的默认处理
ResponseUtil.response(hreq, hres, getCode(defErr), getError(defErr));
return;
}

至此,使用方案三的话,在处理业务逻辑时,我就完全不需要处理异常的流转逻辑了,专注于正常的业务流转逻辑开发。
当然,如果有一些异常的场景,是影响到整个业务的流转方向的,那么自定义Exception并显示地提示调用方处理,还是很有必要的,所以并不能滥用CommonRuntimeException。

补充,每次抛CommonRuntimeException之前,必须打印上下文的日志,本文为了讲述清晰,代码例子都没添加日志。

btw,如果文章有帮助,请点赞转发。