故障安全异常处理

时间:2020-01-09 10:35:58  来源:igfitidea点击:

必须确保异常处理代码是故障安全的。要记住的一个重要规则是

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功能使实现故障安全异常处理更加容易。