注:本文翻译自Exceptional practices,共分为3节。该文章从实践的角度非常透彻地解释了为什么要处理异常,以及如何处理异常。这些都是那些只会介绍trycatchfinally语法的教材所不具备的知识。关于语法知识,请查看Java-错误处理机制学习(一)异常处理。
本文目录如下:
一、在程序中高效地使用异常
二、使用异常链来保存调试信息
三、易于使用的本地化信息目录
一、在程序中高效地使用异常
正确地使用异常能够使你的应用程序易于开发和使用,不被bug所困扰,且易于使用。当异常被错误地使用时,会导致相反的局面:应用程序表现差劲,用户感到困惑,并且难以维护。为了避免这些情况,本系列将提供有价值的异常处理技术。在第一节,我会介绍有效使用异常所面临的挑战,并给出如何在设计阶段将异常处理正确地整合到类中的原则。你应该在设计阶段考虑异常和错误恢复,而不是等开发已经完成。
阅读异常处理的整个系列(原文链接):
正确地使用异常意味着从各个角度来看待错误发生的情况——从抛出异常的类的角度,捕获异常的类的角度,以及需要处理异常的可怜虫。为了写出友好的程序,你必须从所有以上角度来考虑错误发生的情况。
考虑下面这个简单的异常处理类和抛出异常的方法:
package com.me.mypackage; public class ResourceLoadException extends Exception { public ResourceLoadException(String message) { super(message); } } ... public class ResourceLoader { public getResource(String name) throws ResourceLoadException { ... }
当 getResource()抛出一个ResourceNotFoundException异常时,实际上传达了3层信息:
l 异常的类型(示例中的ResourceNotFoundException)
l 发生异常的位置,(exception.printStackTrace())
l 其他异常信息(exception.getMessage())
每一条信息都有其受众。例外类型对于调用者是非常重要的,这样他就能捕获特定的异常而忽略其它。堆栈跟踪是提供给隔离或调试这个问题的开发者或技术支持人员的。而消息字符串对于需要对错误消息或错误日志进行解释的用户来说是最有意义的。
当你抛出一个异常,你应该确保所有收件人会收到有效的信息。这意味着你应该抛出一个异常正确的异常类,使调用者可以采取适当的纠正措施,你应该生成有用的诊断信息,以便用户可以理解发生了什么事。
编写有意义的throws语句
方法将抛出哪些类型的异常是类设计中的一个重要元素。在编写一类throw语句时,你应该从方法调用者而不是类自身的角度来考虑这个问题。什么样的异常是调用者能够正常处理的,什么样的异常是它只能抛给它的调用者或者用户?
将所有可能抛出的异常都进行声明是不合理的。这不仅是设计时的懒惰,而且暴漏了接口的部分实现,使你在将来修改实现时受到限制。例如,假设getResource方法可以从数据库、文件或远程服务器加载资源,具体取决于资源名称和ResourceLoader对象如何被初始化。如果你仅仅是将异常抛给调用者,那么getResource()可能抛出IOException,SQLException,RemoteException等等。但是所有这些异常对于调用者都是有用的吗?如果一个方法因为需要加载资源而调用getResource()时,它是不太可能有能力来正确地区分SQLException和IOException的。从调用者的角度来看,这两个异常都仅仅意味着资源无法加载。因此,只抛出一个ResourceLoadException的话可能更有意义。
关于一个方法究竟应该抛出多少种类型的异常并没有一个又快又好的标准,但一般而言越少越好。方法抛出的每一种异常,要么被捕获要么被调用者抛出,因此方法抛出的异常类型越多则越是难用。如果你抛出的异常类型太多,调用者可能会偷懒,只是catch--或者更糟,再次抛出。这些做法是危险的; 调用者应该对每种异常分别进行处理。抛出超过三种不同的异常一般意味着这样一个问题:该方法要么执行了太多不同的任务,它们应该拆开的;要么是偷懒地传递底层的异常,这些异常应该要么被映射到一个个更高级别的异常,要么在方法内部捕获并处理掉。
在编写throws语句时,对于每种你准备抛出的异常,你都应该问问自己:调用者将如何处理这个异常?调用者能否根据这个异常类型采取不同的处理行为?如果答案是否定的,那么该异常要么被捕获并由你的代码来处理,要么需要转换成另一种更贴切的异常类型。
让你的throws语句更稳定
抛出少量高级的异常类型而不是许多不同的低级的异常类型的做法还有一个显著优势,那就是可以防止方法的更改导致throws语句每次也要修改。
方法签名的改变往往引发调用者的修改。这些修改的影响范围小到轻微的烦恼达到灾难性的不便,取决于有多少类必须改变以及有多少不同的人或机构将使用相关类。throws语句是方法签名的一个重要组成部分,因此你应该小心以确保其稳定性。在方法的throws语句上添加一个新的异常意味着所有调用该方法的类都必须捕获这个新的异常或修改签名以抛出这个异常。显然,这种不稳定性是应当避免且代价昂贵的。避免这个问题的最好办法是阻止它——在一开始就应当指定抛出的异常与方法实际上的职能保持一致,而跟它的具体实现没有关系。在throws语句中应当尽量将相关的异常组织到一个对象结构中并包含在父异常类型中,而不是增加一个新的异常。 Java.io包中的IOException就是一个很好的例子。更具体的异常,比如EOFException,它被定义为IOException的子类,但java.io包中几乎所有的方法都定义为仅抛出IOException。
从一个共同的父类引出子类使得异常处理能够以更面向对象的方式进行。调用者可以仅捕获IOException 并在catch块中对所有IO相关的异常采用相同的处理方式。但是,如果调用者能够对某个特定的IO异常进行特殊处理,它可以首先捕获这种特定异常并进行正确的恢复。
特别是分组到包或应用程序异常类型一起由一个公共的父继承他们提高了方法的稳定性“ 抛出的条款。你可以在不影响现有方法的签名,这反过来又意味着,你不需要每次添加一个新的异常子类的时间来改变客户端代码在未来的版本中添加特定的异常类型的包。
尤其对于包或应用程序而言,通过子类化将不同的异常类型组织到一个共同的父类能够增强throws语句的稳定性。
不想抛出异常时如何处理
当你的代码遇到一个低级异常而其类型信息对于调用者来说意义不大时,将它转换成更有意义的异常。例如:
public class ResourceLoader { public getResource(String name) throws ResourceLoadException { try { // try to load the resource from the database ... } catch (SQLException e) { throw new ResourceLoadException(e.toString()): } }
现在,程序和用户都获得了他们所需要的信息。调用者得知该资源无法加载;由于ResourceLoadException更加匹配调用者的行为,因此获取应该提供一个更有用的异常类型而不是底层的SQLException。但是来自于底层异常的解释消息可以被保留,所以程序仍然可以向用户提供一个更具体的解释,来说明什么地方出了错。
ResourceLoadException提供了一个备用的构造方法来实现以上技术,它可以接收一个异常作为参数并添加一个错误消息。将一个异常包装成另一个对于在保留状态信息的同时管理复杂度方面提供了强大的技术(有时也称为异常链)。在本系列的第2部分中,我将提出一个具体的异常包装技术,它将正确的信息提供给所有三个利益相关方——调用者,调试问题的开发者以及遇到此异常的用户(译者注:见第二节)。
不要忘记用户
尽管应用程序并不关心你在异常信息中放置了哪些文本(应用程序永远不应查看异常消息内容并根据它来确定发生了什么),关心这些消息的是用户。错误消息应该对开发人员(这样他们就可以知道更多有关该错误的来源和原因)和用户(这样他们就知道为什么会运行程序失败)是有意义的。而"Bad index value"或者"Error in initialization"这样的错误消息是没有用的。
为了确保用户能够理解错误信息,你也应该考虑谁可能会成为用户。他们都使用和开发者一样的语言吗?如果不是,你应该使用一些本地化机制(比如资源包)来加载错误消息字符串或模板,使错误消息可以很容易地翻译成其他语言而不需要任何的文本编辑(译者注:见第三节)。
如果你的类将被本地化为其他语言,你应该避免在代码中硬编码英文文本字符串。事实上即便没有本地化的要求,出于许多理由,将错误消息字符串放置在外部的错误的目录或资源包中,然后在运行时再构建它们是一个好主意。比如说,有了程序可能抛出的所有异常的详细错误消息目录,在制作文档时很可能会轻松一些。在编写详细文档时可以以这个目录作为基础。
正确使用异常
适当的异常处理是非常重要的; 然而不幸的是,应用程序或类库生成和处理异常的方式是程序设计中最容易被忽视的一个方面。良好的异常处理方式不只是让你的程序更健壮,更易于维护;当故障事发生时,抛出的异常能够提供故障原因唯一线索。如果你的程序合理地使用了异常并产生了有意义的错误消息,用户就会少一些可能会变得困惑和懊恼(当你的程序出现故障且用户可能已经很生气的时候)。良好的异常处理也将让你的支持人员能够更好地理解问题并解决它。正确使用异常确保所有相关方——代码和人类一样 获得他们所需要的错误恢复信息。
二、使用异常链来保存调试信息
在本系列第一节,我围绕Java程序中异常的合理使用介绍了一些基本概念。在这篇文章中,我更深入地集中在一个特定的异常处理技术,称为异常链(有时也称为例外包装),它彰显了第一节所列出的原则。异常链可以让你将一个例外类型映射到另一个,这样一个方法就可以抛出与自身处于同一个抽象等级的异常,而并不需要丢弃重要的调试信息。
阅读异常处理的整个系列(原文链接):
异常是什么?
异常带有三个重要的信息:
1. 异常的类型——异常类
2. 异常发生的位置——堆栈跟踪
3. 上下文以及解释信息——错误消息以及其他状态信息
每个信息都相关于一个不同的阵营。软件实体关心异常类——JVM和调用者用它来确定如何处理异常的代码。另外两个信息与人相关——分析堆栈跟踪以调试问题的开发人员或者支持工程师,以及检查错误信息的用户或开发人员。每一方都必须接受有效地处理错误所需要的信息。
包装异常的第一次尝试
为了确保异常类首先是尽可能有用的,常用的手段是方法接到一类异常后立即重新抛出另一类异常。作为这种技术的一个例子,考虑下面的ResourceLoader类,应用程序可能使用它来加载诸如图形或音频文件之类的资源。loadResource() 的实现从数据库中获取资源,但ResourceLoader的规范并没有要求这样做,资源可以来自一个文件或一个远程服务器。
public class ResourceLoader { public loadResource(String resourceName) throws ResourceLoadException { Resource r; try { r = loadResourceFromDB(resourceName); } catch (SQLException e) { throw new ResourceLoadException("SQL Exception loading resource " + resourceName: " + e.toString()); } } }
loadResource的实现对于异常的使用还算不错。通过抛出ResourceLoadException而不是SQLException(或其他任何此实现抛出的异常),loadResource对调用者隐藏了实现,使得实现更容易更改而无需修改调用代码。此外,loadResource()抛出的ResourceLoadException直接相关于它所执行的任务:加载资源。低级别的SQLException和IOException与本方法的任务并无直接联系,因此对于调用者来说并不是太有用。此外,这种转换保留了异常的原始错误消息,因此用户能够知道为什么资源无法加载(也许是因为连接错误或不正确的用户名或密码),并可以采取纠正措施。
不要忘了堆栈跟踪
然而,上述loadResource实现在将SQLException包装为ResourceLoadException时丢掉了一个重要信息——异常起源的堆栈跟踪。当你创建并抛出一个新的ResourceLoadException对象时,新异常所包含的堆栈跟踪位于新异常被创建的地方,而不是原来的错误实际发生的地方。如果错误位于loadResourceFromDB()方法中或者它调用的其他方法,那么你无法确定错误的原始位置,因为堆栈跟踪已经丢失了。充其量,你只能猜测它可能是在哪里抛出的。
如果原始异常是从你没有源代码的代码中抛出的,比如供应商提供的类库或Java数据库连接(JDBC)驱动程序代码,情况会更糟。如果你向供应商提交问题报告,该公司可能会要求提供有关异常的位置信息。但由于堆栈跟踪已被丢弃,你所能告诉他们的仅仅是“你的库所调用的某处代码”。
要解决这个问题很简单——将初始异常作为新异常状态信息的一部分。你可以通过创建一个异常链的基类并简单地修改ResourceLoader和ResourceLoadException来实现。
ChainedException类
你可以使用下面的类,ChainedException,作为异常链的基类。它提供了一个构造方法,使用一个异常作为其中一个参数,并且覆盖了printStackTrace()方法来打印所有的堆栈跟踪。具体的异常类只需要扩展ChainedException并提供所需的构造方法即可。
public class ChainedException extends Exception { private Throwable cause = null; public ChainedException() { super(); } public ChainedException(String message) { super(message); } public ChainedException(String message, Throwable cause) { super(message); this.cause = cause; } public Throwable getCause() { return cause; } public void printStackTrace() { super.printStackTrace(); if (cause != null) { System.err.println("Caused by:"); cause.printStackTrace(); } } public void printStackTrace(java.io.PrintStream ps) { super.printStackTrace(ps); if (cause != null) { ps.println("Caused by:"); cause.printStackTrace(ps); } } public void printStackTrace(java.io.PrintWriter pw) { super.printStackTrace(pw); if (cause != null) { pw.println("Caused by:"); cause.printStackTrace(pw); } } }
将ChainedException整合到我们的例子中
修改ResourceLoadException以便使用ChainedException是很简单的;只需要继承ChainedException而不是Exception ,并提供适当的构造方法就可以了。如果你在应用程序中已经建立了异常类的层次结构,就像许多应用所做的那样,只要使你的异常基类继承ChainedException即可。(如果你不提供接受一个异常作为参数的构造方法,ChainedException的功能将与Exception完全一样,因此,你可以放心地将它作为应用中所有异常的基类使用,无论他们是否确实包装其他异常。)
public class ResourceLoadException extends ChainedException { public ResourceLoadException(String message) { super(message); } public ResourceLoadException(String message, Throwable cause) { super(message, cause); } }
修改loadResource()方法来正确地链接异常同样简单,只需为ResourceLoadException的构造方法提供合适的初始异常即可。
public class ResourceLoader { public loadResource(String resourceName) throws ResourceLoadException { Resource r; try { r = loadResourceFromDB(resourceName); } catch (SQLException e) { throw new ResourceLoadException("Unable to load resource " + resourceName, e); } } }
如上所示,ChainedException中的printStackTrace实现是相当原始的;你可以轻易地地改进,通过匹配每个堆栈跟踪的结尾行,只打印与外部追踪不同的地方。这将使堆栈跟踪更紧凑且更易于阅读。
如果我们像第一节所指出的那样编写合理的throws语句——使方法抛出与自身相同抽象等级的异常——那么异常在被传递时就有可能被包装很多次。例如,如果我们的应用程序是一个Web服务器,在加载图像时所遇到的IOException 可能首先被包装为ResourceException,然后,因为它要传递到调用堆栈,异常可能会进一步包装成InvalidReply异常。但是,由于所有的外部异常仍然包含内部异常,查看日志文件的开发者仍然能够得知问题的精确原因以及它被逐层传递时是如何处理的。
JDK中的异常链
在JDK 1.2中,有少数的异常类添加了一些有限的异常链的功能,比如java.lang.ClassNotFoundException。然而,它们都没有继承共同的链异常基类;所有支持链接的异常类都是在其内部直接实现了链接特性。
JDK 1.4继承了Throwable类,能够提供这里显示的ChainedException基类的所有功能。如果你正在为JDK 1.4或更高版本开发应用程序,你可以轻易地使用Throwable所提供的异常链功能。如果你的代码必须运行在JDK 1.2或1.3但最终会迁移到1.4,你现在可以使用ChainedException,当你迁移的时候只需要对你的异常类进行简单修改使它从继承ChainedException变为继承Exception。
养成使用异常链的习惯
异常链是一种有用的技术,它用于保存重要的错误恢复信息,同时允许方法在将异常传递到调用栈时将较低级的异常包装为更高级别的异常。它将内置在JDK 1.4版本的Throwable类中。对于还没有用上1.4的开发者,在此时对于你们几乎所有人,幸运的是你现在可以在程序中使用这里提供的ChainedException来轻松地构建异常链,并在你迁移到JDK 1.4时使用内置的支持轻易地完成合并。
三、易于使用的本地化信息目录
在本系列的第一节和第二节,我研究了关于如何更有效地使用异常以便于将错误恢复信息提交给需要它的相关方——其他Java类,开发者和用户 。在第三节,我介绍下经常被忽略的国际化问题和简化该过程的技术:利用消息目录来存储消息文本。
阅读异常处理的整个系列(原文链接):
第一节强调的是,如果两个不同的异常可能有不同的错误恢复过程,那么他们应该是不同的类——尽管它们可能是从同一个基类派生。你永远不想面临那种尝试使用消息文本来区分两种不同异常的情况。异常的消息文本仅仅是向消费者提供解释的,而不是代码。
不要手动建立错误消息
虽然大多数开发者都认为在程序中硬编码文本字符是一个坏主意,但是我们中的大多数人整体仍然在这样做。下面的代码片段说明了抛出异常的常见但是不好的技巧:
清单1. 手动生成错误消息
if (!file.exists()) { throw new ResourceException("Cannot find file " + file.getName()); }
尽管你可能每天都能看到这个结构,但是这种建立错误消息的方式仍然是糟糕的。解释性的错误文本根本不属于Java代码;它应从从外部源获取。当你要面向国外市场本地化此代码时会发生什么?有人将不得将所有的源代码梳理一遍来确认所有用于构建异常的文本字符。然后,翻译人员来翻译它们,接着你必须弄清楚如何维护相同代码针对多个市场的不同版本。并且每次发布的时候你都要把以上工作重复一遍。
即使你从来没有打算本地化,也有其他好的理由支持从代码中剥离错误消息文本:错误消息很容易与文档失去同步——开发者可能会更改了错误信息却忘了通知文档作者。同样,你可能想改变某种异常的所有错误消息的文字内容以使它们更加一致,但由于你必须把所有源码都梳理一遍来找到所有这些错误信息,因此你很容易会漏掉一个,从而导致错误消息不一致。
显然,源代码不是用于存放解释性消息的地方,实际上文档更应该是。事实上,开发者甚至不应该再写错误消息,而应该是写文档。你见过多少过除了作者以外其他人压根儿看不到的错误消息?只要错误消息存在于源代码中,它们就会被开发者所创建和使用而降低可用性。
使用MessageFormat来构造错误信息
Java为我们提供了一些工具使得国际化消息文本变得简单得多。java.text.MessageFormat类允许我们将文本错误信息参数化。MessageFormat类似于C语言中的printf函数但更加灵活,因为它允许格式化字符串中的参数重新排序。清单1中的以下版本要稍微好一些,因为它没有将错误消息文本隐藏在代码中间:
清单2. 使用MessageFormat构造错误信息
public static final String MSG_FILE_NOT_FOUND = "Cannot find file {1}"; ... public static String formatMessage(String messageString, Object arg0) { MessageFormat mf = new MessageFormat(messageString); Object[] args = new Object[1]; args[0] = arg0; return mf.format(args); } ... if (!file.exists()) { throw new ResourceException(formatMessage(MSG_FILE_NOT_FOUND, file.getName())); }
这个版本在几个地方比第一个更好:错误消息与代码(在某种程度上)分开,所以在对类进行本地化时很容易找到。将错误消息从代码中剥离减少了本地化时的工作量以及错误消息被遗漏的机会。它还鼓励开发人员在多个方法抛出相同错误时重用错误消息,因此程序错误消息可能更加一致。
但是,这种技术仍有较大的不足,因为它仍然把错误消息放在源码内部,这将引起许多问题。本地化者必须要访问源代码,在某些组织中这可能引起权限问题。此外,当这些类更新时多个程序代码分支必须被管理和并合以产生本地化版本。更好的做法是将错误消息完全放置在抛出异常的Java类外部,并组织成某种资源库,有时成为消息目录。
ResourceBundle类
消息目录并不是什么新鲜事物——最早的MacOS(1984)就提供了一个先进的资源管理器,能够独立于序代码存储用户界面项目。甚至在这之前,VMS(虚拟内存系统,大约1978年)操作系统为了简化本地化就有一个独立于程序而存储消息字符串的机制。建立自己的消息目录资源是很容易的,Java类库已经提供了一个有用的机制来管理本地化资源,就是ResourceBundle类。ResourceBundle类存储一个键值对集合,这些键名对于程序而言是静态的和确定的,而值则可以视不同的语言环境而更改。Java类库提供了几个辅助类来简化ResourceBundle的工作过程,即ListResourceBundle和PropertyResourceBundle。
通过创建实现一个或多个实现了ResourceBundle的类来构建资源包,并用一个特殊的命名约定来将具体的实现类与语言环境相关联。例如,假设你想创建一个名为com.foo.FooResources的资源包,并提供英语,法语和德语的实现。为了做到这一点,你可以创建一个名为com.foo.FooResources的默认实现类,然后其他语言环境的实现名为com.foo.FooResources_en(可能与默认设置相同),com.foo.FooResources_fr和 foo.FooResources_de。将来你还可以创建更多的本地化版本。ResourceBundle类将使用当前的语言环境找到最合适的资源包;如果找不到合适的本地化版本,它会使用默认版本。
为了防止由于键名称拼写错误而导致运行时错误,应该为这些按键名称提供具有一定的代表性意义名称;应该使用这些代表性名称而不是实际的键名。这样一来,如果你键入了错误的消息字符串的名字,你在编译时就会发现。(ResourceBundle的Javadoc页面犯了将字符串内嵌到键名称的错误,但你要努力在你的程序做的更好。)下面是使用资源包来实现如上代码的简单例子:
清单3. 使用资源包构造错误消息
public interface FooResourcesKeys { public static String MSG_FILE_NOT_FOUND = "MSG_FILE_NOT_FOUND"; public static String MSG_CANT_OPEN_FILE = "MSG_CANT_OPEN_FILE"; } public class FooResources extends ListResourceBundle implements FooResourcesKeys { public Object[][] getContents() { return contents; } static final Object[][] contents = { // Localize from here {MSG_FILE_NOT_FOUND, "Cannot find file {1}"}, {MSG_CANT_OPEN_FILE, "Cannot open file {1}"}, // Localize to here }; } public class MessageUtil { private static ResourceBundle myResources = ResourceBundle.getBundle("com.foo.FooResources"); private static String getMessageString(String messageKey) { return myResources.getString(messageKey); } public static String formatMessage(String messageKey) { MessageFormat mf = new MessageFormat(getMessageString(messageKey)); return mf.format(new Object[0]); } public static String formatMessage(String messageKey, Object arg0) { MessageFormat mf = new MessageFormat(getMessageString(messageKey)); Object[] args = new Object[1]; args[0] = arg0; return mf.format(args); } public static String formatMessage(String messageKey, Object arg0, Object arg1) { MessageFormat mf = new MessageFormat(getMessageString(messageKey)); Object[] args = new Object[2]; args[0] = arg0; args[1] = arg1; return mf.format(args); } // Include implementations of formatMessage() for as many arguments // as you need } public class SomeClass implements FooResourcesKeys { ... if (!file.exists()) { throw new ResourceException( MessageUtil.formatMessage(MSG_FILE_NOT_FOUND, file.getName())); } }
在上面的例子中,你需要为所有用到的错误消息分别在FooResourcesKeys放置在一个条目,并且在每个资源包实现(每个语言环境包一个)中为该键值放置一个对应的条目。如果你已经正确地命名了你的资源包,ResourceBundle就能按序查找,如果一个条目不在本地化包中它会同时搜索默认资源包。对于代码中每一个会抛出异常的地方,你都需要通过MessageUtil创建消息字符串,就想清单3中的SomeClass那样。
为了便于代码重用,你可以把这一技术扩展一下来支持多个资源包,每个组件一个资源包甚至每个package一个。在这种情况下,MessageUtil类会稍微复杂一些,通过一个额外的参数来识别给定的错误消息从哪个资源包中产生。
资源包也可以用于其他可被本地化的资源,如Swing中按钮或标签等组件所使用的文字,甚至是非文本音频文件或图像资源。
消息目录的隐藏福利
消息目录不仅简化了本地化的过程,也极大地简化了产品的多个本地化版本的维护。对于每个版本,本地化者只需将资源包的以前版本与当前的比较一下就能确切地知道本地化还需要做哪些工作。
使用消息目录来存储异常信息串提供了另一个隐藏福利。一旦你把所有异常消息放置在消息目录中,你现在就有了应用程序会抛出的所有的异常消息的完整列表。这为文档作者编写“故障排除”或“错误消息”章节提供了一个简单的起点。消息目录也很容易让文档作者跟踪资源包的变化,确保文档与软件保持同步。而且,由于错误信息更应该说文档而不是工程的功能,使用资源包对于文档撰写者获取资源包源文件时更加实用,这可能对所有相关人员都有好处都有好处。
创建有用的错误消息
与直观的建立错误消息的方式相比,使用消息目录保存错误消息字符串需要在前期做更多工作,但它在许多方面都有很多回报。Java类库中的ResourceBundle类可让你在应用程序中轻松地构建消息目录。消息目录技术不仅使得UI和程序逻辑之间的完全隔离,也使得软件的本地化和维护本地化版本更容易,并产生准确的反映程序功能的文档。作为未来的福利,由于ResourceBundle类可以让不仅仅是开发者的人参与错误消息的创造开发更加简单,因此错误消息很可能对用户更加有用。
(原文完)
为了练手,将清单3中的代码添加到eclipse中运行,感受一下它的运行过程。下面的代码对文件名及个别地方进行了少量修改,以便于理解。
ResourceBundleDemoStart.java
package ResourceBundleDemo; import java.io.File; import java.io.FileNotFoundException; /* * 工程信息 * @源码地址: http://www.javaworld.com/article/2075897/testing-debugging/exceptional-practices--part-3.html * @功能:演示使用ResourceBundle构造错误消息以及本地化的过程 * @结构:ResourceBundleDemoStart[起始点],MessageUtil,ExceptionMsgResourcesKeys,ExceptionMsgResources_ZN,ExceptionMsgResources_EN * @版本:20150816 */ /* * 类信息 * @功能:项目起始点 * @版本:20150816 */ public class ResourceBundleDemoStart implements ExceptionMsgResourcesKeys { public void run() { try { loadResource(); } catch (ResourceLoadException e) { // TODO Auto-generated catch block System.out.println("error occured when loadResource : "+ e.getMessage()); e.printStackTrace(); if (e.getCause() instanceof FileNotFoundException) { FileNotFoundException exception = (FileNotFoundException) e .getCause(); System.out.println("FileNotFoundException message : " + exception.getMessage()); } } } public void loadResource() throws ResourceLoadException { File file = new File("config.txt"); try { loadFile(file); } catch (FileNotFoundException e) { // TODO Auto-generated catch block //e.printStackTrace(); throw new ResourceLoadException("Unable to load resource " + file.getName(), e); } } private void loadFile(File file) throws FileNotFoundException { String exceptionMsg = MessageUtil.formatMessage(MSG_FILE_NOT_FOUND, file.getName()); throw new FileNotFoundException(exceptionMsg); } public static void main(String[] args) { // TODO Auto-generated method stub ResourceBundleDemoStart resourceBundleDemoStart = new ResourceBundleDemoStart(); resourceBundleDemoStart.run(); } }
ResourceLoadException.java
package ResourceBundleDemo; public class ResourceLoadException extends Exception { private Throwable cause = null; public ResourceLoadException() { super(); } public ResourceLoadException(String message) { super(message); } public ResourceLoadException(String message, Throwable cause) { super(message); this.cause = cause; } public Throwable getCause() { return cause; } }
MessageUtil.java
package ResourceBundleDemo; import java.text.MessageFormat; import java.util.ResourceBundle; /* * 类信息 * @功能:读取资源包并进行本地化,最终生成正确的错误消息 * @版本:20150816 */ public class MessageUtil { //private static ResourceBundle myResources = ResourceBundle.getBundle("ResourceBundleDemo.ExceptionMsgResources_EN"); private static ResourceBundle myResources = ResourceBundle.getBundle("ResourceBundleDemo.ExceptionMsgResources_ZN"); private static String getMessageString(String messageKey) { return myResources.getString(messageKey); } public static String formatMessage(String messageKey) { MessageFormat mf = new MessageFormat(getMessageString(messageKey)); return mf.format(new Object[0]); } public static String formatMessage(String messageKey, Object arg0) { MessageFormat mf = new MessageFormat(getMessageString(messageKey)); Object[] args = new Object[1]; args[0] = arg0; return mf.format(args); } public static String formatMessage(String messageKey, Object arg0, Object arg1) { MessageFormat mf = new MessageFormat(getMessageString(messageKey)); Object[] args = new Object[2]; args[0] = arg0; args[1] = arg1; return mf.format(args); } // Include implementations of formatMessage() for as many arguments // as you need }
ExceptionMsgResourcesKeys.java
package ResourceBundleDemo; /* * 类信息 * @功能:本接口规定错误消息的键名称以及结构 * @版本:20150816 */ public interface ExceptionMsgResourcesKeys { public static String MSG_FILE_NOT_FOUND = "MSG_FILE_NOT_FOUND"; public static String MSG_CANT_OPEN_FILE = "MSG_CANT_OPEN_FILE"; }
ExceptionMsgResources_ZN.java
package ResourceBundleDemo; import java.util.ListResourceBundle; /* * 类信息 * @功能:错误消息的中文资源包 * @版本:20150816 */ public class ExceptionMsgResources_ZN extends ListResourceBundle implements ExceptionMsgResourcesKeys{ @Override protected Object[][] getContents() { // TODO Auto-generated method stub return contents; } static final Object[][] contents = { // Localize from here {MSG_FILE_NOT_FOUND, "找不到文件 {0}"},//原文这里有误 {MSG_CANT_OPEN_FILE, "打不开文件 {0}"},//原文这里有误 // Localize to here }; }
ExceptionMsgResources_EN.java
package ResourceBundleDemo; import java.util.ListResourceBundle; /* * 类信息 * @功能:错误消息的英文资源包 * @版本:20150816 */ public class ExceptionMsgResources_EN extends ListResourceBundle implements ExceptionMsgResourcesKeys{ @Override protected Object[][] getContents() { // TODO Auto-generated method stub return contents; } static final Object[][] contents = { // Localize from here {MSG_FILE_NOT_FOUND, "Cannot find file {0}"},//原文这里有误 {MSG_CANT_OPEN_FILE, "Cannot open file {0}"},//原文这里有误 // Localize to here }; }
使用中文包本地化时,程序输出:
error occured when loadResource : Unable to load resource config.txt ResourceBundleDemo.ResourceLoadException: Unable to load resource config.txt at ResourceBundleDemo.ResourceBundleDemoStart.loadResource(ResourceBundleDemoStart.java:45) at ResourceBundleDemo.ResourceBundleDemoStart.run(ResourceBundleDemoStart.java:24) at ResourceBundleDemo.ResourceBundleDemoStart.main(ResourceBundleDemoStart.java:59) Caused by: java.io.FileNotFoundException: 找不到文件 : config.txt at ResourceBundleDemo.ResourceBundleDemoStart.loadFile(ResourceBundleDemoStart.java:53) at ResourceBundleDemo.ResourceBundleDemoStart.loadResource(ResourceBundleDemoStart.java:41) ... 2 more FileNotFoundException message : 找不到文件 : config.txt
使用英文包本地化时,程序输出:
error occured when loadResource : Unable to load resource config.txt ResourceBundleDemo.ResourceLoadException: Unable to load resource config.txt at ResourceBundleDemo.ResourceBundleDemoStart.loadResource(ResourceBundleDemoStart.java:45) at ResourceBundleDemo.ResourceBundleDemoStart.run(ResourceBundleDemoStart.java:24) at ResourceBundleDemo.ResourceBundleDemoStart.main(ResourceBundleDemoStart.java:59) Caused by: java.io.FileNotFoundException: Cannot find file : config.txt at ResourceBundleDemo.ResourceBundleDemoStart.loadFile(ResourceBundleDemoStart.java:53) at ResourceBundleDemo.ResourceBundleDemoStart.loadResource(ResourceBundleDemoStart.java:41) ... 2 more detailed error message : Cannot find file : config.txt
注意:在ResourceLoadException.java中,并没有像原文中那样对printStackTrace()方法进行重写,因为测试发现在jdk1.8中Exception类的printStackTrace()方法会自动将cause的跟踪堆栈打印出。