写点什么

Java 异常处理的 9 个最佳实践,看看自己是不是都用过?

用户头像
geekymv
关注
发布于: 2020 年 12 月 25 日

原文 | https://dzone.com/articles/9-best-practices-to-handle-exceptions-in-java

作者 | Thorben Janssen

翻译 | geekymv



无论你是初学者还是经验丰富的开发人员,对于你和你的团队来说,提高异常处理的能力可以更好的解决问题。



Java中的异常处理并不是一件容易的事,初学者会觉得很难理解,即使是经验丰富的开发人员也可能需要花费几个小时来讨论应该如何抛出或处理哪些异常。



这也是为什么大多数开发团队对于如何使用它们有自己的一套规则。如果你刚加入一个团队,你可能会惊讶这些规则与你之前使用过的规则是多么的不同。



尽管如此,依然有一些最佳实践在大多数团队中被使用。以下9个最重要的方法,可以帮助你开始或提高异常处理。



1、在 finally 代码块中清理资源或使用 try-with-resource 语句



你经常会在try代码块中使用一个资源,比如 InputStream,需要在之后关闭它。在这种情况下的一个常见错误是在try块的末尾关闭资源。



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 代码块是有原因的,你调用一个或多个可能抛出异常的方法,或者可能是你自己抛出异常,这意味着你可能未到达try代码块的尾部,最终,你无法关闭资源。



因此,你应该把所有清理代码放在 finally 代码块中,或者使用 try-with-resource 语句。



使用 finally 代码块



与 try 代码块最后几行不同,finally 代码块总是被执行。这种情况发生在 try 代码块成功执行之后,或者在catch 代码块中处理异常之后。因此,你可以确保清理了所有打开的资源。



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);
}
}
}
}



Java7 中的 Try-With-Resource 语句



另一种方式是 try-with-resource 语句,我在介绍Java异常处理一文中有更详细的说明。



如果你的资源实现了 AutoCloseable 接口,就可以使用它。这是大多数 Java 标准资源所做的。当你在 try 子句中打开资源时,它将在try 代码块执行或者发生异常后自动关闭。



public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}



2、首选具体的异常



抛出的异常越具体越好。请记住,一个不知道你的代码的同事,也可能是几个月以后的你,需要调用你的方法并处理异常。



因此,确保提供给他们尽可能多的信息。这使你的API更容易理解。最终,方法的调用者将能够更好地处理异常或通过额外的检查来避免异常。



因此,总是试着找到最合适你的异常事件的类,例如,抛出 NumberFormatException 而不是 IllegalArgumentException。避免抛出一个不具体的异常。



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



3、为指定的异常编写文档



无论什么时候你在方法签名上指定一个异常时,你都应该在你的Javadoc中为其编写文档。这与之前的最佳实践有同样的目标:提供给调用者尽可能多的信息,以便他可以避免或者处理异常。



因此,确保在你的Javadoc中增加@throws 声明,并且描述可能造成异常的情况。



/**
* This method does something extremely useful ...
*
* @param input
* @throws MyBusinessException if ... happens
*/
public void doSomething(String input) throws MyBusinessException {
...
}



4、抛出带有描述性信息的异常



这个最佳实践背后的思想和之前两个类似,不同的是,你不用将信息提供给方法的调用者。每个需要了解记录在日志文件或监控工具中异常信息的人,都可以阅读该异常信息。



因此,应该尽可能准确的描述问题,并且提供最相关的信息以了解异常事件。



不要误会我的意思,你不应该写一个文本段落,而是应该用1-2两个短句解释异常的原因。这样可以帮助你的运维团队了解问题的严重性,也可以使你更容易分析任何服务事件。



如果抛出一个具体的异常,它的类名将最可能已经描述了错误的种类。因此,你不需要提供很多额外的信息。一个好的例子是 NumberFormatException。当你提供一个错误的字符串格式时,将由 java.lang.Long 类的构造方法抛出 NumberFormatException 异常。



try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
}



NumberFormatException 类的名字已经告诉你问题的种类。它的信息仅仅需要提供导致问题的输入字符串。如果异常类的名字不那么具有表现力,则需要在消息中提供必要的信息。



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



译者注:可见,给类取个好名字多么重要。



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



大多数 IDE 都可以帮助你实现这个最佳实践。 当你尝试捕获不太具体的异常时,它们会报告一个不可到达的代码块。



问题在于,只有第一个与异常匹配的 catch 代码块才会被执行。因此,如果你首先捕获一个 IllegalArgumentException 异常,你将不能到达应该处理更具体的 NumberFormatException 异常的 catch 代码块。因为它是 IllegalArgumentException 类的子类。



总是首先捕获最具体的异常,然后将不太具体的 catch 代码块添加到列表的尾部。



在下面的代码片段中,你可以看到 try-catch 语句的例子,第一个 catch 代码块处理所有 NumberFormatException 异常,并且第二个 catch 代码块处理所有不属于 NumberFormatException 的 *IllegalArgumentException* 异常。



public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}



6、不要捕获 Throwable



Throwable 是所有 Exception 和 *Error* 类的父类。你可以在 catch 语句中使用它,但是你绝对不要这样做!



如果你在 catch 语句中使用 Throwable,它不仅捕获所有的 Exception,还将捕获所有的 Error。



Error 是被 JVM 抛出的,它表示不能被应用程序处理的严重问题。典型的例子是 OutOfMemoryError 或 *StackOverflowError*,都是由应用程序无法控制的情况引起的,并且无法处理。



因此,最好不要捕获 Throwable,除非你绝对确信自己处于能够或需要处理错误的特殊情况下。



public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}



7、不要忽略异常



你是否曾经分析过一个bug报告,其中只执行了用例的第一部分?



这通常是由忽略异常引起的,开发者可能很确信它从不会抛出,并且添加了不处理或不打印日志的 catch 代码块,当你找到这个代码块的时候,你甚至可能发现一个著名的注释“This will never happen”。



public void doNotIgnoreExceptions() {
try {
// do something
} catch (NumberFormatException e) {
// this will never happen
}
}



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



因此,请不要忽略异常。你不知道代码在未来将如何被改变。有人可能会删除阻止异常事件的验证,而没有意识到这会造成问题。或抛出异常的代码被更改,现在抛出同一个类的多个异常,并且调用代码并不能阻止所有这些异常。



你至少应该写一条日志信息,告诉每个人意想不到的事情刚刚发生了,需要有人来检查它。



public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e);
}
}



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



这可能是列表中最经常被忽略的最佳实践。你可以在许多代码段甚至库中发现异常被捕获、打印日志、并重新抛出。



try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}



记录发生的异常,然后将其重新抛出,以便调用者可以适当的处理它,这可能会很直观。但是它将为同一异常写入多个错误信息。



17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" 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.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)



额外的消息也没有增加任何信息。正如最佳实践4中所述,异常信息应该描述异常事件。堆栈跟踪会告诉你在哪个类,方法和行中引发了异常。



如果你需要增加额外的信息,你应该捕获异常并且将其包装在自定义异常中,但是请确保遵循最佳实践9。



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



9、包装异常而不使用它



有时最好是捕获一个异常并将其包装到自定义异常中。这样异常的典型例子是应用程序或框架的具体业务异常。这使你可以增加额外的信息,并且还可以对异常类实现特殊处理。



当你这样做的时候,确保将原始异常设置为原因(cause)。Exception 类提供了接收 Throwable 参数的具体构造方法。否则,你会丢失堆栈跟踪和原始异常的信息,这将使分析导致异常的事件变得困难。



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



译者注:带有 Throwable 参数的 Exception 类的构造方法。



public Exception(String message, Throwable cause) {
super(message, cause);
}



总结



如你所见,当你抛出或捕获异常的时候,你应该考虑很多不同的事情。它们中大多数的目标是提高代码的可读性或API的可用性。



异常通常是一种错误处理机制,同时也是一种通信机制。因此,你应该确保与你的同事讨论你想要应用的最佳实践和规则,以便每个人都理解一般的概念,并以相同的方式使用它们。



发布于: 2020 年 12 月 25 日阅读数: 23
用户头像

geekymv

关注

公众号:geekymv 2018.03.26 加入

还未添加个人简介

评论

发布
暂无评论
Java异常处理的9个最佳实践,看看自己是不是都用过?