原文请看:Beware the dangers of generic Exceptions
捕获和抛掷一般的异常会让你很快在不知不觉中陷入困境。
概要
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有明显的不同。在所有其它的情况下,异常应该(至少)被记入日志,其内容用堆栈跟踪记录很合适。
未完待续。