Java 中处理 Exception 的几种实践

概述

本文总结了一些在 Java 开发中的异常处理的最佳实践,避免走弯路,主要内容如下

  1. 在 finally 块中清理资源或者使用try-with-resource语句
  2. 指定具体的异常
  3. 对异常进行文档说明
  4. 抛出异常的时候包含描述信息
  5. 首先捕获最具体的异常
  6. 不要捕获 Throwable
  7. 不要忽略异常
  8. 不要记录并抛出异常
  9. 包装异常时不要抛弃原始的异常

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

  • 错误的做法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
// do NOT do this
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
  • 原因是 当 try 块中的语句抛出异常或者自己实现的代码抛出异常,那么就不会执行最后的关闭语句,从而资源也无法释放。
  • 正确的做法参考下面的 try–catch-finallytry-with-resource

try–catch-finally

  • 在 finally 代码块中进行资源的释放处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}

try-with-resource

  • 在 try 中操作需要关闭的关闭的资源
1
2
3
4
5
6
7
8
9
10
public void automaticallyCloseResource() {
final File file = new File("./tmp.txt");
try (final FileInputStream inputStream = new FileInputStream(file)) {
// use the inputStream to read a file
} catch (final FileNotFoundException e) {
logger.error("", e);
} catch (final IOException e) {
logger.error("", e);
}
}

指定具体的异常

  • 错误的做法如下:
1
2
3
public void doNotDoThis() throws Exception {
...
}
  • 正确的做法如下:
1
2
3
public void doThis() throws NumberFormatException {
...
}

如上,NumberFormatException 字面上即可以看出是数字格式化错误。

对异常进行文档说明

和前面的一点一样,都是为了给调用者提供尽可能多的信息,从而可以更好地避免/处理异常。

在 Javadoc 中加入 throws 声明,并且描述抛出异常的场景。

1
2
3
4
5
6
7
8
9
/**
* This method does something extremely useful ...
*
* @param input
* @throws MyBusinessException 什么情况下会发生异常
*/
public void doSomething(String input) throws MyBusinessException {
...
}

抛出异常的时候包含描述信息

在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。

NumberFormatException 即告诉了这个异常是格式化错误,异常的额外信息只需要提供这个错误字符串即可。当异常的名称不够明显的时候,则需要提供尽可能具体的错误信息。

比如下面的这个会出现 NumberFormatException,但是必须要知道是哪个字符串导致的

1
2
3
4
5
6
final String numberString = "xyz";
try {
new Long(numberString);
} catch (final NumberFormatException e) {
logger.error(String.format("%s has exception", numberString), e);
}
  • 异常信息
1
2
3
14:41:07.198 [main] ERROR com.ckjava.xutils.FileUtils - xyz has exception
java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)

首先捕获最具体的异常

现在很多 IDE 都能智能提示这个最佳实践,当你试图首先捕获最笼统的异常时,会提示不能达到的代码。当有多个 catch 块中,按照捕获顺序只有第一个匹配到的 catch 块才能执行。因此,如果先捕获 IllegalArgumentException,那么则无法运行到对 NumberFormatException 的捕获。

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

不要捕获 Throwable

Throwable 是所有异常和错误的父类。你可以在 catch 语句中捕获,但是永远不要这么做。

如果 catch 了 throwable,那么不仅仅会捕获所有 exception,还会捕获 error。而 error 是表明无法恢复的 jvm 错误。因此除非绝对肯定能够处理或者被要求处理 error,不要捕获 throwable。

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

不要忽略异常

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

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

但现实是经常会出现无法预料的异常或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。合理的做法是至少要记录异常的信息。

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

不要记录并抛出异常

可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
17:48:23.084 [main] ERROR com.ckjava.xutils.FileUtils - xyz has exception
java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.<init>(Long.java:965)

java.lang.NumberFormatException: For input string: "xyz"

at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.<init>(Long.java:965)
at com.ckjava.xutils.test.exception.TestException.test2(TestException.java:37)

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

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

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

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

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

需要注意的是,包装异常时,一定要把原始的异常设置为 cause(Exception 有构造方法可以传入 cause)。否则,丢失了原始的异常信息会让错误的分析变得困难。

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

总结

综上可知,当抛出或者捕获异常时,有很多不一样的东西需要考虑。其中的许多点都是为了提升代码的可阅读性或者 api 的可用性。

异常不仅仅是一个错误控制机制,也是一个沟通媒介,因此与你的协作者讨论这些最佳实践并制定一些规范能够让每个人都理解相关的通用概念并且能够按照同样的方式使用它们。

打赏

  • 微信

  • 支付宝