📄 exception.txt
字号:
一个未检查的异常
应变情况恰如其分地匹配给了Java检查的异常。因为它们是方法的语义算法合同中不可缺少的一部分,在这里借助于编译器的帮助来确保它们得到解决是很有道理的。如果你发现编译器坚持应变的异常必须要处理或者在不方便的时候必须要声明会给你带来些麻烦,你在设计上几乎肯定要做些重构了。这其实是件好事。
出现故障的情况对开发人员而言是蛮有意思的,但对软件逻辑而言却并非如此。那些软件”消化问题“的专家们需要关于故障的信息以便来解决问题。因此,未检查的异常是表示故障的很好方式。他们让故障的通知原封不动地从调用栈上所有的方法滤过,到达一个专门来捕获它们的地方,并得到它们自身包含的有利于诊断的信息,对整个事件提供一个有节制的优雅的结论。产生故障的方法不需要来声明(异常),上游的调用方法不需要捕获它们,方法的实施细则被正确的隐藏起来- 以最低的代码复杂度。
新一些的Java API,比如像Spring架构和Java Data Ojects类库对检查的异常几乎没有依赖。Hibernate ORM架构在3.0版本里重新定义了一些关键功能来去除对检查的异常的使用。这就意味着在这些架构举报的绝大部分异常都是不可恢复的,归咎于错误的方法调用代码,或是类似于数据库服务器之类的底层部件的失败。特别的,强迫一个调用方来捕获或声明这些异常几乎没有任何好处。
设计里的故障处理
在你的计划里,承认你需要去做就迈好了有效处理好故障的第一步。对那些坚信自己能写出无懈可击的软件的工程师们来说,承认这一点是不容易的。这里是一些有帮助的思考方式。首先,如果错误俯拾即是,应用的开发时间将很长,当然前提是程序员自己的bug自己修理。第二,在Java类库中,过度使用检查的异常来处理故障情形将迫使你的代码要应对好故障,即使你的调用次序完全正确。如果没有一个故障处理的架构,凑合的异常处理将导致应用中的信息丢失。
一个成功的故障处理架构一定要达到下面的目标:
减少代码的复杂性
捕获和保存诊断性信息
对合适的人提醒注意
优雅地退出行动
故障是应用的真实意图的干扰。因此,用来处理它们的代码应尽量的少,理想上,把它们和应用的语义算法部分隔离开。故障的处理必须满足那些负责改正它们的人的需要。开发人员需要知道故障发生了,并得到能帮助他们搞清为何发生的信息。即使一个故障,在定义上而言,是不可补救的,好的故障处理会试着优雅地结束引起故障的活动。
对故障情况使用未检查的异常
在做框架上的决定时,用未检查的异常来代表故障情况是有很多原因的。Java的运行环境对代码的错误会抛出“运行时异常”的子类,比如,ArithmeticException或ClassCastException。这为你的框架设了一个先例。未检查的异常让上游的调用方法不需要为和它们目的不相关的情况而添加代码,从而减少了混乱。
你的故障处理策略应该认识到Java类库的方法和其他API可能会使用检查的异常来代表对你的应用而言只可能是故障的情况。在这种情形下,采用设计约定来捕获API异常,将其以故障来看待,抛出一个未检查的异常来指示故障的情况和捕获诊断的信息。
在这种情况下抛出的特定异常类型应该由你的框架来定义。不要忘记一个故障异常的主要目的是传递记录下来的诊断信息,以便让人们来想出出错的原因。使用多个故障异常类型可能有些过,因为你的架构对它们都一视同仁。多数情况下,一条好的,描述性强的信息将单一的故障类型嵌入就够用了。使用Java基本的RuntimeException来代表故障情况是很容易的。截止到Java1.4,RuntimeException,和其他的抛出类型一样,都支持异常的嵌套,这样你就可以捕获和报出导向故障的检查的异常。
你也许会为了故障报告的目的而定义你自己的未检查的异常。这样做可能是必要的,如果你使用Java1.3或更早的版本,它们都不支持异常的嵌套。实施一个类似的嵌套功能来捕获和转换你应用中构成故障的检查的异常是很简单的。你的应用在报错时可能需要一个特殊的行为。这可能是你在架构中创建RuntimeException子类的另一个原因。
建立一个故障的屏障
对你的故障处理架构而言,决定抛出什么样的异常,何时抛出是重要的决定。同样重要的是,何时来捕获一个故障异常,之后再怎么办。这里的目的是让你应用中的功能性部分不需要处理故障。把问题分开来处理通常都是一件好事情,有一个中央故障处理机制长远来看是很有裨益的。
在故障屏障的模式里,任何应用组件都可以抛出故障异常,但是只有作为“故障屏障”的组件才捕获异常。采用此种模式去除了大多数程序员为了在本地处理故障而插入的复杂的代码。故障屏障逻辑上位于调用栈的上层,这样在一个默认的行动被激发前,一个异常向上举报的行为就被阻止了。根据不同的应用类型,默认的行动所指也不同。对一个独立的Java应用而言,这个行动指活着的线程被停止。对一个位于应用服务器上的Web应用而言,这个行动指应用服务器向浏览器送出不友好的(甚至令人尴尬的)回应。
一个故障屏障组件的第一要务就是记录下故障异常中包含的信息以为将来所用。到现在为止,一个应用日志是做成此事的首选。异常的嵌套的信息,栈日志,等等,都是对诊断有价值的信息。传递故障信息最差的地方是通过用户界面。把应用的使用者卷进查错的进程对你,对你的用户而言都不好。如果你真的很想把诊断信息放上用户界面,那可能意味着你的日志策略需要改进。
故障屏障的下一个要务是以一种可控的方式来结束操作。这具体的意义要取决于你应用的设计,但通常包括产生一个可通用的回应给可能正在等待的客户端。如果你的应用是一个Web service,这就意味着在回应中用soap:Server的<faultcode>和通用的失败信息<faultstring>来建立一个SOAP故障元素<fault>。如果你的应用于浏览器交流,这个屏障就会安排好一个通用的HTML回应来表明需求是不能被处理的。
在一个Struts的应用里,你的故障屏障会以一种全局异常处理器的形式出现,并被配置成处理RuntimeException的任何子类。你的故障屏障类将延伸org.apache.struts.action.ExceptionHandler类,必要的话,重写它的方法来实施用户自己的特别处理。这样就会处理好不小心产生的故障情况和在处理一个Struts动作时发现的故障。图2显示的就是应变和故障异常。
图2 应变和故障异常
如果你使用的是Spring MVC架构,你可以继承SimpleMappingExceptionResolver类,并配置成处理RuntimeException和它的子类们,这样很容易的就建起了故障屏障。通过重写resolveException的方法,你可以在使用父类的方法来把需求导引到一个发出通用错误提示的view组件之前,加入你需要的用户化的处理。
当你的架构包含了故障屏障,程序员都知晓了后,再写出一次性的故障异常的冲动就会锐减。结果就是应用中出现更干净,更易于维护的代码。
架构中应变的处理
将故障处理交与屏障后,主要组件间的应变交流变得容易多了。一个应变代表着与主要返回结果同等重要的另外一种方法结果。因此,检查的异常类型是一个能够很好地传递应变情况的存在并提供必要的信息来与它竞争的工具。这个方式借助于Java编译器的帮助来提醒程序员关于他们所用的API的方方面面以及提供全套的方法输出的必要性。
仅仅使用方法的返回值类型来传递简单的应变是可能的。比如,返回一个空引用,而不是一个具体的对象,可以意味着对象由于一个已定义的原因不能被建立。Java I/O的方法通常返回一个整数值-1,而不是字节的值或字节的数来表示文件的结尾。如果你的方法的语义简单到可以允许的地步,另一种返回值的方法是可以使用的,因为它摒弃了异常带来的额外的花销。不足之处是方法的调用方要检测一下返回的值来判断是主要结果,还是应变结果。但是,编译器没有办法来保证方法调用者会使用这个判断。
如果一个方法有一个void的返回类型,异常是唯一的方法来表示应变发生了。如果一个方法返回的是一个对象的引用,那么返回值只可能是空或非空(null and non-null)。如果一个方法返回一个整数型,选择与主要返回值不冲突的,可以表示多种应变情况的数值是可能的。但是这样的话,我们就进入了错误代码检查的世界,而这正式Java异常模式所着力避免的。
提供一些有用的信息
定义不同的故障报告的异常类型是没什么道理的,因为故障屏障对所有异常类型一视同仁。应变异常就有很大的不同,因为它们的原意是要向方法调用者传递各种情况。你的架构可能会指出这些异常应该继承java.lang.Exception或一个指定的基类。
不要忘记你的异常应该是百分百的Java类型,你可以用它来存放为你的特殊目的服务的特殊字段,方法,甚至是构造器。比如,被假想的processCheck()方法抛出的InsufficientFundsException这个异常类型就应该包含着一个OverdraftProtection的对象,它能够从另外一个帐户里把短缺的资金转过来。
日志还是不要日志
记录下故障异常是有用处的,因为日志的目的是在一些需要改正的情况下,日志可以吸引人们的注意力。但对应变异常而言却并非如此。应变异常可能代表的只是极少数情况,但是在你的应用里,每一个情况还是会发生的。它们意味着你的应用正在如最初的设计般正常工作着。经常把日志代码加进应变的捕获块里会使你的代码晦涩难懂,而又没有实际的好处。如果一个应变代表了一重要的事件,在抛出一个异常应变来警醒调用者之前,产生一笔日志,记录下这个事件可能会让这个方法更好些。
异常的各个方面
在Aspect Oriented Programming(AOP)的术语里,故障和应变的处理是互相渗透的问题。比如,要实施故障屏障的模式,所有参与的类必须遵循通用规格:
故障屏障方法必须存活在遍历参与类的方法调用图的最前端
参与类必须使用未检查的异常来表示故障情况
参与类必须使用故障屏障期望得到的有针对性的未检查的异常类型
参与类必须捕获并从低端方法中把在执行情境下注定的故障转换成检查的异常
参与类不能干扰故障异常被传递到故障屏障的过程
这些问题超越了那些本不相干的类的边界。结果就是少数零散的故障处理代码,以及屏障类和参与类间暗含的耦合(这已经比不使用模式进步多了!)。AOP让故障处理的问题被封装在通用的可以作用到参与类的层面上。如AspectJ和Spring AOP这样的Java AOP架构认为异常的处理是添加故障处理行为的切入点。这样,把参与者绑定在故障屏障的模式可以放松些。故障的处理可以存活在一个独立的,不相干的方面里,从而摒弃了屏障方法需要放在方法激活次序的最前头的要求。
如果在你的架构里利用了AOP,故障和应变的处理是理想的在应用里用到的在方面上的候选。对故障和应变的处理在AOP架构下的使用做一个完整的勘探将是将来论文里一个很有意思的题目。
结论
虽然Java异常模型自它出现以来就激发了热烈的讨论,如果使用正确的话,它的价值还是很大的。作为一个设计师,你的任务是建立好规格来最大限度地利用好这个模型。以故障和应变的方式来考量异常可以帮助你做出正确的决定。合理使用好Java异常模型可以让你的应用简单,易维护,和正确。AOP技术将故障和应变定位为相互渗透的问题,这个方法可能会对你的架构提供一些帮助。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -