我为什么反对用异常做流程控制?

曲总 技术琐话


我为什么反对用异常做流程控制?


我为什么反对用异常做流程控制?

“懒”是驱动程序员前进的原动力,亦是原罪。


像SSH/M这种基础框架的出现,让不少程序员“瘫痪”成了流水线工人。以前小心翼翼方能写就的逻辑分支判断,演变成了直接丢个异常然后坐等AOP拦截处理,此时的拦截器就是个垃圾处理厂。这种似乎失控的编码方式,让我想到了邪恶的“GoTo”语法,很多编程语言里都有它, 但是都不建议你用它。因为邪恶的不是GoTo本身,而是滥用GoTo的我们。


题眼基本表达了我的论点,随着本文的深入会对该论点做加一个约束条件。现在容我开始论证它~


都说抛异常很重,到底重在哪里?


不整虚的,我们用测试数据来说话。采用OpenJDK的JMH基准测试框架实现,设计如下6种测试场景:

  1. New一个普通的Exception
  2. New一个普通的不包含堆栈信息的Exception
  3. New一个普通的自定义对象
  4. Throw一个普通的Exception
  5. Throw一个普通的不包含堆栈信息的Exception
  6. 获取/打印异常的堆栈信息
我为什么反对用异常做流程控制?

我为什么反对用异常做流程控制?

我为什么反对用异常做流程控制?

我为什么反对用异常做流程控制?

6个场景的benchmark测试报告如上图。从结果数字可以看出:耗时最短的是创建自定义对象,耗时最长的是获取异常的堆栈信息。详细说明几个要点:


&创建对象:自定义对象 VS 无堆栈异常 VS 普通异常

三者的耗时依次递增,自定义对象的创建作为基准参照耗时,无堆栈异常创建的耗时是其5倍,普通异常创建的耗时是其250倍。所以异常从出生就死在起跑线。虽然我们的测试耗时是纳秒级别,若从系统接口通常的秒为单位,就算30倍也可以忽略不计。但是在这里已经可以凸显出异常本身的沉重。


&异常的创建到抛出到捕获

异常的创建 和 叠加异常的抛出捕获 前后并没有特别明显的性能损耗,抛异常的耗时可以忽略不计。

明确概念1:Java中如果不发生异常,try/catch基本是不会造成任何性能损失的(查看字节码了解

异常表)。而一旦发生异常,除了昂贵的异常填充堆栈成本,也就是确认下try block对应异常表记录的起止代码行和异常名称是否一致。上测试结果也表明确实会有性能波动,但其实很小。

我为什么反对用异常做流程控制?

明确概念2:对于try block内的代码,Java会阻止指令重排序一类的内存优化手段。所以即使try的性能损耗很小,但是我们仍旧建议try block的边界越窄越好。


明确概念3:try block的范围即使很宽,对于堆栈深度来说并无特别影响。因为栈帧的深度取决于不同方法之间的调用关系和次数。


&异常堆栈的获取/打印

现实喜欢狠狠的打人脸,原以为测试出真相了,结果数据告诉我们最耗时的操作竟是读取堆栈操作。

我为什么反对用异常做流程控制?

Thread::getStackTrace()做个简单说明。大家可以看一下JDK源码,在当前线程里它等同于

(new Exception()).getStackTrace()

实例化一个异常对象已经够慢了,获取异常堆栈数据的耗时竟然达到10倍以上。大家想一想不管是自己写的try/catch代码块,还是AOP的拦截器,是不是都会读取堆栈,然后打印到日志里用于排障?


所以异常重不重已经很明确了吧?再贴一遍测试数据感受一下,所有的真相都在此图了。

我为什么反对用异常做流程控制?

代码示例已上传Github

https://github.com/NicholasQu/snippets


接口设计如何定义异常的边界?


传统的接口设计规范说明会包含几个基本要素:接口名/地址、版本号、请求参数,响应参数。其中应答的响应码基本都会一一列举并详细说明,让调用方简单直观的理解到此接口的服务能力。


当把控制流程的异常嵌入到接口设计里,随之问题就来了:

  1. 甚少看到有人能够在Javadoc里使用@exception将接口内的异常标注清楚;
  2. 如何权衡选择正常的应答返回还是抛异常?当接口应答只是true/false的时候,抛异常会是个很匪夷所思的设计;
  3. 当下层方法不断的抛出各种异常,然后汇总到拦截器里处理时,或者需要对异常拆开做判断,再自定义成合理的应答话术;或者将好不容易区分开的不同异常,被整合成了“通用系统异常”无法分辨;这时候的拦截器就是个异常中央处理池,拆就是hardcode,不拆就可能是浪费了之前的异常细颗粒度;
  4. 为了让代码不那么丑陋,自定义的异常通常继承自RuntimeException。在接口提供方和调用方没有通过介质(接口设计文档/对话...)充分沟通清楚的情况下,一个神不知鬼不觉的Runtime异常完全可能造成自身业务逻辑的无法自恰;
  5. 异常具有正常应答无法比拟的分层穿透性,也就是不管间隔多少层,都可以直接穿透到接入层。对于某些异常场景下的代码实现确实有很好的支撑,反之,成也萧何败萧何,这种强力的穿透能力是否会让逻辑失控,totally count on you and your team;


综合上面的这些点,异常作为非常规的设计路数,在没有足够的把控能力下,千万不要无尽蔓延,这种类似“GoTo”式的代码实现,可能会让你的系统支离破碎。


我的态度


任何的系统架构设计,都是在不断的在做天人交战,利弊权衡。鲜有绝对的对与错,只有在当前组织环境内相对的合理与不合理。对于异常用作流程控制这件事,我是投反对票。因为即使异常的性能损耗对我们大部分的业务场景可以忽略不计的,但异常在接口中的易被忽视性、不可控的穿透性,就算是高素质的团队也不一定能完全消除这种风险。既然风险如此大,宁肯让团队按部就班老老实实的写好每一种应答。


承篇头的论点,重新展开再抽象归纳一下:

任何逻辑判断的流程控制都不应该用异常来实现,除非那些能明确导致程序中断/终止的节点。异常务必要明确抛checked还是unchecked,对调用者负责。


分享到:


相關文章: