故障安全异常处理
必须确保异常处理代码是故障安全的。要记住的一个重要规则是
The last exception thrown in a try-catch-finally block is the exception that will be propagated up the call stack. All earlier exceptions will disappear.
如果从catch或者finally块内部抛出异常,则此异常可能会隐藏该块捕获的异常。尝试确定错误原因时,这会产生误导。
以下是非故障安全异常处理的经典示例:
InputStream input = null; try{ input = new FileInputStream("myFile.txt"); //do something with the stream } catch(IOException e){ throw new WrapperException(e); } finally { try{ input.close(); } catch(IOException e){ throw new WrapperException(e); } }
如果FileInputStream构造函数抛出FileNotFoundException,我们认为会发生什么?
首先执行catch块。该块仅引发包装在WrapperException中的异常。
其次,将执行finally块,该块将关闭输入流。但是,由于FileInputStream构造函数引发了FileNotFoundException,因此"输入"引用将为null。结果将是从finally块引发的NullPointerException。 NullPointerException不会被finally块的catch(IOException e)子句捕获,因此会在调用堆栈中传播。从catch块抛出的WrapperException将会消失!
处理这种情况的正确方法是,在调用任何方法之前,先检查在try块内分配的引用是否为空。看起来是这样的:
InputStream input = null; try{ input = new FileInputStream("myFile.txt"); //do something with the stream } catch(IOException e){ //first catch block throw new WrapperException(e); } finally { try{ if(input != null) input.close(); } catch(IOException e){ //second catch block throw new WrapperException(e); } }
但是,即使这种异常处理也有问题。假设文件存在,因此"输入"引用现在指向有效的FileInputStream。我们还假装在处理输入流时引发了异常。该异常捕获在catch(IOException e)块中。然后将其包装并重新扔出。在将包装好的异常传播到调用堆栈之前,将执行finally子句。如果input.close()调用失败,并且抛出IOException,则将其捕获,包装和重新抛出。但是,当从finally子句中引发包装的异常时,将从第一个catch块引发的包装的异常再次被忘记。它消失了。只有从第二个catch块抛出的异常才沿调用堆栈传播。
如我们所见,故障安全异常处理并不总是那么简单。 InputStream处理示例甚至不是我们遇到的最复杂的示例。 JDBC中的事务具有更多的错误可能性。尝试提交,然后回滚以及最后尝试关闭连接时,可能会发生异常。所有这些可能的异常都应由异常处理代码处理,因此它们都不会使抛出的第一个异常消失。一种方法是确保最后引发的异常包含所有先前引发的异常。这样,开发人员就可以使用它们来调查错误原因。这就是我们的Java持久性API先生Persister实施事务异常处理的方式。
顺便说一下,Java 7中的try-with-resources功能使实现故障安全异常处理更加容易。