Java响应异常流程如何规整

wen java案例 28

本文目录导读:

Java响应异常流程如何规整

  1. 顶层设计原则
  2. 规整的最佳实践模式
  3. 统一响应与全局异常处理(Spring Boot 实践)
  4. 代码级规整技巧
  5. 避坑指南(必须避免的反模式)
  6. 高级:使用 Result 模式(函数式风格)
  7. 规整流程检查清单

对于Java中的异常处理流程规整,核心目标是:可读性强、职责单一、错误信息不丢失、避免反模式

以下是一套高内聚、低耦合的规整方案,结合了最佳实践代码分层以及现代Java特性

顶层设计原则

在写具体代码前,先确立几个原则:

  1. 异常是异常,不是业务逻辑:不要用 try-catch 控制流程(如用 NumberFormatException 判断字符串是否为数字)。
  2. 早抛出,晚捕获:在发现错误的第一时间抛出异常;在能真正处理它的地方(通常是Service或Controller层)捕获。
  3. 拥抱Unchecked Exception:继承 RuntimeException(非受检异常),减少 throws 声明对上层调用的污染。
  4. 禁止吞食异常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 数据访问 抛出 DataAccessExceptionPersistenceException
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-elseOptional 取代
捕获后 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);
};

规整流程检查清单

  1. 定义清晰异常体系BusinessException + 错误码。
  2. 分层治理:Controller 不处理业务异常(交给@RestControllerAdvice);Service 不捕获底层异常(抛出去)。
  3. 全局统一:只有一个地方(GlobalExceptionHandler)负责拼接 ApiResponse
  4. 日志规范:捕获未知异常必须记录完整堆栈;业务异常只记录必要信息。
  5. 禁止吞食:catch 后要么记录 + 抛出,要么转换为更具体的异常。
  6. 保持简单:能用 if-else 解决,就不要用异常。

按照这套方案,你的Java异常处理流程会变得清晰、可维护,并且对团队协作非常友好。

抱歉,评论功能暂时关闭!