美文网首页
怎么写代码才舒服--合理使用异常

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

作者: 二当家的黑板报 | 来源:发表于2019-06-08 12:30 被阅读0次

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

    异常类型

    先说一下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:

    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/

    相关文章

      网友评论

          本文标题:怎么写代码才舒服--合理使用异常

          本文链接:https://www.haomeiwen.com/subject/phkixctx.html