Java中的异常处理不是一个简单的主题。初学者发现它很难理解,甚至有经验的开发者也可以花几个小时讨论如何以及应该抛出或处理哪些异常。
这就是为什么大多数开发团队都有自己的一套如何使用它们的规则。如果你是一个团队的新手,你可能会惊讶这些规则与你之前使用的规则有多么不同。
尽管如此,大多数团队都使用了几种最佳实践。以下是帮助你入门或改进异常处理的9个最重要的内容。
在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语句。
与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);
}
}
}
}
另一种选择是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);
}
}
抛出的异常越具体越好。请记住,不明白你代码的同事,或者你可能在几个月后需要调用你的方法并处理异常。
因此,请务必提供尽可能多的信息。这使你的API更易于理解。因此,你的方法的调用者将能够更好地处理异常或通过额外的检查来避免它。
因此,总是尝试找到最适合你的异常事件的类,例如抛出NumberFormatException而不是IllegalArgumentException。并避免抛出非特定的异常。
public void doNotDoThis() throws Exception {
...
}
public void doThis() throws NumberFormatException {
...
}
无论何时在方法签名中指定异常,都应该在Javadoc中记录它。这与以前的最佳实践具有相同的目标:为调用者提供尽可能多的信息,以便他可以避免或处理异常。
因此,请确保向Javadoc 添加@throws声明并描述可能导致异常的情况。
/**
* This method does something extremely useful ...
*
* @param input
* @throws MyBusinessException if ... happens
*/
public void doSomething(String input) throws MyBusinessException {
...
}
这种最佳实践背后的想法类似于前两种实践。但是这次,你不向调用方提供有关方法的信息。每个必须了解在日志文件或监视工具中抛出异常时发生了什么的人都会读取异常的消息。
因此,它应该尽可能准确地描述问题,并提供最相关的信息来理解异常事件。
别误会我的意思; 你不应该写一段文字。但是你应该用1-2个简短的句子来解释这个例外的原因。这有助于你的运营团队了解问题的严重性,还可以让你更轻松地分析任何服务事件。
如果抛出一个特定的异常,它的类名很可能已经描述了那种错误。因此,你无需提供大量其他信息。一个很好的例子是NumberFormatException。它会被类java.lang.Long的构造函数抛出,当你以错误的格式提供String参数。
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"
大多数IDE都可以帮助你实现这一最佳实践。当你尝试首先捕获不太具体的异常时,它们提示无法访问的代码块。
问题是只有匹配异常的第一个catch块才会被执行。因此,如果首先捕获IllegalArgumentException,则永远不会到达应该处理更具体的NumberFormatException的catch块,因为它是IllegalArgumentException的子类。
始终优先捕获最具体的异常类,并将不太具体的catch块添加到列表的末尾。
你可以在以下代码段中看到此类try-catch语句的示例。第一个catch块处理所有的NumberFormatException,第二个处理所有不是NumberFormatException的IllegalArgumentException 异常。
public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
Throwable是所有异常和错误的超类。你可以在catch子句中使用它,但你永远不应该这样做!
如果在catch子句中使用Throwable,它不仅会捕获所有异常; 它还会捕获所有错误。JVM抛出错误以指示应用程序无法处理的严重问题。典型的例子是OutOfMemoryError或StackOverflowError。两者都是由应用程序无法控制的情况引起的,无法处理。
所以,最好不要抓住Throwable,除非你完全确定你处于一个特殊情况,你可以或者需要处理错误。
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
你是否曾经分析过只有用例第一部分被执行的错误报告?
这通常是由忽略的异常引起的。开发人员可能非常确定它永远不会被抛出并添加了一个不处理或记录它的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);
}
}
这可能是此列表中最常被忽略的最佳做法。你可以找到许多代码片段,甚至是catch,log和重新throw异常的库。
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);
}
}
因此,如果你想要处理它,只捕获异常。否则,在方法签名中指定它并让调用者处理它。
有时候捕获标准异常并将其包装成自定义异常会更好。此类异常的典型示例是应用程序或框架特定的业务异常。这允许你添加其他信息,还可以为异常类实现特殊处理。
执行此操作时,请确保将原始异常设置为cause。该异常类提供了接受一个特定的构造方法的Throwable作为参数。否则,你将丢失原始异常的堆栈跟踪和消息,这将导致难以分析导致异常的异常事件。
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
正如所看到的,当你抛出或捕获异常时,你应该考虑许多不同的事情。其中大多数都旨在提高代码的可读性或API的可用性。
异常通常同时是错误处理机制和通信媒介。因此,您应该确保与同事讨论要应用的最佳实践和规则,以便每个人都能理解通用概念并以相同的方式使用它们。