Java异常处理最佳实践

在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花

费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都

会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。本文给出几个

被很多团队使用的异常处理 佳实践。

1.  finally 块中清理资源或者使用 try-with-resource 语句

当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在 try块的 后关闭资源。

问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资

源可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而

且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的 后部分。结果就是,你

并没有关闭资源。

所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。

1.1 使用 finally 代码块

与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功

执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资

源。

1 public void doNotCloseResourceInTry() {

2 FileInputStream inputStream = null;

3 try {

4 File file = new File("./tmp.txt");

5 inputStream = new FileInputStream(file);

6 // use the inputStream to read a file

7 // do NOT do this

8 inputStream.close();

9 } catch (FileNotFoundException e) {

10 log.error(e);

11 } catch (IOException e) {

12 log.error(e);

13 }

14 }

1 public void closeResourceInFinally() {

2 FileInputStream inputStream = null;

3 try {

4 File file = new File("./tmp.txt");

5 inputStream = new FileInputStream(file);

6 // use the inputStream to read a file

7 } catch (FileNotFoundException e) {

8 log.error(e);

9 } finally {

10 if (inputStream != null) {

11 try {

12 inputStream.close();

13 } catch (IOException e) {

14 log.error(e);

15 }

16 }

17 }

18 }

1.2 Java 7  try-with-resource 语法

如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这

个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。

2. 优先明确的异常

你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异

常。

因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的

处理异常并且避免额外的检查。因此,总是尝试寻找 适合你的异常事件的类,例如,抛出一个

NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。

3. 对异常进行文档说明

当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可

以更好地避免或处理异常。

 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。

4. 使用描述性消息抛出异常

在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,

都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。

但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需

要用一到两句话描述即可。

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 } catch (FileNotFoundException e) {

6 log.error(e);

7 } catch (IOException e) {

8 log.error(e);

9 }

10 }

11

1 public void doNotDoThis() throws Exception {

2 ...

3 }

4 public void doThis() throws NumberFormatException {

5 ...

6 }

7

1 public void doSomething(String input) throws MyBusinessException {

2 ...

3 }如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信

息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被

java.lang.Long 类的构造函数抛出。

5. 优先捕获最具体的异常

大多数 IDE 都可以帮助你实现这个 佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问

的代码块。

但问题在于,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获

IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException  catch

块,因为它是 IllegalArgumentException 的子类。总是优先捕获 具体的异常类,并将不太具体的

catch 块添加到列表的末尾。你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。 第一个

catch 块处理所有 NumberFormatException 异常,第二个处理所有非

NumberFormatException 异常的IllegalArgumentException 异常。

6. 不要捕获 Throwable

Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你

永远不应该这样做!

如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,

指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError

。两者都是由应用程序控制之外的情况引起的,无法处理。

所以, 好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。

1 try {

2 new Long("xyz");

3 } catch (NumberFormatException e) {

4 log.error(e);

5 }

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 }

1 public void doNotCatchThrowable() {

2 try {

3 // do something

4 } catch (Throwable t) {

5 // don't do this!

6 }

7 }

7. 不要忽略异常

很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。

但现实是经常会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出

的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。合理的做法是至少要记录

异常的信息。

8. 不要记录并抛出异常

这可能是本文中 常被忽略的 佳实践。可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次

抛出的逻辑。如下:

这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下:

如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装

为自定义异常。

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 }

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.NumberFormatExcep tion:

For input string: "xyz"

2 Exception in thread "main" java.lang.NumberFormatException: For input str ing:

"xyz"

3 at java.lang.NumberFormatException.forInputString(NumberFormatException.j

ava: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(TestEx

ceptionHandling.java:63)

7 at

com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:5

8)因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。

9. 包装异常时不要抛弃原始的异常

捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针

对的异常处理。

在你这样做时,请确保将原始异常设置为原因(注:参考下方代码

NumberFormatException e 中的原始异常 e )。Exception 类提供了特殊的

构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将

会使分析导致异常的异常事件变得困难。

10. 不要使用异常控制程序的流程

不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处

理,这是非常不好的习惯,会严重影响应用的性能。

11. 使用标准异常

如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异

常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创

建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。

12. 异常会影响性能

异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一

个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。

仅在异常情况下使用异常;

在可恢复的异常情况下使用异常;尽管使用异常有利于 Java 开发,但是在应用中 好不要捕获太多

的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰

到好处的信息。

public void wrapException(String input) throws MyBusinessException {

3 // do something

4 } catch (NumberFormatException e) {

5 throw new MyBusinessException("A message that describes the error.", e);

6 }

7 }

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 }

813. 总结

综上所述,当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代

码的可读性或者 API 的可用性。

异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要

制定出一个 佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。

你可能感兴趣的:(java,jvm,servlet)