异常以及异常处理框架探析
概述
一般情况下,企业级应用都对应着复杂的业务逻辑,为了保证系统的健壮,必然需要面对各种系统业务异常和运行时异常。
不好的异常处理方式容易造成应用程序逻辑混乱,脆弱而难于管理。应用程序中充斥着零散的异常处理代码,使程序代码晦涩难懂、可读性差,并且难于维护。
一个好的异常处理框架能为应用程序的异常处理提供统一的处理视图,把异常处理从程序正常运行逻辑分离出来,以至于提供更加结构化以及可读性的程序架构。另外,一个好的异常处理框架具备可扩展性,很容易根据具体的异常处理需求,扩展出特定的异常处理逻辑。
另外,异常处理框架从一定程度上依赖并体现系统架构层次。系统架构决定了系统中各个子系统,各个层次之间的交互,而异常处理框架则统一体现这种架构中的各种交互所发生的错误、异常。因此,异常处理框架是系统架构时就应该考虑的问题。
本文将对异常相关方面做一些讨论,并进而探讨一些关于构建稳健且可扩展的异常处理框架方面的视角或设计原则。由于本文引入一部分 Java 语言中异常相关的概念,因此本文假设您熟悉 Java 相关基础知识以及了解 Java EE 和 EJB 相关技术。
Java 异常基本概念
在 Java 程序设计语言中,使用一种异常处理的错误捕获机制。当程序运行过程中发生一些异常情况,程序有可能被中断、或导致错误的结果出现。在这种情况下,程序不会返回任何值,而是抛出封装了错误信息的对象。Java 语言提供了专门的异常处理机制去处理这些异常。如图 1 所示为 Java 异常体系结构:
图 1. Java 异常体系结构
检查 (Checked) 异常与非检查 (Unchecked) 异常
Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常都称为非检查异常。除“非检查异常”以外的所有异常都称为检查异常。检查异常对方法调用者来说属于必须处理的异常,当一个应用系统定义了大量或者容易产生很多检查异常的方法调用,程序中会有很多的异常处理代码。
如果一个异常是致命的且不可恢复并且对于捕获该异常的方法根本不知如何处理时,或者捕获这类异常无任何益处,笔者认为应该定义这类异常为非检查异常,由顶层专门的异常处理程序处理;像数据库连接错误、网络连接错误或者文件打不开等之类的异常一般均属于非检查异常。这类异常一般与外部环境相关,一旦出现,基本无法有效地处理。
而对于一些具备可以回避异常或预料内可以恢复并存在相应的处理方法的异常,可以定义该类异常为检查异常。像一般由输入不合法数据引起的异常或者与业务相关的一些异常,基本上属于检查异常。当出现这类异常,一般可以经过有效处理或通过重试可以恢复正常状态。
由于检查异常属于必须处理的异常,在存在大量的检查异常的程序中,意味着很多的异常处理代码。另外,检查异常也导致破坏接口方法。如果一个接口上的某个方法已被多处使用,当为这个方法添加一个检查异常时,导致所有调用此方法的代码都需要修改处理该异常。当然,存在合适数量的检查异常,无疑是比较优雅的,有助于避免许多潜在的错误。
到底何时使用检查异常,何时使用非检查异常,并没有一个绝对的标准,需要依具体情况而定。很多情况,在我们的程序中需要将检查异常包装成非检查异常抛给顶层程序统一处理;而有些情况,需要将非检查异常包装成检查异常统一抛出。
多视角理解异常
从应用系统最终用户的角度来看,用户所面对的是系统中所提供的各种业务功能以及系统本身的管理功能。用户并不理解系统内部是如何实现以及如何运行的,与系统开发者存在天然的鸿沟,系统运行对用户来说如同一个黑盒一样。对用户而言,系统所出现的任何异常或错误,都属于系统运行时异常。对于这些异常,有些异常是用户可以理解并能解决的;而另外一些异常是用户无法理解和解决的。当一个系统错误出现时,系统本身需要反馈给用户一种可理解的业务相近的信息,从而用户可以根据这些信息去尽可能解决问题。另一方面,有一类错误属于系统内部运行异常或错误,用户对此类错误根本无能为力。而这类异常同样需要提供足够详细的信息,系统管理员可根据这类异常尽可能解决。一般情况下,如果异常面向系统用户,以系统异常呈现更好。
从系统开发者角度来看,更多的是从系统内部逻辑来看异常。有一部分异常需要内部截获处理,而另外一部分异常对于异常产生源而言无法进行有效处理,从而需要向外抛出异常以待合适的调用者进行处理。对于开发者而言,需要预见异常,并且需要考虑何时处理异常,何时抛出异常,必要时以某种方式记录或通知异常。总而言之,开发者需要通过对系统运行时可能出现的异常尽可能地处理以保证系统的正常运行,并对于无法处理的异常以一种合适的方式记录、通知、呈现以便找到发生异常的原因,从而解决或避免异常。
图 2. 异常视图
异常管理与异常框架
基本异常处理结构
如图 3 所示,为一个常见的一般性异常处理代码结构。其中,try 语句块代表要运行的代码并受异常监控,其中代码发生异常时,会创建一个异常对象并抛出。
catch 语句块会捕获 try 代码块中发生的异常,并与自己的异常类型进行匹配,若匹配,则在其 catch 代码块中进行异常处理。catch 语句块可以有多个,当 try 语句块中抛出一个异常时,会针对每个 catch 块进行匹配,一旦与某个 catch 块匹配,就进入该 catch 块处理并不再与其他 catch 块匹配。
finally 语句块是紧跟 catch 语句后的语句块,该语句块总会在方法返回前执行,无论 try 语句块是否发生异常。
图 3. 异常处理代码结构
前面说过,一般当程序发生异常时,通常异常处理可能需要做一些通用处理,如异常日志记录、异常通知,重定向到一个统一的错误页面(如 Web 应用)等。如果这些通用异常处理放置于 catch 块中,将导致大量的重复代码,从而可能引起日志冗余、同一异常的实现多样化等问题。另外,大量异常处理程序放置于 catch 块中造成程序的高耦合性。为了解决此类问题,有必要分离出异常处理程序、统一异常处理风格、降低耦合性、增强异常处理模块的复用程度。通常的异常处理模式包括业务委托模式(Business Delegate)、前端控制器模式(Front Controller)、拦截过滤器模式(Intercepting Filter)、AOP 模式、模板方法模式等。
一般性异常处理框架
为了解决基本异常处理结构所带来的问题,不妨把异常相关处理委托给一个专用 Service 代理,从而分离出异常处理业务,以一种统一的方式和逻辑进行处理,如图 4 所示。异常 Service 主要处理两个方面:一方面是要按照实际系统要求调用通用处理程序处理异常,如日志记录、异常界面展示、异常通知等;另外一方面,需要通过过滤所接受到的异常类型,找到定制的异常处理程序进行异常处理。对于异常 Service 的应用一般可以通过在系统的顶层进行异常自动拦截(一般多层系统中尤为普遍,如放置于前端控制器 Front Controller),或者主动调用异常 Service 进行处理。
图 4. 一般性异常处理框架
如图 5 所示类图显示了一个具体的异常处理框架:
图 5. 异常处理框架类图样例
该框架主要包括三部分:异常 Service、异常处理过滤器、系统异常层次定义。
异常 Service:整个异常框架的核心,通常用于主动拦截异常或被动调用处理异常。根据具体业务需要,调用通用服务程序进行一般化异常处理,如日志服务、异常消息通知服务等;另外,异常 Service 最主要目的用于拦截并处理异常,其需要维护定制的异常处理器链,用于特定类型异常的特定处理。
异常处理过滤器:维护系统中各种异常处理器,包括增加异常处理器、删除异常处理器、查找异常处理器操作等。其中最主要的功能是接收特定异常并找到与之匹配的异常处理器进行处理。异常处理过滤器具体实现可以通过一个配置文件维护所有异常与异常处理器的映射,另外可以通过另外一个配置文件维护系统中所有已定义的异常处理器。从而,异常处理过滤器可以通过配置文件进行初始化操作。
异常层次定义:异常层次定义应用系统的异常基础结构,是异常处理过滤器所处理的目标异常类型集合。
异常层次定义
异常层次结构应该以一种普遍通用的原则定义。为此,我们可以利用面向对象语言具备多态的性质,隐藏异常的实际实现。对于异常 Service 而言,只需要捕获最基本的应用程序异常 AppException,异常处理过滤器会自动过滤实际异常类型并找到相应的异常处理器。另外,在方法的 throws 语句中勿需放入大量的检查异常;对方法调用者也不会出现混乱的 catch 块,最多可能只存在一个用于处理基本应用程序异常 AppException(委托给异常 Service 处理)。
前面的章节讲过,应用系统异常可以从用户和开发者两个视角去考虑。因此,我们可以把异常划分为业务操作异常和系统内部运行时异常两种类型。抛出业务级异常或系统运行时异常的决策,需要与应用系统本身的架构层次相结合,考虑所要处理异常的层次。如图 6 所示为一个典型的异常层次结构:
图 6. 异常层次类图样例
其中,BussinessException 属于基本业务操作异常,所有业务操作异常都继承于该类。例如,通常 UI 层或 Web 层是由系统最终用户执行业务操作驱动,因此最好抛出业务类异常。ServiceException 一般属于中间服务层异常,该层操作引起的异常一般包装成基本 ServiceException。DAOException 属于数据访问相关的基本异常。
对于多层系统,每一层都有该层的基本异常。在层与层之间的信息传递与方法调用时候,一旦在某层发生异常,传递到上一层的时候,一般包装成该层异常,直至与用户最接近的 UI 层,从而转化成用户友好的错误信息。
异常转译以及异常链
前面关于检查异常和非检查异常的论述中提到,在存在大量的检查异常的程序中,意味着很多的异常处理代码,导致晦涩的异常处理,并且检查异常容易破坏接口方法。为了解决检查异常带来的缺陷,我们可以利用异常转译的方法,将检查异常转化为非检查异常,由异常 Service 拦截处理。
异常转译就是将一种异常转换为另一种异常。异常转译针对所有继承 Throwable 超类的异常类而言的。如下图 7 中代码所示展示了异常转译的一个例子:
图 7. 异常转译代码样例
对于任何一个应用系统而言,系统运行过程中所发生的任何异常或错误都应该以合适的方式通知用户或记录;由于异常源可能来自很多方面,其所抛出的异常大多不能为系统用户所理解,此时就必须将各种类型的异常转化成各种用户可理解的异常。这也是异常框架所需要关注和解决的方面。
在异常的层层转译过程中,就形成一个异常链。整个异常链保存了每一层的异常原因。通过递归调用 getCause() 方法可以遍历所有的异常原因。需要注意的是,在形成异常链的过程中,会消耗较多的资源,导致系统性能降低。这里涉及异常原理,在此不必多说,有兴趣可查阅相关资料。在本文提出的异常框架中,异常 service 可以截获来自系统各层的异常,而勿需异常层层转译。
结束语
本文首先简要介绍了异常的基本概念以及 Java 语言中基本异常体系结构,重点介绍了 Java 异常中的检查 (checked) 异常和非检查 (unchecked) 异常两个概念。然后,着重介绍了对于一个应用系统从用户和开发者两个角度如何去看应用系统所发生的异常;通过多视角看应用系统异常对于设计一个合理的系统异常框架可以提供较好的设计原则。最后介绍了一个通用可扩展的异常处理框架,包括设计原则,异常层次结构的定义以及异常转译方面的考虑。
尤其对于比较大的软件系统,异常处理框架是软件系统体系结构需要考量的很重要的一方面。好的异常处理结构既能条理清晰地处理异常,又能保证异常处理的可扩展性与可用性,最后还需要保证系统的性能不受额外的损失。