前言

Java中的异常处理是个不简单的话题。初学者很难理解,即使是经验丰富的开发人员也可以花费数小时来讨论如何以及应该抛出或处理哪些异常。

这就是为什么大多数开发团队都有一套关于如何使用它们的规则的原因。而且,如果您是团队新手,那么您可能会感到惊讶,这些规则与您以前使用的规则有何不同。

尽管如此,大多数团队还是采用了几种最佳实践。以下是9个最重要的信息,它们可以帮助您入门或改善异常处理。

一、在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);
            }
        }
    }
}复制代码

Java 7的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);
    }
}复制代码

二、指定具体的异常

抛出的异常越具体越好。始终牢记,不知道您的代码,或者可能几个月后不知道您的代码的同事,需要调用您的方法并处理该异常。

因此,请确保为他们提供尽可能多的信息。这使您的API更易于理解。结果,您的方法的调用者将能够更好地处理该异常,或者通过额外的check避免该异常。

因此,请始终尝试查找最适合您的异常事件的类,例如,抛出NumberFormatException而不是.

IllegalArgumentException。并避免引发不确定的Exception。

public void doNotDoThis() throws Exception {
    ...
}
public void doThis() throws NumberFormatException {
    ...
}复制代码

整理了一下2021年的Java工程师经典面试真题,共485页大概850道含答案的面试题PDF,包含了Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、MySQL、Spring、Spring Boot、Spring Cloud、RabbitMQ、Kafka、Linux 等几乎所有技术栈,每个技术栈都有不少于50道经典面试真题,不敢说刷完包你进大厂,但有针对性的刷让你面对面试官的时候多几分底气还是没问题的。

三、对特定异常进行归档

每当在方法签名中指定异常时,也应在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。当您以错误的格式提供String时,它将由类java.lang.Long的构造函数引发。

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的子类。

始终首先捕获最具体的异常类,并将不那么具体的捕获块添加到列表的末尾。

您可以在以下代码片段中看到这样的try-catch语句的示例。第一个catch块处理所有NumberFormatException,第二个所有IllegalArgumentException,它们不是NumberFormatException。

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。JVM抛出严重的错误问题,这些问题不会由应用程序处理。

比如说:OutOfMemoryError或StackOverflowError。

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

因此,最好不要捕获Throwable,除非您完全确定自己处于特殊情况下,在这种情况下您能够或被要求处理错误。

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

七、不要忽略异常

您是否曾经分析过仅在用例的第一部分得到执行的错误报告?

这通常是由忽略的异常引起的。开发人员可能非常确定不会将其抛出,并添加了一个不会处理或记录它的catch块。并且,当您找到该块时,您很可能甚至找到了著名的“这将永远不会发生”注释之一。

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);
    }
}复制代码

八、不要记录和抛出

这可能是此列表中最常被忽略的最佳实践。您可以找到许多代码段,甚至可以找到捕获,记录和重新抛出异常的库。

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)
在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);
    }
}复制代码

因此,仅在要处理它时才捕获异常。否则,请在方法签名中指定它,然后让调用者来处理它。

九、在不消耗异常的情况下包装异常

有时最好捕获一个标准异常并将其包装到自定义异常中。这种例外的典型示例是特定于应用程序或框架的业务例外。这使您可以添加其他信息,还可以对异常类实施特殊处理。

执行此操作时,请确保将原始异常设置为原因。该异常类提供了接受一个特定的构造方法的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的可用性。

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