Java - 9个处理异常的最佳准则

其实工作这么久了一直都没搞清楚到底如何来处理异常,偶然看到一篇外文感觉还不错,便把它翻译了下来,原文链接位于本文末尾处。

 

  在java中处理异常并不是一件简单的事,不止初学者觉得它难以理解甚至连有经验的开发者也会花费几个小时来讨论某个异常应该抛出还是处理掉。

  这就是为何大多数开发团队都拥有自己的规范来指明如何使用它们,如果你刚来到一个新的团队,你可能会发现新团队的准则与你之前遵循的大有不同。

  尽管如此,这里还是有几条最佳准则被大多数团队所遵循。这里有9条准则可以帮助你提高处理异常的水平。

  1、在Finally块中清理资源或者使用Try-With-Resource语句

  在try块中使用资源的情况在开发中会经常碰到,比如一个InputStream,使用它之后你需要将它关闭。在这种情况下经常会看到在try块中去关闭资源的错误。

复制代码
 1 public void doNotCloseResourceInTry() {
 2     FileInputStream inputStream = null;
 3     try {
 4         File file = new File("./tmp.txt");
 5         inputStream = new FileInputStream(file);
 6         
 7         // use the inputStream to read a file
 8         
 9         // do NOT do this
10         inputStream.close();
11     } catch (FileNotFoundException e) {
12         log.error(e);
13     } catch (IOException e) {
14         log.error(e);
15     }
16 }
复制代码

  这样写在没有异常抛出的情况下似乎运行得非常溜,所有在try块下的语句都会被正常执行,且资源都会被关闭。

  但使用try块是有它的原因的,你调用的一个或者多个方法可能会抛出异常,或者你自己主动抛出异常,这意味着try块中的语句可能会无法完整的执行,最终导致资源没有关闭。

 

使用Finally块

  与try块不同的是——finally块中的语句总是会被执行,无论是try块中的语句成功执行还是你在catch块中处理了一个异常。因此所有开启的资源都能够确保被关闭。

 

复制代码
 1 public void closeResourceInFinally() {
 2     FileInputStream inputStream = null;
 3     try {
 4         File file = new File("./tmp.txt");
 5         inputStream = new FileInputStream(file);
 6         
 7         // use the inputStream to read a file
 8         
 9     } catch (FileNotFoundException e) {
10         log.error(e);
11     } finally {
12         if (inputStream != null) {
13             try {
14                 inputStream.close();
15             } catch (IOException e) {
16                 log.error(e);
17             }
18         }
19     }
20 }
复制代码

 

 Java 7的Try-With-Resource语句

  还有一种选择便是使用try-with-resource,与之相关的详情在我的另一边文章——introduction to Java exception handling中有介绍。

  如果你的资源实现了AutoCloseable接口的话你便可以使用try-with-resource语句,这也是大多数Java标准资源的做法,如果你在try-with-resource语句中声明了一个资源,它将会在try块中的语句执行后或者将异常处理后自动关闭。

复制代码
 1 public void automaticallyCloseResource() {
 2     File file = new File("./tmp.txt");
 3     try (FileInputStream inputStream = new FileInputStream(file);) {
 4         // use the inputStream to read a file
 5         
 6     } catch (FileNotFoundException e) {
 7         log.error(e);
 8     } catch (IOException e) {
 9         log.error(e);
10     }
11 }
复制代码

 

 

2、优先使用更明确的异常

  抛出的异常越明确越好,你要想着你一个不了解你的代码的同事或者你在几个月后,需要调用你的方法并且处理异常。

  因此要确保提供尽可能多的信息,使你的API更容易被理解,使得该方法的调用者能更好的处理异常且避免额外的检查。

  所以,应该寻找与你的异常事件最贴切的类,例如:抛出一个NumberFormatException而不是IllegalArgumentException(译者注:这句话缺少上下文,不明白作者的意思)。且避免抛出不明确的异常。

1 public void doNotDoThis() throws Exception { ... }
2     
3 public void doThis() throws NumberFormatException { ... }

 

 

3、在文档里记录你的异常

  每当你在方法签名处指定一个异常,都应该同时将它记录到Javadoc里边。

  这与前一条准则的目的是一样的:给方法调用者提供尽可能多的信息,让他可以避免触发异常或者方便的他处理异常。

  所以确保要在Javadoc里边添加@throws声明并且描述什么样的情况会导致异常。

复制代码
1 /**
2 * This method does something extremely useful ...
3 *
4 * @param input
5 * @throws MyBusinessException if ... happens
6 */
7 public void doSomething(String input) throws MyBusinessException { ... }
复制代码

 

 4、将异常与它的描述信息一并抛出

  这条准则的想法与前两条是相同的,但这次你不是给你的方法调用者提供信息,当这个异常信息被打印到日志文件或者反馈到你的监视工具时,它要能被每个想要了解发生了什么的人理解。

  因此,我们应该尽可能精确的描述问题并且提供更接地气的消息来让他人理解发生了什么异常。

  别误会我的意思,你没必因此写上一大段话,但你应该用一两句话简明扼要的解释一下异常的原因。以帮助你的运营团队了解发生了什么问题,同时这也会使你更容易分析问题原因。

  如果你抛出一个明确的异常,它的类名很可能已经描述了这是怎样一个错误了,所以你不需要提供大量格外的信息,NumberFormatException就是一个很好的例子,当你给java.lang.Long的构造函数提供一个错误格式的String类型参数时便会抛出NumberFormatException。

1 try {
2     new Long("xyz");
3 } catch (NumberFormatException e) {
4     log.error(e);
5 }

 

  NumberFormatException的类名已经告诉你这是什么类型的问题了,它的异常消息只需要指明导致这个问题的输入字符串,如果异常类的名称不能达其意,你需要在异常消息中提供必要的信息。

1 17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"

 

 

5、优先捕获更明确的异常

  大部分IDE都会帮你遵循这条准则,当你把没那么明确的异常放在前面的时候他们会提示存在无法到达的代码块。

  这是因为只有第一个匹配到的catch块才会被执行,所以如果你先捕获IllegalFormatException,你将永远无法到达处理NumberFormatException的catch块,因为NumberFormatException是IllegalArgumentException的子类。

 

  所以应优先捕获明确的异常,将没那么明确的catch块放在后边。

 

  如下代码片段中的try-catch语句。第一个catch块处理所有NumberFormatException,第二个则处理所有非NumberFormatException的IllegalArgumentException。

复制代码
1 public void catchMostSpecificExceptionFirst() {
2     try {
3         doSomething("A message");
4     } catch (NumberFormatException e) {
5         log.error(e);
6     } catch (IllegalArgumentException e) {
7         log.error(e)
8     }
9 }
复制代码

 

 

 6、不要捕获Throwable

  Throwable是所有异常(Exception)和错误(Error)的父类,虽然它能在catch从句中使用,但永远都不要这样做!

  如果你在catch从句中使用了Throwable,它将不仅捕获所有异常,它还将捕获所有错误,错误是由JVM抛出的,用来表明不打算让应用来处理的严重错误。

  OutOfMemoryError和StackOverflowError便是典型的例子,它们都是由于一些超出应用处理范围的情况导致的。

复制代码
1 public void doNotCatchThrowable() {
2     try {
3         // do something
4     } catch (Throwable t) {
5         // don't do this!
6     }
7 }
复制代码

 

 

7、别忽略异常

  你是否曾经分析过一份不完整的bug报告?

  这通常是由于忽略异常导致的,这开发者大概很确定这里永远都不会抛出异常并加了一个不处理且不打印日志的catch块,当你找到这个块时,甚至很可能发现这么一句著名的注释——“This will never happen”

复制代码
1 public void doNotIgnoreExceptions() {
2     try {
3         // do something
4     } catch (NumberFormatException e) {
5         // this will never happen
6     }
7 }
复制代码

 

  好吧,你可能正在分析一个不可能发生的问题。

  所以,请不要忽略异常,你不知道代码在将来会如何变动,可能有人会将防止该异常事件的校验移除掉且没有意识到这会产生问题,或者抛出异常的这段代码改变了,相同的类现在变成抛出多个异常,而调用它的代码没有预防所有的异常。

 

  你至少要将日志打印出来告诉别人这里发生了异常,方便别人来检查。

复制代码
1 public void logAnException() {
2     try {
3         // do something
4     } catch (NumberFormatException e) {
5         log.error("This should never happen: " + e);
6     }
7 }
复制代码

 

 

 8、不要打印异常日志的同时将其抛出

  这可能是本文当中最常被忽视一条准则,你会在很多代码片段中甚至库中发现一个异常被捕获打印日志后被重新抛出。

复制代码
1 try {
2     new Long("xyz");
3 } catch (NumberFormatException e) {
4     log.error(e);
5     throw e;
6 }
复制代码

 

   这样做可能确实是直观的看到了异常日志,然后将异常重新抛出,所以调用者也能正确的处理异常,但这样做会使一个异常打印多个异常信息。

复制代码
1 17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
2 Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
3     at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
4     at java.lang.Long.parseLong(Long.java:589)
5     at java.lang.Long.(Long.java:965)
6     at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
7     at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
复制代码

 

  且额外的消息并没有提供任何有用的信息,根据第4条准则,异常消息应该描述异常事件,堆栈信息告诉你异常抛出的所在类、方法、行数。

  如果你需要添加额外的信息,你应该将异常捕获并将其包在你的自定义异常中,但要确保遵循第9条准则。

 

复制代码
1 public void wrapException(String input) throws MyBusinessException {
2     try {
3         // do something
4     } catch (NumberFormatException e) {
5         throw new MyBusinessException("A message that describes the error.", e);
6     }
7 }
复制代码

 

  所以只有在你想要处理某个异常的时候才应该去捕获它,否则在方法签名处声明抛出该异常让调用者去关注它就好了。

 

9、包裹某个异常的同时不要丢弃它原本的信息

  有时候我们需要捕获一个标准异常并用自定义异常包裹住它,一个典型的例子便是比如某个应用或者框架的指明业务异常,它允许你添加额外的信息,你也可以实现特别的异常处理方法。

  当你这么做的时候,要确保将原本的异常作为原因设置到自定义异常里面,Exception类提供指定的构造方法可以接收Throwable类的对象作为参数,否则你将会丢失堆栈信息和原异常的消息,这将会令异常分析变得什么的困难。

复制代码
1 public void wrapException(String input) throws MyBusinessException {
2     try {
3         // do something
4     } catch (NumberFormatException e) {
5         throw new MyBusinessException("A message that describes the error.", e);
6     }
7 }
复制代码

 

 

总结

  如你所见,当你捕获或者抛出异常的时候你需要考虑很多事情,总的来说这些准则都是为了提高代码的可读性,API的可用性。

  异常往往既是是错误处理机制也是沟通媒介,因此,你应该与你的同事一起讨论这些准则,使每个人都理解这些常规的概念,并以相同的风格在实践中应用它们。

展开阅读全文

没有更多推荐了,返回首页