章
目
录
本文主要介绍Java 异常处理种无错误代码的 20 个最佳实践案例。
1. Java 中的内置异常
在我们深入探讨异常处理最佳实践的深入概念之前,让我们从最重要的概念之一开始,即了解 Java 中存在三种常见类型的可抛出类。
1.1. 检查异常
受检查的异常必须在方法的throws子句中声明。它们继承了Exception类,旨在成为“面对面”类型的异常。Java 希望我们处理它们,因为它们依赖于我们程序之外的外部因素。
检查的异常表示正常系统操作期间可能发生的预期问题。大多数情况下,当我们尝试通过网络使用外部系统或读取文件系统中的资源时,会发生这些异常。大多数情况下,对已检查异常的正确响应应该是稍后重试或提示用户修改其输入。
1.2. 未经检查的异常
未经检查的异常不需要在throws子句中声明。JVM 根本不会强制我们处理它们,因为它们大多是由于程序错误而在运行时生成的。他们继承了RuntimeException。
最常见的示例是NullPointerException。未经检查的异常不应该try,正确的操作通常应该是不执行任何操作,并让它从您的方法中抛出来并通过执行堆栈。在高级执行中,应该记录这种类型的异常。
1.3. 错误
这些错误是严重的运行时环境问题,几乎肯定无法恢复。一些示例包括OutOfMemoryError、LinkageError和StackOverflowError。它们通常会使您的程序或程序的一部分崩溃。只有良好的日志记录实践才能帮助您确定错误的确切原因。
2. 自定义异常
任何时候,当用户出于某种原因觉得他想要使用自己的特定于应用程序的异常时,他都可以创建一个扩展适当的超类(主要是其异常)的新类,并开始在适当的位置使用它。这些用户定义的异常可以通过两种方式使用:
- 当应用程序出现问题时,要么直接抛出自定义异常。
throw new DaoObjectNotFoundException("Couldn't find dao with id " + id);
- 或者将原始异常包装在自定义异常中并抛出它。
catch (NoSuchMethodException e) {
throw new DaoObjectNotFoundException("Couldn't find DAO with " + id, e);
}
包装异常可以通过添加您自己的消息/上下文信息来向用户提供额外的信息,同时仍然保留原始异常的堆栈跟踪和消息。它还允许您隐藏代码的实现细节,这是包装异常的最重要原因。
现在,让我们开始探索业界异常处理所遵循的最佳实践。
3. 异常处理的最佳实践
3.1. 永远不要吞掉catch块中的异常
catch (NoSuchMethodException e) {
return null;
}
这样做不仅返回“ null ”而不是处理或重新抛出异常,它还完全吞噬了异常,永远失去了错误的原始原因。当你不知道失败的原因时,你将如何防止将来再次发生?永远不要这样做!
3.2. 声明该方法可以抛出的特定检查异常
public void foo() throws Exception { //Incorrect way
}
始终避免像上面的代码示例那样这样做。它完全违背了检查异常的全部目的。声明您的方法可以抛出的特定已检查异常。
如果这样的检查异常太多,您可能应该将它们包装在您自己的异常中,并在异常消息中添加信息。如果可能的话,您还可以考虑代码重构。
public void foo() throws FileNotFoundException, ParseException { //Correct way
}
3.3. 不要捕获Exception类,而是捕获特定的子类
try {
someMethod();
} catch (Exception e) {
LOGGER.error("method has failed", e);
}
捕获Exception类的问题是,如果该方法稍后向其方法签名添加新的已检查异常,则开发人员的意图是您应该处理特定的新异常。如果您的代码只是捕获Exception(或Throwable),您将永远不会知道更改以及您的代码现在是错误的并且可能在运行时的任何时间点中断的事实。
3.4. 永远不要抓住Throwable
好吧,这是更严重的麻烦。因为 java 错误也是Throwable的子类。
错误是 JVM 本身无法处理的不可逆转的情况。对于某些 JVM 实现,JVM 实际上可能不会在出现错误时调用 catch 子句。
3.5. 始终正确地将异常包装在自定义异常中,以便堆栈跟踪不会丢失
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " + e.getMessage()); //Incorrect way
}
这会破坏原始异常的堆栈跟踪,并且总是错误的。正确的做法是:
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " , e); //Correct way
}
3.6. 要么记录异常,要么抛出异常,但永远不要同时执行这两种操作
catch (NoSuchMethodException e) {
LOGGER.error("Some information", e);
throw e;
}
如上面的示例代码所示,对于代码中的单个问题,记录和抛出将导致日志文件中出现多条日志消息,这会让试图挖掘日志的工程师陷入困境。
3.7. 永远不要从finally块中抛出任何异常
try {
someMethod(); //Throws exceptionOne
} finally {
cleanUp(); //If finally also threw any exception the exceptionOne will be lost forever
}
这很好,只要cleanUp()永远不会抛出任何异常。在上面的示例中,如果someMethod()抛出异常,并且在finally块中,cleanUp()也抛出异常,则第二个异常将从该方法中出来,而原始的第一个异常(正确的原因)将永远丢失。
如果您在finally块中调用的代码可能会引发异常,请确保您处理它或记录它。永远不要让它从finally块中出来。
3.8. 始终只捕获那些您可以处理的异常
catch (NoSuchMethodException e) {
throw e; //Avoid this as it doesn't help anything
}
嗯,这是最重要的概念。不要仅仅为了捕获异常而捕获异常。仅当您想要处理任何异常或者想要在该异常中提供其他上下文信息时才捕获该异常。
如果您无法在 catch 块中处理它,那么最好的建议就是不要捕获它只是为了重新抛出它。
3.9. 不要使用printStackTrace()语句或类似方法
catch (SomeException e) {
e.printStackTrace();
throw e;
}
完成代码后切勿离开printStackTrace() 。您的一位同事很可能最终会获得这些堆栈跟踪之一,并且对于如何处理它的知识完全为零,因为它不会附加任何上下文信息。
3.10. 如果不打算处理异常,请使用finally块而不是catch块
try {
someMethod(); //Method 2
} finally {
cleanUp(); //do cleanup here
}
这也是一个很好的做法。如果在您的方法中您正在访问某些方法 2,并且方法 2 抛出一些您不想在方法 1 中处理的异常,但仍希望在发生异常时进行一些清理,那么请在 finally 块中执行此清理。不要使用catch块。
3.11. 记住“早投晚投”原则
这可能是关于异常处理最著名的原则。它基本上说你应该尽快抛出异常,并尽可能晚地捕获它。您应该等到掌握了所有信息才能正确处理它。
这个原则隐含地表明,您将更有可能将其扔到低级方法中,在其中您将检查单个值是否为空或不合适。并且您将例外地爬升堆栈跟踪相当多个级别,直到达到足够的抽象级别以能够处理问题。
3.12. 处理异常后始终进行清理
如果您正在使用数据库连接或网络连接等资源,请确保清理它们。如果您调用的 API 仅使用未经检查的异常,您仍然应该在使用后使用try-finally块清理资源。在 try 块内访问资源并在finally 内关闭资源。即使访问资源时发生任何异常,资源也会被优雅地关闭。
作为最佳实践,请对AutoCloseable资源使用try-with-resources。
3.13. 仅从方法中抛出相关异常
相关性对于保持应用程序的整洁很重要。尝试读取文件的方法;如果抛出 NullPointerException 那么它不会向用户提供任何相关信息。相反,如果将此类异常包装在自定义异常(例如NoSuchFileFoundException)中,那么它对于该方法的用户会更有用。
3.14.切勿在程序中使用异常进行流程控制
我们已经读过很多次了,但有时我们会在项目中看到开发人员尝试对应用程序逻辑使用异常的代码。永远不要那样做。它使代码难以阅读、理解并且丑陋。
3.15.验证用户输入以在请求处理的早期阶段捕获不利条件
始终在很早的阶段验证用户输入,甚至在它到达实际的请求处理程序之前。它将帮助您最大限度地减少核心应用程序逻辑中的异常处理代码。如果用户输入存在错误,它还可以帮助您使应用程序保持一致。
例如:如果在用户注册应用程序中,您遵循以下逻辑:
- 验证用户
- 插入用户
- 验证地址
- 插入地址
- 如果出现问题,则回滚一切。
这是一种非常不正确的做法。它可能会使您的数据库在各种情况下处于不一致的状态。而是首先验证所有内容,然后在 dao 层获取用户数据并进行数据库更新。
正确的做法是:
- 验证用户
- 验证地址
- 插入用户
- 插入地址
- 如果出现问题,则回滚一切。
3.16.始终在单个日志消息中包含有关异常的所有信息
LOGGER.debug("Using cache sector A");
LOGGER.debug("Using retry sector B");
不要这样做。
在您的测试用例中使用多行日志消息并多次调用LOGGER.debug()可能看起来不错,但是当它显示在具有 400 个并行运行线程的应用程序服务器的日志文件中时,所有信息都将转储到同一个日志文件中,您的两条日志消息最终可能会在日志文件中间隔 1000 行,即使它们出现在代码中的后续行中。
像这样做:
LOGGER.debug("Using cache sector A, using retry sector B");
3.17。将所有相关信息传递给异常,使它们尽可能提供信息
这对于使异常消息和堆栈跟踪变得有用且信息丰富也非常重要。
如果您无法从中确定任何内容,那么日志有什么用呢?这些类型的日志仅存在于您的代码中用于装饰目的。
3.18.总是终止被中断的线程
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {} //Don't do this
doSomethingCool();
}
InterruptedException是你的代码的一个线索,它应该停止正在做的任何事情。线程中断的一些常见用例是活动事务超时或线程池关闭。
您的代码不应忽略InterruptedException,而应尽力完成正在执行的操作并完成当前的执行线程。
所以纠正上面的例子:
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
break;
}
}
doSomethingCool();
3.19.使用模板方法进行重复的 try-catch
在代码中的 100 个地方使用类似的 catch 块是没有用的。它增加了代码的口是心非,这没有任何帮助。对于这种情况,请使用模板方法。
例如,下面的代码尝试关闭数据库连接。
class DBUtil{
public static void closeConnection(Connection conn){
try{
conn.close();
} catch(Exception ex){
//Log Exception - Cannot close connection
}
}
}
这种类型的方法将在应用程序的数千个地方使用。不要将整个代码放在每个地方,而是定义上述方法并在任何地方使用它,如下所示:
public void dataAccessCode() {
Connection conn = null;
try{
conn = getConnection();
....
} finally{
DBUtil.closeConnection(conn);
}
}
3.20.使用 javadoc 记录应用程序中的所有异常
养成对一段代码在运行时可能抛出的所有异常进行 javadoc 的习惯。另外,尝试包括可能的操作方案,用户应遵循,以防发生这些异常。
这就是我现在所想到的与 Java 异常处理最佳实践相关的全部内容。如果您发现任何遗漏或者您在任何观点上与我的观点不符,请给我留言。我很乐意讨论这个问题。
4. 结论
在本 Java 异常处理教程中,学习一些重要的最佳实践,以编写健壮且无错误的应用程序代码。