人人都会AI编程

12.6 全局异常处理:@RestControllerAdvice + @ExceptionHandler

更新时间:2026-07-11

一个对外提供 REST API 的后端系统,如果不对异常做统一处理,客户端可能会看到满屏的错误堆栈、状态码混乱的 HTML 页面,或者最糟糕的——因为未捕获异常导致整个请求直接返回 500 却没有任何业务上下文。全局异常处理的目标就是:无论应用的哪一层抛出何种异常,最终都以统一、友好的 JSON 结构返回给调用方,同时内部能够完整记录错误信息。

Spring 为这一需求提供了极其简洁的解决方案:@RestControllerAdvice + @ExceptionHandler

12.6.1 未处理异常带来的问题

假设 Controller 中有一个查询接口,可能会因为参数非法而抛出自定义异常:

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    if (id <= 0) {
        throw new IllegalArgumentException("用户ID必须为正数");
    }
    return userService.findById(id);
}

如果没有全局异常处理,调用 /users/-1 会得到类似这样的响应:

{
    "timestamp": "2025-03-15T10:20:30.123",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/users/-1"
}

客户端收到的是一个毫无业务含义的 500 错误,真正的错误信息“用户ID必须为正数”被淹没在服务端日志中。这种不一致的响应格式还增加前端处理的成本——不同的异常类型可能返回不同的 JSON 结构(有的是 Spring Boot 默认的错误页面,有的是框架底层抛出的片段)。

12.6.2 @ExceptionHandler 与 @RestControllerAdvice 的原理

@ExceptionHandler 可标注在 Controller 类中的方法上,用于捕获该 Controller 内部抛出的特定异常。但如果每个 Controller 都写一遍异常处理逻辑,仍会大量重复。@RestControllerAdvice 则是 @ControllerAdvice + @ResponseBody 的组合,它能使异常处理方法全局生效,并自动将返回值序列化为 JSON。

两者的结合意味着:你可以在一个独立的类中,用若干方法定义对不同异常的处理方式,这些方法将拦截所有 Controller 抛出的异常,并返回统一格式的响应。

12.6.3 构建统一响应格式

全局异常处理的第一步是定义一套固定的 API 响应结构。通常包含状态码、消息、数据(可选)和异常详情(可选,开发环境提供更多信息):

public class ApiResult<T> {
    private int code;
    private String message;
    private T data;

    public static <T> ApiResult<T> success(T data) {
        ApiResult<T> result = new ApiResult<>();
        result.code = 200;
        result.message = "ok";
        result.data = data;
        return result;
    }

    public static ApiResult<Void> error(int code, String message) {
        ApiResult<Void> result = new ApiResult<>();
        result.code = code;
        result.message = message;
        return result;
    }

    // getters and setters ...
}

所有正常响应都通过 ApiResult 包装,异常处理也返回同样的结构,保证前端只面对一种形状的数据。

12.6.4 设计自定义业务异常

建议为业务错误设计一个基础异常类,携带业务错误码和消息:

public class BusinessException extends RuntimeException {
    private final int code;

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

    public int getCode() { return code; }
}

具体的业务场景可以继承或直接使用它:

if (user == null) {
    throw new BusinessException(40401, "用户不存在");
}
if (balance < amount) {
    throw new BusinessException(40002, "余额不足");
}

12.6.5 编写全局异常处理器

使用 @RestControllerAdvice 统一拦截各类异常,将其映射到自定义的响应结构:

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 1. 处理自定义业务异常
    @ExceptionHandler(BusinessException.class)
    public ApiResult<Void> handleBusinessException(BusinessException e) {
        // 业务异常通常只记录警告,因为属于预期内异常
        log.warn("业务异常:code={}, message={}", e.getCode(), e.getMessage());
        return ApiResult.error(e.getCode(), e.getMessage());
    }

    // 2. 处理参数校验异常(如 @Valid 校验失败)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResult<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(err -> err.getField() + " " + err.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return ApiResult.error(40001, "参数校验失败:" + message);
    }

    // 3. 处理其他未预期的异常(兜底)
    @ExceptionHandler(Exception.class)
    public ApiResult<Void> handleUnknownException(Exception e) {
        // 系统异常应记录完整的错误堆栈,方便排查
        log.error("系统异常:", e);
        return ApiResult.error(50000, "服务器内部错误,请稍后重试");
    }
}

几个关键点:

  • @ExceptionHandler 方法可以接收被捕获的异常对象,还能接收 HttpServletRequestHttpServletResponse 等参数以便按需操作。
  • 注解的 value 属性指定要处理的异常类型,不指定则默认为方法参数列表中的异常类型。
  • 处理多个异常时可以定义多个方法,Spring 会自动匹配最具体的异常类型。
  • 方法的返回值会直接通过消息转换器写入 HTTP 响应体,因此可返回 ApiResultResponseEntity 甚至 void

12.6.6 适应不同环境返回不同的错误细节

在生产环境中,最好不要把异常堆栈返回给客户端,避免泄露系统细节。可以在开发时多返回一些调试信息:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @Value("${spring.profiles.active:dev}")
    private String activeProfile;

    @ExceptionHandler(Exception.class)
    public ApiResult<?> handleUnknownException(Exception e, HttpServletRequest request) {
        String message = "prod".equals(activeProfile) 
                ? "服务器内部错误" 
                : e.getMessage();  // dev/test 环境返回具体异常信息
        log.error("请求 {} 发生系统异常", request.getRequestURI(), e);
        return ApiResult.error(50000, message);
    }
}

12.6.7 常用异常类型处理示例

下面是一个更完整的处理器示例,覆盖了实际项目中最常见的异常类型:

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 自定义业务异常
    @ExceptionHandler(BusinessException.class)
    public ApiResult<Void> handleBusiness(BusinessException e) {
        return ApiResult.error(e.getCode(), e.getMessage());
    }

    // 参数校验失败(@Valid)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResult<Void> handleValid(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldError().getDefaultMessage();
        return ApiResult.error(40001, msg);
    }

    // 参数绑定失败(如类型转换错误)
    @ExceptionHandler(BindException.class)
    public ApiResult<Void> handleBind(BindException e) {
        String msg = e.getFieldError().getDefaultMessage();
        return ApiResult.error(40002, "参数错误:" + msg);
    }

    // 权限不足(Spring Security 常用)
    @ExceptionHandler(AccessDeniedException.class)
    public ApiResult<Void> handleAccessDenied(AccessDeniedException e) {
        return ApiResult.error(40300, "无访问权限");
    }

    // 404 资源不存在(可配合自定义异常)
    @ExceptionHandler(NoHandlerFoundException.class)
    public ApiResult<Void> handleNotFound(NoHandlerFoundException e) {
        return ApiResult.error(40400, "接口不存在");
    }

    // 兜底处理系统异常
    @ExceptionHandler(Exception.class)
    public ApiResult<Void> handleException(Exception e) {
        log.error("系统异常:", e);
        return ApiResult.error(50000, "服务器内部错误");
    }
}

12.6.8 实践中的注意事项

  1. 异常处理顺序:Spring 会找最匹配的 @ExceptionHandler 方法;如果多个方法的异常类型存在父子关系,具体的优先。需要注意,如果定义了一个 Exception 的处理器,务必将所有自定义异常的处理置于其前。
  2. 避免吞掉重要异常:日志记录必不可少,尤其是系统级异常。切记不要只返回一个模糊消息而没有任何日志,否则线上问题无从查起。
  3. 区分业务异常与系统异常:业务异常是程序预期的处理结果(如余额不足、订单已取消),通常只需记录 WARN 或 INFO 日志;系统异常(NPE、数据库连接失败等)则需要 ERROR 日志并触发告警。
  4. 与 HTTP 状态码配合:虽然不是强制要求,但合理使用 HttpStatus 能让 API 更符合 REST 规范。可以使用 ResponseEntity 返回自定义的 HTTP 状态码:
   @ExceptionHandler(BusinessException.class)
   public ResponseEntity<ApiResult<Void>> handleBusiness(BusinessException e) {
       return ResponseEntity.badRequest().body(ApiResult.error(e.getCode(), e.getMessage()));
   }
   
  1. Spring Security 的异常链:如果应用中使用了 Spring Security,认证相关异常(如 AuthenticationException)是在 Filter 层抛出的,@RestControllerAdvice 默认只能拦截 DispatcherServlet 内的异常。需要配置 authenticationEntryPointaccessDeniedHandler 来统一处理,或借助 Spring Security 提供的转发机制将异常重新抛入 DispatcherServlet。

全局异常处理是构建健壮 API 的基础设施之一。通过 @RestControllerAdvice + @ExceptionHandler,你可以用极少的代码将杂乱无章的异常输出统一为理性的、结构化的信息,既方便客户端对接,也大大提升了服务端的可维护性和问题排查效率。