引言 异常处理看似简单——try-catch-finally 谁都会写。但在实际项目中,”怎么处理异常”远比”怎么捕获异常”更难回答。你是否见过这样的代码:空 catch 块、catch(Exception e) 一把梭、return null 代替异常传播、日志打印后又重新抛出?本文将梳理 Java 异常处理体系,剖析常见的反模式,并结合 Spring 框架和异步场景给出生产级最佳实践。
Java 异常体系 Java 中所有异常事件都继承自 java.lang.Throwable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Throwable ├── Error(错误) │ ├── OutOfMemoryError │ ├── StackOverflowError │ └── NoClassDefFoundError └── Exception(异常) ├── RuntimeException(运行时异常,非受检) │ ├── NullPointerException │ ├── IllegalArgumentException │ └── IndexOutOfBoundsException └── Checked Exception(受检异常) ├── IOException ├── SQLException └── ClassNotFoundException
Error :表示 JVM 层面的严重问题,应用程序不应也通常无法处理。例如 OutOfMemoryError、StackOverflowError。
Checked Exception :编译器强制要求处理(try-catch 或 throws 声明)。设计意图是迫使开发者预见并处理可恢复的错误情况。
RuntimeException :非受检异常,编译器不强制处理。通常表示编程错误(逻辑 bug),应当在开发阶段修复而非运行时捕获。
Checked vs Unchecked:永恒的争议 受检异常的初衷是好的——让 API 的异常语义显式化。但在实践中它带来了明显的问题:
传播成本高 :调用链上的每个方法都必须声明 throws,导致接口签名膨胀
Lambda 不友好 :函数式接口的抽象方法没有声明 throws,受检异常在 Stream 中难以处理
滥用导致”吞异常” :开发者为了快速消除编译错误,直接写空 catch 块
目前业界的主流观点倾向于:业务异常使用非受检异常,基础设施异常在适当层级转换处理 。Spring 框架的设计选择也印证了这一趋势——DataAccessException 及其子类均为非受检异常。
Try-With-Resources Java 7 引入的 try-with-resources 是异常处理领域最实用的语法改进。它确保实现了 AutoCloseable 接口的资源在代码块执行完毕后被自动关闭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 BufferedReader br = null ;try { br = new BufferedReader (new FileReader ("file.txt" )); return br.readLine(); } finally { if (br != null ) br.close(); } try (BufferedReader br = new BufferedReader (new FileReader ("file.txt" ))) { return br.readLine(); } try (Connection conn = dataSource.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql); ResultSet rs = stmt.executeQuery()) { }
关键细节:close() 抛出的异常会被抑制(suppressed) ——如果 try 块和 close() 都抛出了异常,try 块中的异常被抛出,close() 的异常被附加为 suppressed exception,可通过 getSuppressed() 获取。
异常链与异常转译 异常链(Exception Chaining)允许在抛出新异常时保留原始异常信息:
1 2 3 4 5 try { } catch (SQLException e) { throw new DataAccessException ("Failed to query user" , e); }
异常转译(Exception Translation)是在层次边界 处将底层异常转换为有业务含义的高层异常。例如:
数据访问层捕获 SQLException,转换为 DataAccessException
服务层捕获 DataAccessException,转换为 UserNotFoundException
控制器层捕获 UserNotFoundException,转换为 HTTP 404 响应
层次化异常设计 是分层架构的重要组成部分:低层不应向高层暴露实现细节,高层不应关心低层使用什么数据库或 RPC 框架。
最佳实践 1. 永远不要吞掉异常 1 2 3 4 5 6 7 8 9 10 11 12 13 try { doSomething(); } catch (Exception e) { } try { doSomething(); } catch (Exception e) { e.printStackTrace(); }
如果确实不需要处理异常,至少要记录日志并说明理由:
1 2 3 4 5 try { cache.put(key, value); } catch (Exception e) { log.warn("Failed to update cache for key={}, continuing" , key, e); }
2. 尽早失败(Fail-Fast) 错误发生的时间点越接近根源,排查成本越低。在方法入口处验证参数并立即抛出异常:
1 2 3 4 5 6 7 public void process (Order order) { Objects.requireNonNull(order, "Order must not be null" ); if (order.getAmount().compareTo(BigDecimal.ZERO) <= 0 ) { throw new IllegalArgumentException ("Order amount must be positive" ); } }
3. 抛具体的异常 1 2 3 4 5 6 7 8 9 10 throw new Exception ("Something went wrong" );throw new OrderNotFoundException ("Order #" + orderId + " not found" );throw new InsufficientBalanceException ( String.format("Account %s balance %.2f is insufficient for amount %.2f" , accountId, balance, amount));
4. 异常消息要有价值 好的异常消息应该回答三个问题:发生了什么、涉及什么数据、当前状态是什么 。
1 2 3 4 5 6 throw new IllegalArgumentException ("Invalid value" );throw new IllegalArgumentException ( "Product quantity must be positive, but was: " + quantity);
5. 不要用异常控制流程 异常机制设计用于异常情况 ,用它控制正常业务流程是严重的性能反模式:
1 2 3 4 5 6 7 8 9 10 11 try { for (int i = 0 ; ; i++) { arr[i] = process(arr[i]); } } catch (ArrayIndexOutOfBoundsException ignored) {} for (int i = 0 ; i < arr.length; i++) { arr[i] = process(arr[i]); }
常见反模式
反模式
表现
危害
万能捕获
catch(Exception e) 在最外层
掩盖真正错误,调试困难
错误时返回 null
catch { return null; }
调用方不知道出错了,NPE 在远端爆发
日志后重抛
catch { log.error(...); throw e; }
同一异常被多次记录,日志污染
空 catch
catch(Exception e) {}
完全隐蔽问题,生产环境难以排查
过度 try-catch
每个语句都包裹
代码可读性崩溃
在 finally 中写 return
finally { return ...; }
吞掉 try 块中的异常和原始返回值
Spring 框架中的异常处理 @ControllerAdvice 全局异常处理 @ControllerAdvice 配合 @ExceptionHandler 提供了集中的异常映射机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNotFound (ResourceNotFoundException ex) { return new ErrorResponse ("NOT_FOUND" , ex.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleValidation (MethodArgumentNotValidException ex) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(e -> e.getField() + ": " + e.getDefaultMessage()) .toList(); return new ErrorResponse ("VALIDATION_FAILED" , String.join("; " , errors)); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleAll (Exception ex) { log.error("Unexpected error" , ex); return new ErrorResponse ("INTERNAL_ERROR" , "An unexpected error occurred" ); } }
异步代码中的异常处理 @Async 方法中的异常不会自动传播到调用者,需要配合 AsyncUncaughtExceptionHandler:
1 2 3 4 5 6 7 8 9 @Configuration public class AsyncConfig implements AsyncConfigurer { @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler () { return (ex, method, params) -> log.error("Async method {} failed with params {}" , method.getName(), params, ex); } }
对于 CompletableFuture,使用 exceptionally() 或 handle() 进行处理:
1 2 3 4 5 CompletableFuture.supplyAsync(() -> fetchData()) .exceptionally(ex -> { log.error("Async fetch failed" , ex); return fallbackData(); });
自定义异常设计指南 一个设计良好的自定义异常应当:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class OrderProcessingException extends RuntimeException { private final String orderId; private final ErrorCode errorCode; public OrderProcessingException (ErrorCode errorCode, String orderId, String message) { super (message); this .errorCode = errorCode; this .orderId = orderId; } public enum ErrorCode { ORDER_NOT_FOUND, INSUFFICIENT_INVENTORY, PAYMENT_FAILED } }
关键原则:
从业务语义出发,而非技术细节(如 InsufficientInventoryException 而非 DatabaseException)
携带足够的上下文信息,便于排查(订单号、用户 ID、时间戳)
可以包含错误码枚举,方便监控和国际化
继承 RuntimeException(在现代实践中通常优先于 checked exception)
结语 异常处理是代码质量的试金石。一段异常处理设计得当的代码,可以让运维团队在凌晨三点快速定位问题根因;而一个被吞掉的异常,可能让排查成本从十分钟膨胀到几天。始终记住:异常不是为了”不出错”,而是为了”出错时能快速知道发生了什么”。敬畏异常,就是敬畏生产环境的稳定和可观测性。