2. @ControllerAdvice 与 @ExceptionHandler:基础用法、优先级规则、返回自定义错误响应体

在 Spring Boot 错误处理体系中,@ControllerAdvice 与 @ExceptionHandler 是构建全局异常拦截器的核心组合。它们允许你集中定义异常处理逻辑,避免在每个 Controller 中重复编写 try-catch 代码,同时保证错误响应格式的统一性。

2.1 基础用法:全局异常拦截器

@ControllerAdvice 是一个组件注解,用于声明一个“全局控制器增强器”。它默认会拦截所有 @RequestMapping 方法抛出的异常,配合 @ExceptionHandler 指定具体处理的异常类型。

核心机制: @ControllerAdvice 内部通过 AOP 原理,在控制器方法执行前后织入增强逻辑。当方法抛出异常时,Spring 会遍历所有注册的 @ControllerAdvice 类,找到匹配的 @ExceptionHandler 方法进行处理。

标准实现步骤:

  1. 创建一个类,标注 @ControllerAdvice。
  2. 在类中定义方法,标注 @ExceptionHandler,参数为要捕获的异常类型。
  3. 方法返回 ResponseEntity 或自定义响应体,并设置 HTTP 状态码。
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            System.currentTimeMillis()
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "参数校验失败: " + ex.getMessage(),
            System.currentTimeMillis()
        );
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "服务器内部错误,请稍后重试",
            System.currentTimeMillis()
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
最佳实践: 始终在全局处理器中保留一个 @ExceptionHandler(Exception.class) 兜底方法,确保所有未明确处理的异常都能返回友好的错误信息,避免直接暴露堆栈跟踪给客户端。

2.2 优先级规则:谁先处理?

当多个 @ExceptionHandler 方法都能处理同一个异常时,Spring 遵循明确的优先级规则。理解这些规则是避免“异常被错误拦截”的关键。

优先级 规则 说明
1(最高) 异常类型精确匹配 优先选择参数类型与抛出异常类型完全一致的方法
2 异常类型继承链最近 若无精确匹配,选择继承层次中最近的父类异常处理方法
3 Controller 局部 > 全局 Controller 类内部的 @ExceptionHandler 优先级高于 @ControllerAdvice 全局处理器
4(最低) 兜底 Exception.class 所有异常最终可被 Exception.class 捕获,作为最后防线
常见陷阱: 如果同时定义了 @ExceptionHandler(IllegalArgumentException.class) 和 @ExceptionHandler(RuntimeException.class),当抛出 IllegalArgumentException 时,Spring 会优先选择精确匹配的 IllegalArgumentException 方法,而不是父类 RuntimeException 的方法。不要依赖方法定义的顺序来决定优先级。

优先级决策流程:

@ExceptionHandler 优先级决策流程 异常抛出 是否有精确匹配的异常类型? 精确匹配方法处理 继承链最近父类处理 兜底 Exception.class 优先级说明 1. 精确匹配 > 继承匹配 2. 局部 > 全局 3. 兜底永远最后执行

2.3 返回自定义错误响应体

为了给客户端提供一致的错误结构,建议定义统一的错误响应 DTO。这有助于前端解析错误信息,也便于 API 文档的生成。

标准错误响应体设计:

public class ErrorResponse {
    private int status;        // HTTP 状态码
    private String message;    // 错误描述
    private long timestamp;    // 错误发生时间戳
    private String path;       // 请求路径(可选)
    private String errorCode;  // 业务错误码(可选)

    // 全参构造方法
    public ErrorResponse(int status, String message, long timestamp) {
        this.status = status;
        this.message = message;
        this.timestamp = timestamp;
    }

    // 带路径的构造方法
    public ErrorResponse(int status, String message, long timestamp, String path) {
        this.status = status;
        this.message = message;
        this.timestamp = timestamp;
        this.path = path;
    }

    // Getters and Setters
    public int getStatus() { return status; }
    public String getMessage() { return message; }
    public long getTimestamp() { return timestamp; }
    public String getPath() { return path; }
    public String getErrorCode() { return errorCode; }
    public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
}

在全局处理器中注入请求路径:

@ControllerAdvice
public class EnhancedGlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            ResourceNotFoundException ex,
            HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            System.currentTimeMillis(),
            request.getRequestURI()
        );
        error.setErrorCode("RESOURCE_NOT_FOUND");
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex,
            HttpServletRequest request) {
        String message = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .collect(Collectors.joining(", "));
        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            message,
            System.currentTimeMillis(),
            request.getRequestURI()
        );
        error.setErrorCode("VALIDATION_FAILED");
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
}
增强建议: 在错误响应中加入 errorCode 字段,用于标识业务层面的错误类型(如 INVALID_PARAM、USER_NOT_FOUND)。这比仅依赖 HTTP 状态码更精确,便于客户端做差异化处理。

2.4 多 ControllerAdvice 共存时的优先级

当项目中存在多个 @ControllerAdvice 类时,Spring 通过 @Order 注解或 Ordered 接口控制执行顺序。

排序方式 示例 优先级
@Order(1) 特定模块的异常处理器 高(数值越小优先级越高)
@Order(2) 通用异常处理器 中
无 @Order 兜底处理器 低(默认最低优先级)
@Order(1)
@ControllerAdvice(basePackages = "com.example.order")
public class OrderModuleExceptionHandler {
    @ExceptionHandler(OrderNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleOrderNotFound(OrderNotFoundException ex) {
        // 订单模块专属处理
    }
}

@Order(2)
@ControllerAdvice
public class CommonExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
        // 通用兜底处理
    }
}
注意: 当多个 @ControllerAdvice 都定义了处理同一异常类型的方法时,只有 @Order 值最小的那个方法会被执行。其他方法会被忽略,不会形成“链式调用”。因此,建议将精确的异常处理放在高优先级的 Advice 中,将兜底处理放在低优先级的 Advice 中。

2.5 实战对比:局部 vs 全局异常处理

下表总结了 @ControllerAdvice 全局处理与 Controller 内部局部处理的适用场景:

维度 全局 @ControllerAdvice 局部 @ExceptionHandler
适用范围 所有 Controller(或指定包) 仅当前 Controller 类
代码复用 高,一处定义全局生效 低,每个 Controller 需重复定义
优先级 低(被局部覆盖) 高(优先于全局)
推荐场景 通用异常、统一响应格式 特殊业务逻辑、需要特殊响应的场景
维护成本 低,集中管理 高,分散在各 Controller

推荐策略: 以全局 @ControllerAdvice 为主,处理 90% 的通用异常;仅在极少数需要特殊响应格式的 Controller 中使用局部 @ExceptionHandler 覆盖默认行为。