Skip to content
第 47 章 后端 ⏱ 10 分钟阅读

第 47 章:统一返回与异常处理 ​

学习目标 ​

  • 自定义统一返回 Result<T>
  • 全局异常处理器
  • 业务异常统一封装

一、统一返回 Result<T> ​

问题:每个接口返回不同结构,前端要写一堆 if-else。

解决:所有接口统一返回 Result<T>。

java
@Data
public class Result<T> {
    private int code;        // 200 = 成功,其他 = 失败
    private String message;
    private T data;
    private String traceId;  // 链路 ID,方便排查

    public static <T> Result<T> ok(T data) {
        Result<T> r = new Result<>();
        r.setCode(200);
        r.setMessage("OK");
        r.setData(data);
        return r;
    }

    public static <T> Result<T> error(int code, String message) {
        Result<T> r = new Result<>();
        r.setCode(code);
        r.setMessage(message);
        return r;
    }
}

Controller:

java
@GetMapping("/{id}")
public Result<UserVO> getById(@PathVariable Long id) {
    return Result.ok(userService.getById(id));
}

二、错误码枚举 ​

java
public enum ErrorCode {
    SUCCESS(200, "OK"),
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未登录"),
    FORBIDDEN(403, "无权限"),
    NOT_FOUND(404, "资源不存在"),
    INTERNAL_ERROR(500, "服务器内部错误"),
    // 业务错误(10000+)
    USER_NOT_FOUND(10001, "用户不存在"),
    USER_PASSWORD_ERROR(10002, "密码错误"),
    ORDER_ALREADY_PAID(20001, "订单已支付");

    private final int code;
    private final String message;
    ErrorCode(int code, String message) { this.code = code; this.message = message; }
    public int getCode() { return code; }
    public String getMessage() { return message; }
}

三、业务异常 ​

java
public class BusinessException extends RuntimeException {
    private final int code;

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
    }

    public BusinessException(ErrorCode errorCode, String message) {
        super(message);
        this.code = errorCode.getCode();
    }

    public int getCode() { return code; }
}

用法:

java
public User getById(Long id) {
    return userMapper.selectById(id)
        .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
}

⚠️ 坑 1:业务异常用 RuntimeException,不要继承 Exception(受检异常,会强制 try-catch,污染代码)。

四、@RestControllerAdvice 全局异常处理 ​

java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // ① 业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusiness(BusinessException e) {
        log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    // ② 参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
            .map(f -> f.getField() + ": " + f.getDefaultMessage())
            .collect(Collectors.joining("; "));
        return Result.error(400, msg);
    }

    // ③ 兜底:未知异常
    @ExceptionHandler(Exception.class)
    public Result<Void> handleUnknown(Exception e) {
        log.error("系统异常", e);              // 详细日志
        return Result.error(500, "服务器开小差,请稍后再试");  // 友好提示
    }
}

⚠️ 坑 2:⚠️ 兜底里千万别把异常 message 直接返回给前端!会泄露 SQL、文件路径等内部信息。

五、404 也要走 Result ​

java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NoHandlerFoundException.class)
    public Result<Void> handle404(NoHandlerFoundException e) {
        return Result.error(404, "接口不存在: " + e.getRequestURL());
    }
}

需要在 application.yml 开启:

yaml
spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false   # 禁用默认静态资源处理

六、异常处理顺序 ​

Spring 按最具体匹配:

BusinessException → 走 handler1
MethodArgumentNotValidException → 走 handler2
Exception → 走 handler3(兜底)

⚠️ 坑 3:兜底 handler 里千万不要 throw new RuntimeException(e)——会再走一遍,然后死循环。

七、自定义错误码的国际化 ​

如果需要中英文切换,把 message 放 messages.properties:

properties
# messages_zh_CN.properties
error.user.not_found=用户不存在
error.user.password_error=密码错误
java
public BusinessException(ErrorCode code, Object... args) {
    super(messageSource.getMessage(code.getMessageKey(), args, LocaleContextHolder.getLocale()));
}

简单项目不用做国际化,直接把中文 message 放在枚举里就行。

八、本章小结 ​

要点关键
统一返回Result<T> + 静态工厂方法
错误码enum 集中管理
业务异常extends RuntimeException
全局处理@RestControllerAdvice + @ExceptionHandler
兜底详细日志 + 友好提示,不泄露内部信息

动手练习 ​

  1. 实现 Result<T>、ErrorCode、BusinessException、GlobalExceptionHandler
  2. 写个 controller 抛 BusinessException,看返回结构
  3. 故意写个除零异常,看兜底 handler 是否安全

下一章:第 48 章:参数校验 →

本站基于 VitePress 构建 · 由 StackHub 团队维护