在 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 块中的所有语句都将被执行,并且资源被关闭。

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

新的 Try-With-Resource 语句

另一种选择是 try-with-resource 语句,我们在 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. 特定的异常

我们抛出的异常越具体越好。 永远记住,不了解你的代码的同事(或者可能是几个月后的我们自己)可能需要调用自己的方法并处理异常。

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

3.记录我们指定的异常情况

每当我们在方法签名中指定异常时,我们还应该在 Javadoc 中记录它。

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

4. 使用描述性消息抛出异常

这个最佳实践背后的想法与前两个相似。但这一次,我们不向方法的调用者提供信息。

当日志文件或您的监控工具报告异常时,必须了解发生了什么的每个人都会阅读异常的消息。

这有助于的运营团队了解问题的严重性,还可以让大家更轻松地分析任何服务事件。

如果你抛出一个特定的异常,它的类名很可能已经描述了错误的种类。因此,我们不需要提供很多额外信息。

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 块被执行。

始终首先捕获最具体的异常类,然后将不太具体的 catch 块添加到列表的末尾。

我们可以在以下代码片段中看到此类 try-catch 语句的示例。 第一个 catch 块处理所有 NumberFormatExceptions,第二个 catch 块处理所有非 NumberFormatException 的 IllegalArgumentExceptions。

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

6. 不要捕获 Throwable

Throwable 是所有异常和错误的超类。 你可以在 catch 子句中使用它,但你永远不应该这样做!

如果在 catch 子句中使用 Throwable,它不仅会捕获所有异常; 它还会捕获所有错误。

错误由 JVM 抛出以指示不打算由应用程序处理的严重问题。

典型的例子是 OutOfMemoryError 或 StackOverflowError。 两者都是由应用程序无法控制且无法处理的情况引起的。

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

7. 不要忽略异常

你是否曾经分析过仅执行了用例的第一部分的错误报告?

这通常是由被忽略的异常引起的。 开发人员可能非常确定它永远不会被抛出并添加了一个不处理或记录它的 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);
    }
}

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. 封装异常而不使用它

有时捕获标准异常并将其封装装到自定义异常中会更好。

此类异常的典型示例是应用程序或框架特定的业务异常。 这允许我们添加额外的信息,我们还可以为异常类实施特殊处理。

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

总结

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

异常通常同时是错误处理机制和通信媒介。

因此,我们应该确保与我们的同事讨论我们希望应用的 Java 异常处理最佳实践和规则,以便每个人都能理解一般概念并以相同的方式使用它们。