Java异常处理的一些总结
原帖讨论在这里;http://www.iteye.com/topic/964535
这篇文章只是提取出我的观点,还缺乏组织
最早的争论之一就是spring,提倡异常封装成runtime,于是很多人就都以此为论据证明应该让runtime这样的unchecked异常来封装甚至替代checked异常。
spring dao封装excpetion为runtime类型,这样不用捕捉,我也这样做过
感觉如果自己内部就能消化这没什么问题,但如果是自己提供面向第三方的服务,就应该清楚告诉对方正常怎样,不正常怎样,什么异常会抛出来,就和合同一样,这本身就应该是函数调用的定义契约。
不能别人觉得好好的,突然丢个出去,造成一堆调用中断。
在这种环境下checked的语法特征就很切合这种需求。
有种说法是用runtime抛一切异常的的人应该在api层面写好说明,自己这个api应该会抛什么runtime异常,这样调用者调用此api能自己决定是否处理异常,而不必受服务提供方的接口约束。
说通俗一些,在语法成层面,checked是类似社会中的潜规则,checked是规则。
潜规则就是可忽略规则,不过你忽略它就可能存在不可知后果,而有些时候即使你明知道不能忽略潜规则,但因为服务方的疏忽,没说明他们的潜规则,使得你根本没意识到还存在这些没公开的潜规则,结果事情崩了,而且还不止你甭,你处理不了这种破坏,可能一直捅到N层以上去了,而且更坏的结果是:你很可能把一个对你或者下层来说可预见的异常情况,转变成为对于上层不可预见的一个异常,从而直接对上层造成破坏。
而不同于潜规则,规则是你必须遵守的,你遵守或者不遵守都注定会走向某个固定而且确定的流程,都是台面上的东西,双方都一定会遵守的,这样的好处就是可预见的异常一定会顺着可预见的通道走,因为所有的checked异常都是被强制安排了处理流程,或当层处理或继续上抛
当a库依赖b库,你调用a库,如果b库全是RuntimeException,并且你拿不到b库文档,你用a库能写出健壮的代码是不可能的,你自己精心构建的异常处理逻辑能轻松被无法预见的RuntimeException打断,因为你预计不到a库什么时候会被b库的雷给牵连,任何对a的调用都要考虑可能由b导致的连环雷
把代码比作做事,RuntimeException就相当于天灾人祸,意外之外的,至于发生了你愿意自己继续干还是终止取决于你自己意愿
而checked就是说你需要弄清楚并且汇报给上级那些是问题,那些是你会遇到但你解决不了的,但都是你意识到的问题,别人让你做事,你要给别人说好,如果让我干可能出现意外1,2,3,我向你汇报,并且你需要强制有意识的接受这些异常,做些安排,至于具体处理逻辑是什么你自己决定
调用链是a->b->c->d,如果d是没任何这种提前约定的契约,那么a要操心的就是调用b的所有接口,b随时也有可能被c的调用打断,而c随时会被d打断,看似编码简单,实际异常处理被大幅扩大
最简单的例子,你老板让你干活都需要先分析风险,你可能会让底下再分析,然后汇报,然后才是每层自己根据汇报的风险制定计划和执行
如果全unchecked相当于你没任何汇报,只是告诉上级,你让我干事有风险,干什么事都有风险也可能都没风险,你分配我任务的时候自己看着办,还有我下属的风险我可能也转给你也可能不给你我自己搞定,这看我自己风格,你也自己看着办吧
有checked你可以默认所有没异常申明的函数都是ok的,除了那些申明自己要抛的不用太关心那些没申明的
你使用api是不可能每个都去看文档的
我原来之所以会在dao采取这种方式因为我认为每个dao函数都应该有抛异常的可能,用check的话上层调用写起来太罗嗦,尤其再加个接口,实际spring的dao也是这样处理
而实际普遍考虑,大部分api是不应该我们操心问题的,如果有异常声明,则我们只用关注这些少量的问题点
如果全部都runtime那我们就要考虑每个调用可能造成的中断,每个api我都要参考它的文档关于异常的描述
另外一旦调用链拉长,a->d的文档是不会都那么全面和可靠,因为抛异常的可能性会随链的长度相乘。
依赖库调用的程序做异常设计是基于少数抛异常,多数不抛的原则设计
另外用文档来保证异常设计很天真,如果没有约束就连自己设计库都很难保证下层不会出乱子透过自己丢到上面,所有的异常都是runtime那么所有异常都可以有很大机会一直窜到根部调用,把你精心构建的异常层次搅得乱七八糟
要理解约束与自由的关系和它们带来的作用和副作用,理解级联调用带来隐患的可能性随级联数目是乘法式的上升。
增加一个约束往往要增加不少成本,你应该多考虑设计者增加checked这个约束到底是用来解决什么问题。
在那个帖子也有提到框架喜欢抛unchecked一个原因是因为框架异常实际客户代码根本搞不定,这样很多时候就不存在在某层恢复的可能性,这使得框架异常经常需要一直捅上去,所以check机制这时是种阻碍
但你构建自己的应用并不肯定全是这样,你这个层次搞不定的问题,可能必须要上面某一层搞定,所以安全的途径是通过固化机制强制执行
我原来做开发是不会在根部catch任何runtime的异常,意料之外的runtime都是会直接导致当机
因为这如果发生代表我的代码出现了我完全没想到的问题,既不在主流程也不在预计内的异常流程
这个异常突破了所有异常处理和保护来到main,那么调用链涉及的下面的component因为这个异常导致的问题和后续可能造成的影响我无法估计,因为每个调用都只跑了不确定的半截,这个时候不如停下来,保证破坏不会被扩大,当然这种设计不一定会被接受,尤其国内,也不适合所有系统
实际上根调用加个catch all易如反掌,但异常真的到了这步,其实这个应用就像内部已经出了一系列故障的飞机,你只是视而不见,却仍然让他继续飞
checked异常的缺点,反对方通常集中抨击两点:吞异常和接口污染
分开说
1.吞异常是态度问题。
下级来异常,要么扩展接口向上抛,要么代码吞,如果吞了,至少说明他认为问题应中止于此,也不需要做什么,如果不应该他消化的他也照吞只为了图简便,那还是开了他吧。
这样的人更不会好好对待下层上来的和自身的unchecked exception,也不用指望他能好好注明文档。应该止于此处的excpetion很可能就被轻而易举抛到上层,而上层对这种可能性一无所知,毫无保护,因为这样的人懒得要吞checked异常就更不会为下面那些很难估计的unchecked去考虑完善文档,指望测试搞定这些不可预计的异常?拉到吧,国内绝大部分公司连ifelse支路测试覆盖都过不了80%,就别谈那些unchecked异常了
2.接口污染
确实有这个问题,异常成为了接口一部分会影响上层
但是你要明白,checked 异常和返回类型、函数签名一样,你用了,就已经被侵入了,你怎么不质疑为什么函数调用返回不能是obj或者void*,我自己决定转不转型,你返回你的类型不是强迫我import或者include返回值类型定义吗?这不是服务端强暴调用端吗?为什么你要有时返回空指针,不能返回空对象,害得调用端还要判断,这不又是强暴吗?
一个接口7,8个异常,这是设计层面的问题
每个层应该有自己的异常,层内部本身应该转义,每个层应该内部消化掉或者转义大部分的下层以异常。你给客户看个spring初始化文件读取错误的异常有意义吗?如果能做到这样即使深层次的嵌套异常数目也不会数目爆炸。
因为checked实际已经把异常上升到签名级别就导致侵入是一定的,所以设计必须谨慎,要确认应该由调用这处理异常或者恢复的情况才使用。
靠近底层多用checked,靠近上层多用unchecked,越往上实际对异常恢复的能力就会越弱,并且越往上依赖下层数目较少的底层api散开的上层业务逻辑api会越多,这个时候checked会方便很多。
而且上层的业务逻辑一大特点是不用太关注一致性(都是底层保证了),都是runtime的内容,而且可以大多都能容忍失败,发生失败的时候只要捕捉抛到高层的异常和失败结果抛到客户端,表示成用户能阅读的内容就可以。
数据库、文件访问,在集群环境底下其实都会成为可恢复异常,经常也是必须恢复和处理的异常,这些异常如果无法解决,程序可能根本没必要运行,如果一个数据库异常或者配置文件缺失的异常以runtime形式而未被捕捉,抛到了end user,可以说基本没任何意义。
如果有层A,B,C,D,A->D是顶层到底层
异常e1,e2,e3都会从D层抛出,而e1是必须在B解决的,e2,e3分别是无关痛痒的异常
那么e1即使本身是unchecked也建议包成checked,那怕B->D隔着10层也应该这样。
unchecked导致的try偏多顶多是麻烦,不去try,那么顶多是导致接口会加入异常抛出申明,但这比正式上架漏掉某些关键异常的保护导致的客户损失要轻很多了