Java异常处理最佳实践

#java #最佳实践

总结
  • 捕获特定异常,不生吞,至少记日志并保留异常链
  • 早抛出(入口快速失败)、晚捕获(有业务上下文的地方统一处理)
  • 业务异常用 unchecked(继承 RuntimeException),配合 @ControllerAdvice 全局处理
  • 记日志一定传异常对象 log.error("msg", e),只记 e.getMessage() 会丢堆栈

1. 先说结论

几条核心原则:

2. 捕获特定异常

// 别这么写,太宽泛,会把不该捕获的异常也吃掉
try {
    Thread.sleep(1000L);
} catch (Exception e) { }

// 应该这么写
try {
    Thread.sleep(1000L);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // 恢复中断标志
    throw new RuntimeException("线程被中断", e);
}

3. 不生吞异常

// 最差的写法,出了问题完全不知道
try {
    doSomething();
} catch (IOException e) {
    // 什么都不做
}

// 至少记日志,保留完整堆栈(注意要传 e,不是 e.getMessage())
try {
    doSomething();
} catch (IOException e) {
    log.error("处理失败", e);
    throw new BizException("处理失败", e); // 保留异常链
}

4. 早抛出,晚捕获

早抛出:入口处就检查参数,快速失败,方便定位问题。

// 别等到深处才报 NPE,调用栈一长就难找了
public void readFile(String filename) {
    Objects.requireNonNull(filename, "filename 不能为空");
    // 后续逻辑...
}

晚捕获:底层不知道怎么处理就别捕获,让异常往上抛,在有业务上下文的地方统一处理。

// DAO 层:直接抛,不处理
public User findById(Long id) throws DataAccessException { ... }

// Service 层:转成业务异常
public User getUser(Long id) {
    try {
        return userDao.findById(id);
    } catch (DataAccessException e) {
        throw new BizException("用户查询失败", e);
    }
}

// Controller 层:交给全局异常处理器,不用每个接口都 catch

5. 业务异常用 unchecked

checked 异常(非 RuntimeException)会强迫调用链每一层都处理或声明,污染代码。Spring 框架本身也全用 unchecked,Effective Java 也建议避免不必要的 checked 异常。

// 自定义业务异常,继承 RuntimeException
public class BizException extends RuntimeException {
    private final String code;

    public BizException(String code, String message) {
        super(message);
        this.code = code;
    }

    public BizException(String message, Throwable cause) {
        super(message, cause);
        this.code = "SYSTEM_ERROR";
    }
}

6. 全局异常处理

别在每个 Controller 里各自 catch,用 @ControllerAdvice 统一处理:

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 业务异常:返回具体错误信息
    @ExceptionHandler(BizException.class)
    public Result<Void> handleBizException(BizException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.fail(e.getCode(), e.getMessage());
    }

    // 参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidException(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
            .mapgetDefaultMessage
            .collect(Collectors.joining(", "));
        return Result.fail("PARAM_ERROR", msg);
    }

    // 兜底:未知异常不暴露内部细节
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.fail("SYSTEM_ERROR", "系统繁忙,请稍后重试");
    }
}

7. 几个注意点