Java异常处理的20个最佳实践:告别系统崩溃

打印 上一主题 下一主题

主题 926|帖子 926|积分 2778

引言

在Java编程中,异常处理是一个至关重要的环节,它不仅涉及到程序的稳定性和安全性,还关系到用户体验和系统资源的合理利用。合理的异常处理能够使得程序在面对不可预知错误时,能够优雅地恢复或者给出明确的反馈,而不是简单地崩溃退出。
文章开始前,我们先看下思维导图熟悉下有哪些异常

正文

1、尽量不要捕获 RuntimeException(Unchecked Exception)

阿里巴巴Java开发手册上这样规定:
尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。
正例
  1. if (obj != null) {
  2.   //...
  3. }
  4. " + (a / b));
  5. }
复制代码
反例
  1. try {
  2.   obj.method();
  3. } catch (NullPointerException e) {
  4.   //...
  5. }
复制代码
如果有些异常预检查不出来呢?比如说 NumberFormatException,虽然也属于 RuntimeException,但没办法预检查,所以还是应该用 catch 捕获处理。
2、 切勿在代码中使用异常来进行流程控制

异常应当是在真正的异常情况下使用,而不是用来控制程序流程。
  1. public class Demo {
  2.     public static void main(String[] args) {
  3.         String input = "1,2,3,a,5";
  4.         String[] values = input.split(",");
  5.         for (String value : values) {
  6.             try {
  7.                 int num = Integer.parseInt(value);
  8.                 System.out.println(num);
  9.             } catch (NumberFormatException e) {
  10.                 System.err.println(value + " is not a valid number");
  11.             }
  12.         }
  13.     }
  14. }
  15. }
复制代码
3、 合理利用finally块

确保在finally块中释放资源,比如关闭文件流或数据库连接,无论是否发生异常。
  1. FileInputStream file = null;
  2. try {
  3.     file = new FileInputStream("someFile.txt");
  4.     // 使用文件流
  5. } catch (IOException e) {
  6.     e.printStackTrace();
  7. } finally {
  8.     if (file != null) {
  9.         try {
  10.             file.close();
  11.         } catch (IOException e) {
  12.             e.printStackTrace();
  13.         }
  14.     }
  15. }
复制代码
4、 不要在finally块中使用return

这会导致try块中的return语句被忽略。
  1. public int notGood() {
  2.     try {
  3.         // 假设这里有逻辑代码
  4.         return 1;
  5.     } finally {
  6.         return 2;
  7.     }
  8. }
复制代码
5、 避免忽略异常

即使认为某些异常不重要也不应该完全忽略它们,至少要记录下来。
反例
  1. public void doNotIgnoreExceptions() {
  2.     try {
  3.     } catch (NumberFormatException e) {
  4.         // 没有记录异常
  5.     }
  6. }
复制代码
正例
  1. public void logAnException() {
  2.     try {
  3.     } catch (NumberFormatException e) {
  4.         log.error("哦,错误竟然发生了: " + e);
  5.     }
  6. }
复制代码
6、 捕获具体的子类而不是捕获 Exception 类

对不同类型的异常给出不同的处理逻辑。
反例
  1. try {
  2.    someMethod();
  3. } catch (Exception e) { //错误方式
  4.    LOGGER.error("method has failed", e);
  5. }
复制代码
正例
  1. try {
  2.     // 某些可能产生异常的操作
  3. } catch (FileNotFoundException e) {
  4.     // 文件未找到的处理逻辑
  5. } catch (IOException e) {
  6.     // IO异常的处理逻辑
  7. }
复制代码
7、 将所有相关信息尽可能地传递给异常

有用的异常消息和堆栈跟踪非常重要,如果你的日志不能定位异常位置,那要日志有什么用呢?
  1. try {
  2.     // 某些可能产生异常的操作
  3. } catch (IOException | SQLException e) {
  4. // Log exception message and stack trace
  5.     LOGGER.debug("Error reading file", e);
  6. }
复制代码
应该尽量把 String message, Throwable cause 异常信息和堆栈都输出。
8、 使用自定义异常传递更多信息

当内置的异常类型不能满足需求时,可以创建自定义异常。
  1. public class MyException extends Exception {
  2.     public MyException(String message) {
  3.         super(message);
  4.     }
  5. }
  6. public void doSomething() throws MyException {
  7.     // 某些逻辑
  8.     throw new MyException("特定错误信息");
  9. }
复制代码
9、 自定义异常时不要丢失堆栈跟踪

在捕获一个异常并抛出另一个异常时,保留原始异常的信息。
  1. catch (NoSuchMethodException e) {
  2.   //错误方式
  3. throw new MyServiceException("Some information: " + e.getMessage());
  4. }
复制代码
这破坏了原始异常的堆栈跟踪,正确的做法是:
  1. catch (NoSuchMethodException e) {
  2. //正确方式
  3. throw new MyServiceException("Some information: " , e);
  4. }
复制代码
10、优先使用标准异常

在可能的情况下,应优先使用Java标准库中定义的异常。
  1. public void setValue(int value) {
  2.     if (value < 0) {
  3.         throw new IllegalArgumentException("值不能为负"); // 使用标准异常
  4.     }
  5.     // 设置值的逻辑
  6. }
复制代码
11、不要在生产环境中使用 printStackTrace()

在 Java 中,printStackTrace() 方法用于将异常的堆栈跟踪信息输出到标准错误流中。这个方法对于调试和排错非常有用。但在生产环境中,不应该使用 printStackTrace() 方法,因为它可能会导致以下问题:

  • printStackTrace() 方法将异常的堆栈跟踪信息输出到标准错误流中,这可能会暴露敏感信息,如文件路径、用户名、密码等。
  • printStackTrace() 方法会将堆栈跟踪信息输出到标准错误流中,这可能会影响程序的性能和稳定性。在高并发的生产环境中,大量的异常堆栈跟踪信息可能会导致系统崩溃或出现意外的行为。
  • 由于生产环境中往往是多线程、分布式的复杂系统,printStackTrace() 方法输出的堆栈跟踪信息可能并不完整或准确。
在生产环境中,应该使用日志系统来记录异常信息,例如** log4j、slf4j、logback**等。日志系统可以将异常信息记录到文件或数据库中,而不会暴露敏感信息,也不会影响程序的性能和稳定性。同时,日志系统也提供了更多的功能,如级别控制、滚动日志、邮件通知等。
  1. //例如,可以使用 logback 记录异常信息,如下所示:
  2. try {
  3.     // some code
  4. } catch (Exception e) {
  5.     logger.error("An error occurred: ", e);
  6. }
复制代码
12、 不要捕获 Throwable

Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了。
  1. public void doNotCatchThrowable() {
  2.     try {
  3.     } catch (Throwable t) {
  4.         // 不要这样做
  5.     }
  6. }
复制代码
13、利用try-with-resources自动管理资源

Java 7引入的try-with-resources语句可以自动管理资源,减少代码冗余。
  1. try (FileInputStream input = new FileInputStream("file.txt")) {
  2.     // 使用资源
  3. } catch (IOException e) {
  4.     e.printStackTrace();
  5. }
复制代码
14、为异常提供详细的上下文信息

在抛出异常时,提供足够的上下文信息,以帮助定位和解决问题。
  1. public void loadConfiguration(String path) throws IOException {
  2.     try {
  3.         // 加载配置逻辑
  4.     } catch (IOException e) {
  5.         throw new IOException("加载配置文件失败,路径:" + path, e);
  6.     }
  7. }
复制代码
15、不要记录了异常又抛出了异常

这纯属画蛇添足,并且容易造成错误信息的混乱。
反例:
  1. try {
  2. } catch (NumberFormatException e) {
  3.     log.error(e);
  4.     throw e;
  5. }
复制代码
要抛出就抛出,不要记录,记录了又抛出,等于多此一举。
正例:
  1. public void wrapException(String input) throws MyBusinessException {
  2.     try {
  3.     } catch (NumberFormatException e) {
  4.         throw new MyBusinessException("错误信息描述:", e);
  5.     }
  6. }
复制代码
16、finally 块中不要抛出任何异常
  1. try {
  2.   someMethod();  //Throws exceptionOne
  3. } finally {
  4.   cleanUp();    //如果finally还抛出异常,那么exceptionOne将永远丢失
  5. }
复制代码
finally 块用于定义一段代码,无论 try 块中是否出现异常,都会被执行。finally 块通常用于释放资源、关闭文件等必须执行的操作。
如果在 finally 块中抛出异常,可能会导致原始异常被掩盖。比如说上例中,一旦 cleanup 抛出异常,someMethod 中的异常将会被覆盖
17、只抛出和方法相关的异常

相关性对于保持代码的整洁非常重要。一种尝试读取文件的方法,如果抛出 NullPointerException,那么它不会给用户提供有价值的信息。相反,如果这种异常被包裹在自定义异常中,则会更好。NoSuchFileFoundException 则对该方法的用户更有用。
  1. public class Demo {
  2.     public static void main(String[] args) {
  3.         try {
  4.             int result = divide(10, 0);
  5.             System.out.println("The result is: " + result);
  6.         } catch (ArithmeticException e) {
  7.             System.err.println("Error: " + e.getMessage());
  8.         }
  9.     }
  10.     public static int divide(int a, int b) throws ArithmeticException {
  11.         if (b == 0) {
  12.             throw new ArithmeticException("Division by zero");
  13.         }
  14.         return a / b;
  15.     }
  16. }
复制代码
18、尽早验证用户输入以在请求处理的早期捕获异常

再好的异常捕获不如没有异常,所以我们在业务开发中,可以提前验证用户输入,以在请求处理的早期就捕获处理异常
举个例子,我们用 JDBC 的方式往数据库插入数据,那么最好是先 validate 再 insert,而不是 validateUserInput、insertUserData、validateAddressInput、insertAddressData。
  1. Connection conn = null;
  2. try {
  3.     // Connect to the database
  4.     conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "username", "password");
  5.     // Start a transaction
  6.     conn.setAutoCommit(false);
  7.     // Validate user input
  8.     validateUserInput();
  9.     // Insert user data
  10.     insertUserData(conn);
  11.     // Validate address input
  12.     validateAddressInput();
  13.     // Insert address data
  14.     insertAddressData(conn);
  15.     // Commit the transaction if everything is successful
  16.     conn.commit();
  17. } catch (SQLException e) {
  18.     // Rollback the transaction if there is an error
  19.     if (conn != null) {
  20.         try {
  21.             conn.rollback();
  22.         } catch (SQLException ex) {
  23.             System.err.println("Error: " + ex.getMessage());
  24.         }
  25.     }
  26.     System.err.println("Error: " + e.getMessage());
  27. } finally {
  28.     // Close the database connection
  29.     if (conn != null) {
  30.         try {
  31.             conn.close();
  32.         } catch (SQLException e) {
  33.             System.err.println("Error: " + e.getMessage());
  34.         }
  35.     }
  36. }
复制代码
19、 一个异常只能包含在一个日志中

反例
  1. log.debug("Using redis one");
  2. log.debug("Using redis two");
复制代码
在单线程环境中,这样看起来没什么问题,但如果在多线程环境中,这两行紧挨着的代码中间可能会输出很多其他的内容,导致问题查起来会很难受。应该这样做:
  1. LOGGER.debug("Using redis one, Using redis two");
复制代码
20、对于不打算处理的异常,直接使用 try-finally,不用 catch
  1. try {
  2.   method1();  // 会调用 Method 2
  3. } finally {
  4.   cleanUp();    //do cleanup here
  5. }
复制代码
如果 method1 正在访问 Method 2,而 Method 2 抛出一些你不想在 Method 1 中处理的异常,但是仍然希望在发生异常时进行一些清理,可以直接在 finally 块中进行清理,不要使用 catch 块。
总结

有效的异常处理是高质量Java应用开发的基石。也是Java面试中被问频率很高的问题,通过遵循上述20个最佳实践,开发者不仅能够编写出更加健壮和可维护的代码,还能应对面试中面试官的各种关于异常的问题。
最后说一句(求关注,求赞,别白嫖我)

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。
这是大佬写的, 7701页的BAT大佬写的刷题笔记,让我offer拿到手软
本文,已收录于,我的技术网站 aijiangsir.com,有大厂完整面经,工作技术,架构师成长之路,等经验分享
求一键三连:点赞、分享、收藏

点赞对我真的非常重要!在线求赞,加个关注我会非常感激![]()

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

去皮卡多

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表