处理异常是最常见但不一定是最简单的任务之一。 它仍然是经验丰富的团队中经常讨论的话题之一,并且我们应该了解一些最佳实践和常见错误。
以下是在处理应用程序中的异常时应避免的几个错误。
错误 1:指定一个 java.lang.Exception 或 java.lang.Throwable
正如我们之前解释的那样,我们需要指定或处理已检查的异常。 但是检查异常并不是我们可以指定的唯一异常。 我们可以在 throws
子句中使用 java.lang.Throwable
的任何子类。 因此,我们可以在 throws
子句中使用 java.lang.Exception
,而不是指定以下代码片段抛出的两个不同异常。
public void doNotSpecifyException() throws Exception {
doSomething();
}
public void doSomething() throws NumberFormatException, IllegalArgumentException {
// do something
}
但这并不意味着你应该那样做。 指定 Exception
或 Throwable
使得在调用我们的方法时几乎不可能正确处理它们。
!> 我们的方法的调用者获得的唯一信息是可能出了问题。 但我们不会分享有关可能发生的异常事件类型的任何信息。 将此信息隐藏在一个不明确的 throws 子句后面。
当我们的应用程序随时间变化时,情况会变得更糟。 非特定的 throws
子句隐藏了对调用者必须预期和处理的异常的所有更改。 这可能会导致一些意外错误,我们需要通过测试用例而不是编译器错误来查找这些错误。
使用特定类
因此,最好指定最具体的异常类,即使我们必须使用多个异常类也是如此。 这会告诉方法的调用者需要处理哪些异常事件。 它还允许我们在方法抛出其他异常时更新 throws
子句。 因此,如果我们更改 throws
子句,我们的客户会知道更改,甚至会收到错误。 这比仅在运行特定测试用例时才出现的异常更容易查找和处理。
public void specifySpecificExceptions() throws NumberFormatException, IllegalArgumentException {
doSomething();
}
错误 2:捕获不特定的异常
此错误的严重程度取决于我们正在实施的软件组件的类型以及捕获异常的位置。 在 Java SE 应用程序的主要方法中捕获 java.lang.Exception
可能没问题。 但是如果你正在实现一个库或者如果你正在处理应用程序的更深层,你应该更喜欢捕获特定的异常。
!> 这提供了几个好处。 它允许我们以不同的方式处理每个异常类,并防止我们捕获没有预料到的异常。
%> 但请记住
,处理异常类或其超类之一的第一个 catch
块将捕获它。 所以,一定要先上最具体的类。 否则,我们的 IDE 将显示错误或警告消息,告诉我们无法访问代码块。
try {
doSomething();
} catch (NumberFormatException e) {
// handle the NumberFormatException
log.error(e);
} catch (IllegalArgumentException e) {
// handle the IllegalArgumentException
log.error(e);
}
错误 3:记录并抛出异常
这是处理 Java 异常时最常见的错误之一。在抛出异常的地方记录异常,然后将其重新抛给可以实现用例特定处理的调用者,这似乎是合乎逻辑的。但由于以下三个原因,我们不应该这样做:
- 我们没有足够的关于方法调用者想要实现的用例的信息。异常可能是预期行为的一部分并由客户端处理。在这种情况下,可能不需要记录它。这只会在我们的日志文件中添加错误消息,我们的操作团队需要对其进行过滤。
- 日志消息不提供任何不属于异常本身的信息。它的消息和堆栈跟踪应提供有关异常事件的所有相关信息。消息描述它,堆栈跟踪包含有关类、方法和发生它的行的详细信息。
- 当我们在捕获它的每个
catch
块中记录同一个异常时,我们可能会多次记录同一个异常。这会扰乱我们的监控工具中的统计数据,并使我们的运营和开发团队更难阅读日志文件。
处理时记录下来
因此,最好只在处理异常时记录异常。 就像在下面的代码片段中一样。 doSomething
方法抛出异常。 doMore
方法只是指定它,因为开发人员没有足够的信息来处理它。 然后它在 doEvenMore
方法中得到处理,该方法还写入一条日志消息。
public void doEvenMore() {
try {
doMore();
} catch (NumberFormatException e) {
// handle the NumberFormatException
} catch (IllegalArgumentException e) {
// handle the IllegalArgumentException
}
}
public void doMore() throws NumberFormatException, IllegalArgumentException {
doSomething();
}
public void doSomething() throws NumberFormatException, IllegalArgumentException {
// do something
}
错误 4:使用异常来控制流程
使用异常来控制应用程序的流程被认为是一种反模式,主要原因有两个:
- 它们基本上像 Go To 语句一样工作,因为它们取消代码块的执行并跳转到处理异常的第一个
catch
块。 这使得代码很难阅读。 - 它们不如 Java 的通用控制结构高效。 顾名思义,我们应该只将它们用于异常事件,并且 JVM 不会像其他代码那样优化它们。
因此,最好使用适当的条件来打破循环或使用 if-else
语句来决定应该执行哪些代码块。
错误 5:删除异常的原始原因
有时我们可能希望将异常包装在另一个异常中。 也许我们的团队决定使用带有错误代码和统一处理的自定义业务异常。 只要我们不消除原因,这种方法就没有错。
当你实例化一个新的异常时,你应该总是将捕获的异常设置为它的原因。 否则,我们将丢失描述导致异常的异常事件的消息和堆栈跟踪。 Exception
类及其所有子类提供了几个构造方法,这些方法接受原始异常作为参数并将其设置为原因。
try {
doSomething();
} catch (NumberFormatException e) {
throw new MyBusinessException(e, ErrorCode.CONFIGURATION_ERROR);
} catch (IllegalArgumentException e) {
throw new MyBusinessException(e, ErrorCode.UNEXPECTED);
}
错误 6:泛化异常
当我们泛化一个异常时,我们会捕获一个特定的异常,例如 NumberFormatException
,然后抛出一个非特定的 java.lang.Exception
。 这与我在这篇文章中描述的第一个错误相似但更糟。 它不仅在我们的 API 上隐藏了有关特定错误情况的信息,而且还使其难以访问。
public void doNotGeneralizeException() throws Exception {
try {
doSomething();
} catch (NumberFormatException e) {
throw new Exception(e);
} catch (IllegalArgumentException e) {
throw new Exception(e);
}
}
正如我们在下面的代码片段中看到的,即使知道该方法可能抛出哪些异常,我们也不能简单地捕获它们。 我们需要捕获通用异常类,然后检查其原因的类型。 这段代码不仅实现起来很麻烦,而且也很难阅读。 如果将此方法与错误 5 结合使用,情况会变得更糟。这会删除有关异常事件的所有信息。
try {
doNotGeneralizeException();
} catch (Exception e) {
if (e.getCause() instanceof NumberFormatException) {
log.error("NumberFormatException: " + e);
} else if (e.getCause() instanceof IllegalArgumentException) {
log.error("IllegalArgumentException: " + e);
} else {
log.error("Unexpected exception: " + e);
}
}
那么,更好的方法是什么?
具体并保留原因
这很容易回答。 我们抛出的异常应始终尽可能具体。 如果你包装了一个异常,你也应该将原始异常设置为原因,这样你就不会丢失描述异常事件的堆栈跟踪和其他信息。
try {
doSomething();
} catch (NumberFormatException e) {
throw new MyBusinessException(e, ErrorCode.CONFIGURATION_ERROR);
} catch (IllegalArgumentException e) {
throw new MyBusinessException(e, ErrorCode.UNEXPECTED);
}
错误 7:添加不必要的异常转换
正如我们之前解释的,将异常包装到自定义异常中会很有用,只要我们将原始异常设置为其原因即可。 但是一些架构师做得太过头了,为每个架构层引入了一个自定义的异常类。 因此,他们在持久层中捕获异常并将其包装到 MyPersistenceException
中。 业务层捕获它并将其包装在 MyBusinessException
中,这一直持续到它到达 API 层或得到处理。
public void persistCustomer(Customer c) throws MyPersistenceException {
// persist a Customer
}
public void manageCustomer(Customer c) throws MyBusinessException {
// manage a Customer
try {
persistCustomer(c);
} catch (MyPersistenceException e) {
throw new MyBusinessException(e, e.getCode());
}
}
public void createCustomer(Customer c) throws MyApiException {
// create a Customer
try {
manageCustomer(c);
} catch (MyBusinessException e) {
throw new MyApiException(e, e.getCode());
}
}
很容易看出这些额外的异常类没有提供任何好处。他们只是引入了额外的层来包装异常。虽然用很多彩色纸包装礼物可能很有趣,但这并不是软件开发的好方法。
确保添加信息
当我们需要查找导致异常的问题时,只需考虑需要处理异常的代码或您自己。我们首先需要挖掘几层异常以找到原始原因。直到今天,我从未见过使用这种方法并在每个异常层中添加有用信息的应用程序。它们要么概括错误消息和代码,要么提供冗余信息。
!> 因此
,请注意我们引入的自定义异常类的数量。我们应该经常问自己新的异常类是否提供了任何附加信息或其他好处。在大多数情况下,我们不需要超过一层的自定义异常即可实现。
public void persistCustomer(Customer c) {
// persist a Customer
}
public void manageCustomer(Customer c) throws MyBusinessException {
// manage a Customer
throw new MyBusinessException(e, e.getCode());
}
public void createCustomer(Customer c) throws MyBusinessException {
// create a Customer
manageCustomer(c);
}
有关 Java 异常的更多信息
如大家所见,在处理 Java 异常时应尽量避免一些常见错误。 这有助于我们避免常见错误并实施易于维护和在生产中监控的应用程序。
如果这份常见错误快速列表有用,大家还应该看看我们的最佳实践这篇文章。 它为大家提供了一个建议列表,大多数软件开发团队都使用这些建议来实施他们的异常处理并避免出现本文中描述的问题。