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

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

异常类型

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容