前言

在Java中应该如何处理异常,这个话题看似简单,不就是​​try...catch​​嘛,但是往往BUG更容易出现在一些简单地、我们更容易忽略的地方。

大多数成熟的开发团队对于如何进行异常处理都有一套规范和最佳实践。

本期内容我整理了一些在我的团队正在使用的9个最佳实践,希望能让你对异常处理有所帮助。

1、使用finally或try...with...resource关闭资源

如果我们在try代码块中需要使用到一些资源,比如​​InputStream​​,在使用完之后我们需要将资源关闭。

这是一个错误示例

public void tryResource() {
FileInputStream inputStream = null;
try {
File file = new File("./小黑说Java.txt");
inputStream = new FileInputStream(file);

// 使用inputStream读取文件

// 不要这样做
inputStream.close();
} catch (FileNotFoundException e) {
log.error("文件未找到", e);
} catch (IOException e) {
log.error("文件读取异常", e);
}
}

在上面这段代码中,只要在文件读取时没有出现异常,这段代码是可以正常工作的,但是只要在try块中的​​close()​​方法中抛出异常,资源就不会被关闭。

所以这种情况我们应该将资源关闭的代码放在​​finally​​中或者使用​​try...with...resource​​语句。

使用finally

在finally块中的代码不管是否出现异常,都会被执行,因此可以确保资源对象被关闭。

public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./小黑说Java.txt");
inputStream = new FileInputStream(file);
// 使用inputStream读取文件
} catch (FileNotFoundException e) {
log.error("文件未找到", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error("资源关闭异常", e);
}
}
}
}

使用try...with...resource

如果你使用的JDK版本是1.7+,那么也可以选择使用​​try...with...resource​​语句。如果你使用的资源类实现​​AutoCloseable​​接口,则可以使用这种方式。

Java中的大多数标准资源类API都实现了这个接口。在try子句中打开资源,将会在try代码块执行完毕或异常处理后自动关闭资源对象。

public void useTryWithResource() {
File file = new File("./小黑说Java.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// 使用inputStream读取文件

} catch (FileNotFoundException e) {
log.error("文件未找到", e);
} catch (IOException e) {
log.error("文件读取异常", e);
}
}

2、使用更明确的异常

如果我们的方法需要向外抛出异常,那么异常类型越具体越好。因为在外部调用你代码的其他人对你内部的实现逻辑可能并不清楚,所以要确保能提供给他尽可能多的信息,可以让别人在使用你的方法时更容易理解,这样调用方可以更好地处理抛出的异常。

比如,在你的方法内容抛出​​NumberFormatException​​比抛出​​IllegalArgumentException​​或者直接抛出​​Exception​​,所代表的含义就会更明确。

3、方法注释中对异常进行说明

如果你的方法声明了可能会抛出异常,那么在方法的文档注释中,应该对异常进行说明。这和上一条的目的一样,都是为了让方法的调用者能提前获得更多的信息,方便他避免在调用你方法时出现异常,或者更明确如果进行异常处理。

所以,我们应该在方法的文档注释中添加@throws声明,并说明什么情况下会抛出对应的异常。

/**
* 这个方法内部做了什么什么事情...
*
* @param input
* @throws BusinessException 如果出现xxx情况,则会抛出这个异常
*/
public void doSomething(String input) throws BusinessException {
...
}

4、在异常中携带足够的描述信息

这一点和前两条做法的目的类似。在异常中携带足够的描述信息,是为了在出现该异常时,能够在日志文件中查看异常信息时,能看到更有用的信息。

所以我们应该尽可能准确地描述出为什么抛出了这个异常,并提供最相关的数据信息让别人定位。

当然这里也不能太极端,你洋洋洒洒写一篇小作文,应该使用简短的一段信息描述,让运维同事能了解到这个问题的严重性,更轻松地分析问题所在。

也不用提供一堆额外的冗余信息,尽量做到足够精准。比如当你再创建一个Long对象时如果传入一个字符串,就会抛出​​NumberFormatException​​。

public static void testLong() {
try {
Long abc = new Long("ABC");
} catch (NumberFormatException e) {
log.error("格式异常", e);
}
}

​NumberFormatException​​的类名已经告诉我们出现的是数字格式化异常,所以在​​message​​中只需要提供输入的字符串。如果你定义的异常类名不能很明确的表达出是什么异常,比如​​BusinessException​​,你就应该在​​message​​中表达出更多的信息。

处理异常的标准姿势!一定要学会!_java

5、先捕获更明确的异常

一般在我们使用的IDE中,如果当你在做异常捕获时,先捕获了不太具体的异常比如Exception,然后再捕获更具体的异常如IOException,都会提示我们后面的catch块无法到达。所以我们应该先捕获最具体的异常类,将不太具体的异常类的捕获放在最后。

public void catchExceptions() {
try {
doSomething("小黑说Java");
} catch (NumberFormatException e) {
log.error("格式异常", e);
} catch (IllegalArgumentException e) {
log.error("非法参数", e);
}
}

6、不要捕获Throwable

​Throwable​​是所有​​Exception​​和​​Error​​的父类。

虽然可以在​​catch​​块中捕获它,但是我们不应该这样去做。因为如果使用了​​Throwable​​,那么不仅会对所有抛出的​​Exception​​进行捕获,还会捕获所有的​​Error​​。

而当我们的程序抛出​​Error​​时表示是一个无法处理的严重问题,例如典型的​​OutofMemoryError​​,​​StackOverflowError​​等,这两个​​Error​​都是由程序无法控制并且不能处理的情况引起的。所以说,最好不要在你的​​catch​​中捕获​​Throwable​​,除非你非常确定​​try​​块中的代码抛出的是可以处理的异常情况。

public void catchThrowable() {
try {
// 一些业务代码
} catch (Throwable t) {
// 不要这样做
}
}

7、不要将异常忽略

在你开发的时候可能非常确定不会抛出异常,并且在你开发时确实没有发生过抛出异常的情况,所以你在catch块中没有对异常做任何处理。

public void doNotIgnoreExceptions() {
try {
// 一些业务代码
} catch (NumberFormatException e) {
// 认为永远不会执行到这里
}
}

但是,你其实不确定在将来会不会有人在你的try块中添加新的代码,并且他可能也不会意识到他添加的代码会导致有异常抛出,这将会导致在线上真的有异常产生,但是没有一个人知道。

所以,你至少应该在catch中打印一行日志,告诉运维同事,“警报,这里出现了一个不可能会出现的异常”。

public void doNotIgnoreExceptions() {
try {
// 一些业务代码
} catch (NumberFormatException e) {
log.error("警报,这里出现了一个不可能会出现的异常", e);
}
}

8、不要打印日志后又将异常抛出

这一条可能绝大多数人都会犯过,我见过非常多别人的代码在异常处理时,先打印了一行异常日志,然后将异常抛出,或者转成一个​​RuntimeException​​抛出。

甚至在一些开源框架中都有出现过。

public void testCatchEx() {
try {
new Long("heihei");
} catch (NumberFormatException e) {
log.error("数字格式异常", e);
throw e;
}
}

你可能会认为这样做很直观,也没什么错,让调用你方法的人去处理就好了。但是这样一来,在日志中会对抛出的一个异常打印多条错误信息。

处理异常的标准姿势!一定要学会!_抛出异常_02

重复的日志并没有带来任何有价值的信息,参考上面第4条中描述,在异常信息中应该携带足够的信息,并且要做到精准。如果需要在添加其他信息,你应该将捕获到的异常封装在你的自定义异常中再进行抛出。

public void wrapException(String input) throws BusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new BusinessException("一段对异常的描述信息.", e);
}
}

所以,我们应该只有在想对异常进行处理时捕获,否则就应该在抛出去,并且在方法前面上加以说明,让调用方去处理。

9、在包装异常时使用原始异常

通常在项目开发时,都会有一套自定义的异常,用于将API中的标准异常封装到自定义异常中,可以用于在外层做一些统一的异常处理。

但是我们在使用自定义异常对原始异常进行封装时,需要确保将原始异常作为​​cause​​保存在自定义异常中,否则你在外层将会丢失原始异常的堆栈跟踪信息,到你你无法通过异常信息分析抛出异常的具体原因。

public void wrapException(String input) throws BusinessException {
try {
// do something
} catch (NumberFormatException e) {
// 将e作为构造参数中的cause
throw new MyBusinessException("一段对异常的描述信息.", e);
}
}
public void wrapException(String input) throws BusinessException {    try {        // do something    } catch (NumberFormatException e) {        // 将e作为构造参数中的cause        throw new MyBusinessException("一段对异常的描述信息.", e);    }}复制代码

总结

在抛出或者捕获异常时,我们应该考虑很多不同的事情,上面所说的大多数都是为了提高代码的可读性和提供给别人的API更易用。

通常异常不光是一种错误处理机制,同时还具备一定的信息媒介作用。我们应该遵循这些异常处理的规则和最佳实践,写出更规范,不让别人吐槽的好代码。

我是小黑,一个在互联网“苟且”的程序员。

流水不争先,贵在滔滔不绝