// 错误示例:捕获通用 Exception,丢失具体异常信息 try { // 可能抛出 IOException 或 SQLException 的代码 } catch (Exception e) { e.printStackTrace(); // 仅打印堆栈,未处理具体异常 } // 正确示例:捕获具体异常,分层处理 try { readFile("data.txt"); saveData(conn, data); } catch (FileNotFoundException e) { log.error("文件不存在: {}", filePath, e); // 记录上下文信息 thrownewBusinessException("数据文件缺失,请联系管理员", e); // 转换为业务异常 } catch (SQLException e) { log.error("数据库保存失败: {}", data, e); conn.rollback(); // 事务回滚 thrownewRetryableException("数据库临时故障,请重试", e); // 标记为可重试异常 } finally { // 关闭资源(Java 7 前的传统方式) if (conn != null) { try { conn.close(); } catch (SQLException e) { log.warn("关闭连接失败", e); // 资源关闭异常不影响主流程 } } }核心要点:
• finally 用于必须执行的资源清理(如关闭流、释放锁),但需注意:若 finally 中抛出异常,会覆盖 try/catch 中的异常,导致原始异常丢失。
// 错误示例:手动关闭资源,易遗漏或抛出异常 InputStreamin=newFileInputStream("file.txt"); try { // 使用资源 } catch (IOException e) { log.error("读取失败", e); } finally { if (in != null) { try { in.close(); // 可能抛出异常,覆盖原始异常 } catch (IOException e) { log.warn("关闭失败", e); } } } // 堆代码 duidaima.com // 正确示例:try-with-resources 自动关闭资源 try (InputStreamin=newFileInputStream("file.txt"); BufferedReaderreader=newBufferedReader(newInputStreamReader(in))) { // 使用资源,无需手动关闭 Stringline= reader.readLine(); } catch (IOException e) { log.error("文件处理失败", e); // 原始异常完整保留 }优势:资源自动关闭,即使 try 或 catch 中抛出异常,资源也会被正确释放,且不会覆盖原始异常。
if (userId == null) { throw new IllegalArgumentException("用户ID不能为空"); // 明确参数错误 }• throws:在方法签名中声明可能抛出的异常,告知调用方"此处可能出错,需处理或继续抛出"。
public User getUser(Long id) throws SQLException, UserNotFoundException { // 可能抛出数据库异常或用户不存在异常 }最佳实践:
• throws 声明最小必要的异常类型,避免 throws Exception 迫使调用方捕获无关异常。
try { // 复杂业务逻辑,可能抛出多种异常 } catch (Exception e) { log.info("操作失败"); // 仅记录简单信息,丢失异常类型和堆栈 }问题分析:
try { // 业务逻辑 } catch (NullPointerException e) { log.error("空指针异常,检查参数: {}", param, e); // 逻辑错误,需修复代码 } catch (IOException e) { log.error("IO异常,文件路径: {}", path, e); // 外部资源错误,需处理 } catch (BusinessException e) { log.warn("业务异常: {}", e.getMessage()); // 已知业务规则,无需堆栈 }错误 2:空 catch 块(异常静默失败)
try { Integer.parseInt(userInput); } catch (NumberFormatException e) { // 什么都不做,希望程序继续执行 }问题分析:
try { Integer.parseInt(userInput); } catch (NumberFormatException e) { log.error("用户输入格式错误: '{}'", userInput, e); // 记录原始输入 throw new ValidationException("请输入有效的数字", e); // 告知用户具体错误 }错误 3:finally 块中使用 return
public int calculate() { try { return 1 / 0; // 抛出 ArithmeticException } catch (Exception e) { return -1; } finally { return 0; // finally 中的 return 覆盖异常和 catch 的返回值 } }执行结果:返回 0,而非 -1,且异常被静默忽略。
• JVM 规范明确:finally 中的 return 会终止异常传播。
public int calculate() { int result = -1; try { result = 1 / 0; } catch (ArithmeticException e) { log.error("计算失败", e); // 保持 result 为 -1 } finally { // 仅清理资源,无返回 } return result; }错误 4:忽略异常链传递(丢失原始异常)
try { readConfig("app.properties"); } catch (IOException e) { throw new RuntimeException("配置读取失败"); // 丢失原始异常堆栈 }问题分析:
try { readConfig("app.properties"); } catch (IOException e) { // 构造新异常时传入原始异常 throw new ConfigurationException("配置读取失败", e); }效果:日志中会显示完整堆栈,包含 ConfigurationException 和 IOException 的因果关系,快速定位问题。
// 错误:用异常判断列表是否为空 List<User> users = getUserList(); try { User first = users.get(0); // 列表为空时抛出 IndexOutOfBoundsException process(first); } catch (IndexOutOfBoundsException e) { log.info("用户列表为空,执行默认逻辑"); }问题分析:
List<User> users = getUserList(); if (users.isEmpty()) { log.info("用户列表为空,执行默认逻辑"); } else { process(users.get(0)); }错误 6:资源未关闭导致泄漏(try-with-resources 缺失)
// Java 7 前未使用 try-with-resources,资源关闭遗漏 InputStreamin=null; try { in = newFileInputStream("largeFile.txt"); // 处理文件(可能抛出异常,导致 in.close() 未执行) } catch (IOException e) { log.error("处理失败", e); } finally { // 若 in 为 null(如构造器失败),则跳过关闭 if (in != null) { in.close(); // 此处可能抛出 IOException,覆盖原始异常 } }问题分析:
try (InputStream in = new FileInputStream("largeFile.txt"); BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { // 处理文件,无需手动关闭 } catch (IOException e) { log.error("处理失败", e); // 原始异常完整保留 }错误 7:自定义异常设计不当
// 错误:自定义异常继承 Exception(受检异常),但无需强制处理 publicclassDataNotFoundExceptionextendsException { publicDataNotFoundException(String message) { super(message); } } // 调用方被迫处理或声明抛出 public User getUser(Long id)throws DataNotFoundException { // ... }问题分析:
public classDataNotFoundExceptionextendsRuntimeException { privatefinal String errorCode; privatefinalboolean retryable; publicDataNotFoundException(String message, String errorCode, boolean retryable) { super(message); this.errorCode = errorCode; this.retryable = retryable; } // getter 方法 }错误 8:日志记录不完整(缺少上下文)
try { updateUser(user); } catch (SQLException e) { log.error("更新用户失败"); // 仅记录消息,无用户ID、SQL语句等上下文 }问题分析:
// 错误:将逻辑错误定义为受检异常 publicclassInvalidParamExceptionextendsException { // ... } // 调用方必须捕获或声明,增加冗余代码 publicvoidvalidateParam(String param)throws InvalidParamException { if (param == null) { thrownewInvalidParamException("参数为空"); } }问题分析:
// 底层方法抛出受检异常 publicvoidreadFile()throws IOException { ... } // 中间层不处理,直接抛出 publicvoidprocessData()throws IOException { readFile(); } // 最终传递到 Controller,被迫捕获 @GetMapping("/data") public String getData() { try { service.processData(); } catch (IOException e) { return"操作失败"; // 无法区分是文件不存在还是权限不足 } }问题分析:
public void processData() { try { readFile(); } catch (FileNotFoundException e) { throw new BusinessException("数据文件缺失,请联系管理员", e); // 用户可理解的消息 } catch (IOException e) { throw new SystemException("文件系统故障", "FS-001", e); // 系统级错误,标记错误码 } }三、异常处理最佳实践(2023-2025 最新指南)
层级 | 职责 | 处理方式 |
底层(工具/DAO) | 抛出原始异常,不处理业务逻辑 | throws SQLException、IOException |
中间层(Service) | 转换异常类型,附加业务上下文 | 捕获底层异常 → 抛出 BusinessException |
顶层(Controller) | 统一异常响应,用户友好提示 | 捕获业务异常 → 返回标准化错误响应 |
@RestControllerAdvice publicclassGlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) { ErrorResponseerror=newErrorResponse(e.getErrorCode(), e.getMessage()); returnnewResponseEntity<>(error, HttpStatus.BAD_REQUEST); } @ExceptionHandler(RetryableException.class) public ResponseEntity<ErrorResponse> handleRetryableException(RetryableException e) { ErrorResponseerror=newErrorResponse(e.getErrorCode(), e.getMessage() + ", 建议10秒后重试"); returnnewResponseEntity<>(error, HttpStatus.SERVICE_UNAVAILABLE); } @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleUnknownException(Exception e) { log.error("未处理异常", e); // 记录完整堆栈 ErrorResponseerror=newErrorResponse("SYSTEM_ERROR", "系统繁忙,请稍后再试"); returnnewResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); } }2. Java 9+ 新特性:提升异常处理效率
// Java 7-:必须在 try 中声明资源 try (BufferedReaderreader=newBufferedReader(newFileReader("file.txt"))) { ... } // Java 9+:可使用外部声明的资源(需为 final 或 effectively final) BufferedReaderreader=newBufferedReader(newFileReader("file.txt")); try (reader) { // 直接使用变量 ... }2.2 Java 21:switch 异常处理(预览特性 JEP 8323658)
// 传统方式:需在 switch 外 try-catch try { Objectresult=switch (input) { case Integer i -> processNumber(i); case String s -> processString(s); default -> thrownewIllegalArgumentException("不支持的类型"); }; } catch (IllegalArgumentException e) { log.error("处理失败", e); } // Java 21+:在 switch 中处理异常(预览特性) Objectresult=switch (input) { case Integer i -> processNumber(i); case String s -> processString(s); casenull -> thrownewNullPointerException("输入不能为空"); default -> thrownewIllegalArgumentException("不支持的类型: " + input); };优势:将 null 检查和异常处理整合到 switch 中,减少嵌套。
import io.vavr.control.Either; // 函数返回 Either<异常, 结果>,而非抛出异常 public Either<AppException, User> findUser(Long id) { try { Useruser= userRepository.findById(id); return user != null ? Either.right(user) : Either.left(newUserNotFoundException(id)); } catch (SQLException e) { return Either.left(newDatabaseException("查询失败", e)); } } // 调用方处理:链式调用,清晰分离成功/失败逻辑 findUser(123) .map(user -> user.getName()) // 成功时处理 .leftMap(e -> { // 失败时处理不同异常类型 if (e instanceof UserNotFoundException) { log.warn("用户不存在: {}", e.getMessage()); return"默认用户"; } else { log.error("系统异常", e); return"系统错误"; } }) .forEach(name -> System.out.println("用户名: " + name));优势:
{ "errorCode":"USER_NOT_FOUND", "message":"用户ID为123的用户不存在", "details":{"userId":"123"}, "timestamp":"2025-08-15T10:30:00Z", "traceId":"abc-123-xyz"// 分布式追踪ID }4.2 异常转换:避免暴露内部实现
@FeignClient(name = "user-service") publicinterfaceUserServiceClient { @GetMapping("/users/{id}") UserDTO getUser(@PathVariable("id") Long id); } // 调用方处理Feign异常 public UserDTO getUser(Long id) { try { return userClient.getUser(id); } catch (FeignException e) { if (e.status() == 404) { thrownewUserNotFoundException("用户不存在: " + id); } elseif (e.status() == 503) { thrownewServiceUnavailableException("用户服务暂时不可用", e); } else { thrownewSystemException("调用用户服务失败", e); } } }5. 单元测试:验证异常行为
// 使用 JUnit 5 测试异常 @Test voidshouldThrowUserNotFoundWhenIdInvalid() { // 准备 UserServiceservice=newUserService(mockRepo); // 执行 & 验证 assertThrows(UserNotFoundException.class, () -> service.getUser(999L), "当用户ID不存在时,应抛出UserNotFoundException"); // 验证异常详情 UserNotFoundExceptionexception= assertThrows(UserNotFoundException.class, () -> service.getUser(999L)); assertEquals("USER_NOT_FOUND", exception.getErrorCode()); assertTrue(exception.isRetryable() == false); }关键测试点:
• 资源是否正确释放(如数据库连接关闭)。
四、构建健壮的异常处理体系