记录例外:其中记录例外?
通常要求必须记录业务应用程序中发生的异常,以便在必要时可以由人员进行进一步检查。特别是在Web或者服务器应用程序中,控制台输出可能无法检查。异常日志可以通过包含足够的信息来揭示错误原因,或者有助于重现异常,来帮助确定问题所在。
在设计应用程序的日志记录时,经常会出现问题:应在代码中的何处记录异常?基本上,我们有三种不同的选择:
- 底层日志记录登录发生异常的组件
- 中级日志记录在调用堆栈中间的某个地方记录日志,那里有足够的可用信息(组件调用的上下文)
- 顶层日志集中记录在调用堆栈的顶部
下面的列表显示了一个调用堆栈,其中组件A调用B。B调用其他组件,这些组件依次调用组件F。组件A是顶层组件,B等是中层组件,F是底层组件。
A B ... F
底层记录
我们拥有的第一个选择是将异常记录在发生异常的组件中。如果我们不能更改该组件的源,那么我们将尽可能地靠近抛出异常的组件方法调用进行记录。乍一看,将日志记录封装在引发异常的组件中似乎是一个不错的设计选择。但是,这种方法有一些缺点。
首先,必须将日志记录编码到每个能够引发异常或者调用引发异常的第三方组件的组件中。这是很多要编写和维护的日志记录代码。
其次,如果要在不同的应用程序之间重用该组件,则该组件无法知道如何在每个应用程序中记录异常。我们将不得不让日志记录策略可插入组件中,然后无论如何日志记录实际上并没有封装。
第三,引发异常的组件可能实际上没有足够的信息来编写详细且明智的日志消息。假设我们有一个将对象写入数据库的组件。因为字段之一为空,所以引发异常。组件可以准确告诉日志中的哪个字段为null吗?它可以说明为什么该字段为空吗?也许该对象使用该字段加载为null,或者用户采取的某些操作导致该对象变为null。也许某些代码中的错误导致该字段为空。发生异常时,组件是否知道登录的用户?所有这些信息可能需要记录。底层组件可能没有所有可用的信息。其余的所需信息可能在调用堆栈的更远处可用。如果在组件内部的底部完成日志记录,则无法记录此信息。
中级日志记录
除了底层记录之外,我们还可以在中层记录日志,只要有足够的信息可写出令人满意的日志消息即可。这使底层组件免于记录代码,但是也有一些缺点。
在中层,我们可能无法获得引发异常的最底层组件的所有详细信息。然后,底层组件必须提供详细的错误文本甚至可能是错误代码,以使在中间层进行明智的记录成为可能。
在中级日志记录时,仍然有很多日志记录代码需要编写。在捕获和记录异常的每个位置,都将必须插入几乎相同的记录代码。如果以后需要更改日志记录策略,则需要进行大量工作。一种快捷方式是使用面向方面的编程来注入日志记录代码,但我在这里不做介绍。
顶级日志
在顶层进行日志记录意味着我们可以在代码中占据一个中心位置,以捕获所有引发的异常并将其记录下来。在Java Web应用程序中,它可以是控制servlet或者servlet过滤器。在桌面应用程序中,也许事件处理程序将扩展一个基本事件处理程序,使其能够捕获和记录所有引发的异常。
顶级日志记录的优点是我们在应用程序中只有一个位置可以编写和维护日志记录代码。除了易于实现之外,如果需要的话,这还使得将日志记录实现推迟到开发过程的后期变得更加容易。日志记录代码只需要在一个地方实现,并且很可能不会更改整个应用程序调用结构。
顶层日志记录的缺点当然是,该中心位置不了解引发异常的底层组件发生了什么错误,或者调用该组件的中层代码试图做什么。这使得编写明智的日志消息变得更加困难。
异常丰富
为了克服顶层(以及中层在某种程度上)可用细节的不足,我们可以在异常沿调用堆栈向上传播到顶层时丰富它们。通过捕获异常,添加额外的信息并再次抛出异常,可以丰富异常。重新抛出捕获的Java异常不会更改嵌入式堆栈跟踪。堆栈跟踪与底层组件首先引发异常时的堆栈跟踪相同。
我们将必须编写自己的异常类,以使异常丰富化成为可能。或者这样做,或者我们必须将捕获的异常包装到新异常中并抛出新异常。
总结与建议
我们研究了三种不同的日志记录策略:底层,中级和顶层日志记录。
我建议尽可能使用顶级日志记录,因为它最容易编码和维护,并且如果我们不想一开始就被它打扰,也可以在开发过程的后期进行添加。我们可能会遇到必须在较低级别捕获和处理异常的情况,但是根据我的经验,这些情况并不经常发生。大多数情况下,发生异常时,将跳过请求或者事件处理,并将异常与足够的信息一起记录下来以确定错误原因,或者至少重现该异常。