本文目录导读:

对于Java中的异常处理流程规整,核心目标是:可读性强、职责单一、错误信息不丢失、避免反模式。
以下是一套高内聚、低耦合的规整方案,结合了最佳实践、代码分层以及现代Java特性。
顶层设计原则
在写具体代码前,先确立几个原则:
- 异常是异常,不是业务逻辑:不要用
try-catch控制流程(如用NumberFormatException判断字符串是否为数字)。 - 早抛出,晚捕获:在发现错误的第一时间抛出异常;在能真正处理它的地方(通常是Service或Controller层)捕获。
- 拥抱Unchecked Exception:继承
RuntimeException(非受检异常),减少throws声明对上层调用的污染。 - 禁止吞食异常:
catch之后什么都不做,或者只打日志却继续返回错误结果。
规整的最佳实践模式
定义统一的异常类体系
不要到处用 new Exception("msg"),建立一个规范化的结构。
// 基础业务异常
public class BusinessException extends RuntimeException {
private final String code; // 错误码,用于前端或API调用方识别
private final String message;
public BusinessException(String code, String message) {
super(message); // 保留给父类用于StackTrace
this.code = code;
this.message = message;
}
public BusinessException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.message = message;
}
// Getter...
}
使用“封箱”模式:要么返回结果,要么抛出异常
避免方法内部既有返回值又悄悄处理异常,导致调用方困惑。
// ❌ 反模式:返回null表示失败
public User findUser(Long id) {
try {
return userMapper.selectById(id);
} catch (DataAccessException e) {
log.error("查询用户失败", e);
return null; // 调用方拿到null后还要判断
}
}
// ✅ 规整做法:明确声明可能抛出的异常
public User findUserOrThrow(Long id) {
try {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException("USER_NOT_FOUND", "用户不存在: " + id);
}
return user;
} catch (DataAccessException e) {
// 转换异常类型,让上层统一处理
throw new BusinessException("DB_ERROR", "数据库查询失败", e);
}
}
分层异常处理规范
不同的层关注点不同,异常处理要分层负责。
| 层级 | 职责 | 异常处理方式 |
|---|---|---|
| Controller | 接收请求,返回响应 | 不处理具体业务异常,交给全局异常处理器 |
| Service | 核心业务逻辑 | 抛出 BusinessException 或 调用外部依赖的异常 |
| Repository/DAO | 数据访问 | 抛出 DataAccessException 或 PersistenceException |
| Infrastructure | 网络、IO、第三方 | 捕获底层异常,转换成业务层能理解的类型 |
@Service
public class OrderService {
public void createOrder(OrderDTO dto) {
// 1. 参数校验(最好前置)
if (dto.getAmount() < 0) {
throw new BusinessException("INVALID_AMOUNT", "金额不能为负");
}
// 2. 业务逻辑
try {
// 调用余额服务(可能网络超时)
boolean balanceResult = balanceClient.deduct(dto.getUserId(), dto.getAmount());
} catch (TimeoutException e) {
// Infrastructure层的异常 -> 转为BusinessException
throw new BusinessException("BALANCE_TIMEOUT", "余额服务调用超时", e);
}
// 3. 更多逻辑...
}
}
统一响应与全局异常处理(Spring Boot 实践)
这是“规整”最关键的一步。不要让每个Controller方法都写 try-catch。
定义统一的响应体
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String code; // 业务码
private String message;
private T data;
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, "SUCCESS", "操作成功", data);
}
public static <T> ApiResponse<T> error(String code, String message) {
return new ApiResponse<>(false, code, message, null);
}
}
全局异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理所有自定义的业务异常
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK) // 前端注意看 code,而非 HTTP 状态码
public ApiResponse<Void> handleBusinessException(BusinessException e) {
// 可以在这里记录错误日志,但不要影响返回
log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
return ApiResponse.error(e.getCode(), e.getMessage());
}
// 处理参数校验异常(@Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> handleValidationException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return ApiResponse.error("VALIDATION_ERROR", msg);
}
// 处理未预料的异常(兜底)
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResponse<Void> handleUnknownException(Exception e) {
log.error("系统未知异常", e); // 需要记录详细堆栈
return ApiResponse.error("SYSTEM_ERROR", "服务器繁忙,请稍后重试");
}
}
代码级规整技巧
使用 Optional 代替 if (obj == null)
// ❌
public String getCity(User user) {
if (user != null && user.getAddress() != null) {
return user.getAddress().getCity();
}
return "未知";
}
// ✅
public String getCity(User user) {
return Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("未知");
}
注意:Optional 不要用作方法参数类型,它只适合做返回值。
使用 try-with-resources 自动关闭资源
Java 7+ 特性,避免 finally 中手动关闭造成的异常丢失。
// ✅ 自动关闭 InputStream 和 SQL 连接
try (InputStream is = new FileInputStream("data.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
String line = br.readLine();
} catch (IOException e) {
log.error("文件读取失败", e);
}
异常信息要包含上下文
// ❌ 不好的信息
log.error("插入失败");
// ✅ 好的信息
log.error("插入用户失败, userId={}, userName={}, cause={}", user.getId(), user.getName(), e.getMessage());
避坑指南(必须避免的反模式)
| 反模式 | 问题 | 规整做法 |
|---|---|---|
| 捕获 Exception 却不处理 | 隐藏了Bug,系统静默失效 | 至少打日志,并明确抛出业务异常或返回错误 |
| 用异常控制流程 | 性能极差(Stack Trace开销) | 用 if-else 或 Optional 取代 |
| 捕获后 throw new Exception() | 丢失原始异常堆栈 | 用 throw new BusinessException(msg, e) 传入原异常 |
| 在 finally 中使用 return | 吞掉 try 块中的异常 | 不用在 finally 中 return,或使用 try-with-resources |
| 方法签名抛出大量受检异常 | 调用方被迫 caught 或 propagate | 统一转为非受检异常(RuntimeException) |
高级:使用 Result 模式(函数式风格)
如果不喜欢异常,或者需要显式展示所有可能结果(如CQRS、FP风格),可以使用 Result 类型(类似Vavr或Java官方计划中的Result),但大多数Java项目使用“全局异常处理”已经足够规整了。
// 当你需要在一个方法里既返回成功值,也返回失败信息,并且不想抛异常时
public sealed interface Result<T> permits Success, Failure {
static <T> Result<T> ok(T value) { ... }
static <T> Result<T> fail(String msg) { ... }
}
// 使用
Result<User> result = userService.findUser(1L);
User user = switch (result) {
case Success(var v) -> v;
case Failure(var m) -> throw new BusinessException("ERROR", m);
};
规整流程检查清单
- 定义清晰异常体系:
BusinessException+ 错误码。 - 分层治理:Controller 不处理业务异常(交给
@RestControllerAdvice);Service 不捕获底层异常(抛出去)。 - 全局统一:只有一个地方(
GlobalExceptionHandler)负责拼接ApiResponse。 - 日志规范:捕获未知异常必须记录完整堆栈;业务异常只记录必要信息。
- 禁止吞食:catch 后要么记录 + 抛出,要么转换为更具体的异常。
- 保持简单:能用
if-else解决,就不要用异常。
按照这套方案,你的Java异常处理流程会变得清晰、可维护,并且对团队协作非常友好。