今天讲点干货,来点思想上的碰撞,代码风格的讨论,我们来探讨一下怎么合理使用异常,来使代码流程简洁易懂,使写代码写得舒服。
异常类型
先说一下java的异常类型有:
检查性异常(checked exceptions) 是必须在在方法的
throws
子句中声明的异常。它们扩展了异常,旨在成为一种“在你面前”的异常类型。JAVA希望你能够处理它们,因为它们以某种方式依赖于程序之外的外部因素。检查的异常表示在正常系统操作期间可能发生的预期问题。 当你尝试通过网络或文件系统使用外部系统时,通常会发生这些异常。 大多数情况下,对检查性异常的正确响应应该是稍后重试,或者提示用户修改其输入。
非检查性异常(unchecked Exceptions) 是不需要在throws子句中声明的异常。 由于程序错误,JVM并不会强制你处理它们,因为它们大多数是在运行时生成的。 它们扩展了RuntimeException
。 最常见的例子是NullPointerException
[相当可怕..是不是?]。 未经检查的异常可能不应该重试,正确的操作通常应该是什么都不做,并让它从你的方法和执行堆栈中出来。 在高层次的执行中,应该记录这种类型的异常。
错误(errors) 是严重的运行时系统问题,几乎肯定无法恢复。 例如OutOfMemoryError
,LinkageError
和StackOverflowError
, 它们通常会让程序崩溃或程序的一部分。 只有良好的日志练习才能帮助你确定错误的确切原因。
补充完以上的概念后,我们再来探讨一下这三种异常怎么用,什么时候用,该用哪种。Error是系统异常就不说了,我们直接讲Exception和RuntimeException。
怎么用
直接通过场景对比来剖析异常什么时候用比较好,该用哪种类型异常。
场景分析
用户在系统中输入证件号查询用户信息,如果成功返回{"code":200, "result":{用户信息对象}}
,验证不通过则返回{"code":-100, "error":"系统验证错误"}
,那么这里的验证不通过,怎么处理才比较好。
方案一
return的方式,定义通用结果对象CommonResult:
public class CommonResult {
private int code;
private UseInfo result;
private String error;
//===== 忽略getset ====
}
如果只是单层方法里面的验证逻辑,可能代码是这样的:
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如下:
public class CommonException extends Exception {
private int code;
private String error;
public CommonException(int code, String error) {
this.code = code;
this.error = error;
}
//==== 其他构造方法 ====
}
最后方法里面就变成如下:
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如下:
public class CommonRuntimeException extends RuntimeException {
private int code;
private String error;
public CommonRuntimeException(int code, String error) {
this.code = code;
this.error = error;
}
//==== 其他构造方法 ====
}
方法逻辑如下:
public UserInfo handle3(String phone) {
// 验证逻辑1
// 验证逻辑2
if ("123".equals(phone)) {
throw new CommonRuntimeException(-100, "验证逻辑2不通过");
}
// 验证逻辑3
//用户信息填充
UserInfo userinfo = new UserInfo();
return userinfo;
}
这种的话,无论几层的方法嵌套,我都不需要显式的抛出异常,我只需要舒服地专注于写正常流程的代码即可,异常的、校验不通过的,并且流程跑不下去的情况,我只需抛CommonRuntimeException出来即可,其他就不需要管了。
补充个全局过滤器的实现:
@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,如果文章有帮助,请点赞转发。
更多技术文章可查看:https://www.edjdhbb.com/categories/