概要
Java提供了丰富的异常处理框架,但是尽管它很丰富和简单易用,却发现许多程序员很容易忽略它。这篇文章探究了抛掷、捕获和忽略普通异常的风险,并且提出在面对一个综合的大型软件项目时如何处理复杂异常的最佳方案。(2003年10月3日 By Paul Philion)
在最近的工作的一个软件项目中,我发现了一段处理资源清理工作的方法的代码。因为它有许多不同的调用形式,所以它会潜在的抛出6个不同的异常。(编写这段代码的)最初的程序员试图简化代码(或者是想节省输入),声明这个方法将抛出Exception异常,而不是那六个不同的异常。这使得调用代码被封装在捕获Exception的try/catch块中。他决定那样做是因为这些代码是为资源清理的目的,失败的情况并不重要,所以catch块为空而不管系统以何种方式关闭。
很明显,这不是最佳的编程实践。除了在原始代码第三行有一点逻辑问题之外,别的没有什么明显的错误。
程序清单1:原始的清理代码
private void cleanupConnections() throws ExceptionOne, ExceptionTwo {
for (int i = 0; i < connections.length; i++) {
connection[i].release(); // Throws ExceptionOne, ExceptionTwo
connection[i] = null;
}
connections = null;
}
protected abstract void cleanupFiles() throws ExceptionThree, ExceptionFour;
protected abstract void removeListeners() throws ExceptionFive, ExceptionSix;
public void cleanupEverything() throws Exception {
cleanupConnections();
cleanupFiles();
removeListeners();
}
public void done() {
try {
doStuff();
cleanupEverything();
doMoreStuff();
}catch (Exception e) {}
}
在其它地方的某段代码中,数组connections在第一个连接被建立时初始化。但是如果没有一个被建立,那么connections将是空的。所以在某种情况下,语句connections[i].release()的调用结果将会引发一个NullPointerException异常。这是一个相当容易修复的问题。仅仅添加一个检查语句“connections!=null”即可。
然而,有一个异常永远都不会被报告,它先被cleanupConnections()抛出,接着又被cleanupEverything()抛出,最后在done()中被捕获。done()方法的异常处理没有做任何事情,甚至没有日志记录。因为cleanupEverything()仅通过done()被调用,这个异常因此永远不会被看到。所以代码也永远不会被修复。
这样的话,假设现在cleanupConnections()执行失败,那么cleanupFiles()和removeListeners()方法永远都不会被调用(资源也就永远不会释放),并且doMoreStuff()也从不会被调用,这样在done()中的最后处理永远不会完成。为了使事情更糟,在系统关闭时done()不被调用,相反在完成每个事务时被调用,因而资源泄漏会出现在每个事务中。
问题很明显,主要是:错误没有被报告和资源泄漏。但是代码本身似乎没有错误,并且从代码编写方式中,这个问题增加了跟踪的难度。然而,通过应用一些简单的指导原则,这个问题就会被找出和修复:
不要忽略异常
不要捕获泛型的Exceptions
不要抛掷泛型Exceptions
不要忽略异常
在代码清单1中最明显的问题是在程序中的一个错误被完全忽略。一个非预期的异常(异常天生就是非预期的)被抛出,并且代码也没有打算处理那个异常。这个异常甚至没有被报告因为代码假设预期的异常并不重要。
在大多数情况下,一个异常至少应该被记录。有几个日志包(请看“Logging Exceptions”)可以记录系统错误和对系统没有重大影响的异常。
大多数日志系统也允许栈记录被打印,这样就提供了在哪和为什么会发生异常的有价值的信息。最后,因为一般情况下日志都被写到文件中,所以一条异常记录能被查看和分析。你可以看一下在“Logging Exceptions的代码清单11中有一个记录堆栈跟踪的例子。
在某些特殊情况下,日志异常并不是关键的,比如在finally子句中的资源清理是其中之一。
finally中的异常(Exceptions)
在下面的代码清单2中,需要从一个文件中读取数据。无论在读数据时是否发生异常文件都需要关闭,所以close()方法被放在finally子句中。但是如果在关闭文件时再发生错误,那就无能为力了。
代码清单2:
public void loadFile(String fileName) throws IOException {
InputStream in = null;
try {
in = new FileInputStream(fileName);
readSomeData(in);
}finally {
if (in != null) {
try {
in.close();
}catch(IOException ioe) {
// Ignored
}
}
}
}
注意如果由于I/O的问题而使数据装入失败,那么对loadFile()方法的调用仍然会抛出一个IOException异常。另外一点,即使异常在close()中被忽略,代码的声明(在注释语句行)也使它对任何使用它的人清晰易读。你可以应用这种方式去清理任何I/O流、关闭socket和JDBC连接等等。
在忽略异常时关键是要确保在要忽略的try/catch块只有一个方法(这样别的方法才可能在其之外被调用)并且有一个指定的异常被捕获。这种特殊的情况与捕获一个普通的Exception有明显的不同。在所有其它的情况下,异常应该(至少)被记入日志,其内容用堆栈跟踪记录很合适。
不要捕获泛型异常
在复杂的软件中,经常会有一些特定的代码块执行时会抛出多种不同异常的方法。动态装入一个类和实例化一个对象都可能会产生几个不同的异常,包括ClassNotFoundException, InstantiationException, IllegalAccessException, 和 ClassCastException。
一个繁忙的程序员在遇到这种情况时可能简单的把方法调用包在一个只会捕获泛型异常Exception的try/catch块,而不是添加四个不同的catch块到try块后面(看下面的代码清单3)。这看起来似乎无可置否,却会产生一些无意识的副面效果。例如,如果className()是null,那么Class.forName()将会抛出一个NullPointerException异常并在这个方法中被捕获。在这种情况下,catch块将捕获此异常虽然它从没打算去捕获这样一个异常,只是因为NullPointerException是RuntimeException的一个子类,而且RuntimeException又是Exception的一个子类。所以一个普通的catch(Exception e)将会捕获所有RuntimeException的子类,包括NullPointerException, IndexOutOfBoundsException, 和ArrayStoreException。通常,一个程序员并不打算去捕获这些异常。
在代码清单3中,null的className会导致一个NullPointerException异常产生,它告诉在调用的方法中类名无效。
代码清单3
public SomeInterface buildInstance(String className) {
SomeInterface impl = null;
try {
Class clazz = Class.forName(className);
impl = (SomeInterface)clazz.newInstance();
}catch (Exception e) {
log.error("Error creating class: " + className);
}
return impl;
}
另一个使用泛型捕获子句的结果是限制日志记录,因为catch不知道到底那一个特殊的异常被捕获。有些程序员在面对这种问题的时候,采取添加检测的手段去查看异常的类型(代码清单4),而这正好与使用catch块的目的相背离。
代码清单4
catch (Exception e) {
if (e instanceof ClassNotFoundException) {
log.error("Invalid class name: " + className + ", " + e.toString());
}else{
log.error("Cannot create class: " + className + ", " + e.toString());
}
}
代码清单5提供一种完整的捕获特殊异常的例子,一些程序员可能会对它感趣。操作符instanceof不是必须的因为这个特殊的异常自会被捕获。每一个被检查的异常(ClassNotFoundException, InstantiationException, IllegalAccessException) 会被捕获和处理。对于一个类装入正确,但是却没有实现SomeInterface接口这种特殊情况会产生一个ClassCastException异常,这个异常也会被查证。
代码清单5
public SomeInterface buildInstance(String className) {
SomeInterface impl = null;
try {
Class clazz = Class.forName(className);
impl = (SomeInterface)clazz.newInstance();
}catch (ClassNotFoundException e) {
log.error("Invalid class name: " + className + ", " + e.toString());
}catch (InstantiationException e) {
log.error("Cannot create class: " + className + ", " + e.toString());
}catch (IllegalAccessException e) {
log.error("Cannot create class: " + className + ", " + e.toString());
}catch (ClassCastException e) {
log.error("Invalid class type, " + className + " does not implement " + SomeInterface.class.getName());
}
return impl;
}
在某些情况下,更好的方法是重新抛出一个已知的异常(或者叫创建一个新的异常)而不是试图去在当前这个方法中处理。这允许调用方法通过放置这个异常到一个已知的上下文中去处理这种错误情形。
下面的代码清单6提供了一个buildInterface()方法的替换版本。如果在装入和实例化类时发生问题,这个版本会抛出一个ClassNotFoundException异常。在这个例子中,调用方法会确保得到一个正确的实例化对象或者是一个异常。这样调用方法就不需要去检查返回的对象是否为空了。
注意这个例子使用了Java1.4的方法来创建一个已经被另外的异常封装的新的异常,以便保存原始的堆栈跟踪信息。否则,堆栈跟踪将指明方法buildInstance()是引起异常的源,而不是潜在的由newInstance()抛出的异常。
代码清单6
public SomeInterface buildInstance(String className)throws ClassNotFoundException {
try {
Class clazz = Class.forName(className);
return (SomeInterface)clazz.newInstance();
}catch (ClassNotFoundException e) {
log.error("Invalid class name: " + className + ", " + e.toString());
throw e;
}catch (InstantiationException e) {
throw new ClassNotFoundException("Cannot create class: " + className, e);
}catch (IllegalAccessException e) {
throw new ClassNotFoundException("Cannot create class: " + className, e);
}catch (ClassCastException e) {
throw new ClassNotFoundException(className + " does not implement " + SomeInterface.class.getName(), e);
}
}
在有些情况下,这段代码可能无法从某种错误状态恢复,这时,捕获一个特殊的异常以使代码能指出某种状态是否是可恢复的就变得很重要了。请试着以这种观点去看代码清单6中类实例化的例子。
在代码清单7中,如果className无效,程序会返回一个缺省的对象,并且抛出一个异常以指明非法的操作,比如错误的转型或访问权限不够。
注意:IllegalClassException是一系列的异常类,为示范的目的而在这提及(译注:并不是Java标准库所带)。
代码清单7
public SomeInterface buildInstance(String className)
throws IllegalClassException {
SomeInterface impl = null;
try {
Class clazz = Class.forName(className);
return (SomeInterface)clazz.newInstance();
}catch (ClassNotFoundException e) {
log.warn("Invalid class name: " + className + ", using default");
}catch (InstantiationException e) {
log.warn("Invalid class name: " + className + ", using default");
}catch (IllegalAccessException e) {
throw new IllegalClassException("Cannot create class: " + className, e);
}catch (ClassCastException e) {
throw new IllegalClassException(className + " does not implement " + SomeInterface.class.getName(), e);
}
if (impl == null) {
impl = new DefaultImplemantation();
}
return impl;
}