# Spring MVC / Spring Boot全局异常处理实践指南


# 1. 关键机制速览

机制 场景 触发顺序 说明
@ExceptionHandler 处理特定控制器(或其父类)抛出的异常 写在控制器里:局部异常处理
@ControllerAdvice / @RestControllerAdvice + @ExceptionHandler 全局处理所有控制器异常 推荐做全局兜底;@RestControllerAdvice 自动 @ResponseBody
实现 HandlerExceptionResolver / 继承 ResponseEntityExceptionHandler 需定制高度个性化逻辑或集成第三方框架 Spring MVC 内部 SPI;优先级可配置
Filter / Aspect 级别捕获 想覆盖 Filter → Servlet 链或 AOP 切面 处理更底层、非 MVC 线程抛出的异常
Spring Cloud Gateway / Feign Fallback / Hystrix… 分布式或网关层 - 属于微服务弹性保护,不在 MVC 处理链内

一般项目仅需 @RestControllerAdvice + @ExceptionHandler 即可满足 80% 以上场景。


# 2. 快速上手:@RestControllerAdvice

# 2.1 创建统一响应模型

// 通用响应包装
@Data
@AllArgsConstructor(staticName = "of")
public class ApiResponse<T> {
    private Integer code;     // 业务码
    private String  message;  // 提示语
    private T       data;     // 数据
}

# 2.2 定义业务异常

@Getter
public class BizException extends RuntimeException {
    private final Integer code;

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

# 2.3 编写全局异常处理器

@Slf4j
@RestControllerAdvice     // 等同于 @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    /** 处理业务异常 */
    @ExceptionHandler(BizException.class)
    public ResponseEntity<ApiResponse<Void>> handleBiz(BizException ex) {
        log.warn("业务异常: {}", ex.getMessage());
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.of(ex.getCode(), ex.getMessage(), null));
    }

    /** 处理校验失败(Bean Validation) */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<List<String>>> handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
                                .getFieldErrors()
                                .stream()
                                .map(err -> err.getField() + ": " + err.getDefaultMessage())
                                .toList();
        return ResponseEntity
                .badRequest()
                .body(ApiResponse.of(4001, "参数校验失败", errors));
    }

    /** 兜底:所有未处理的异常 */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleDefault(Exception ex) {
        log.error("系统异常", ex);
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.of(5000, "系统繁忙,请稍后再试", null));
    }
}

小贴士

  • 若希望统一返回 HTTP 200,而业务码区分错误,可把 ResponseEntity 改成 ApiResponse 并在 @ExceptionHandler 上添加 @ResponseStatus(HttpStatus.OK)
  • @RestControllerAdvice(basePackages="com.xxx.api") 可限制扫描范围,避免拦到 actuator 等端点。

# 3. 进阶技巧

# 3.1 自定义错误码枚举

@Getter
@AllArgsConstructor
public enum ErrorCode {
    // 通用
    OK(0, "成功"),
    INVALID_PARAM(4001, "参数错误"),
    SYSTEM_ERROR(5000, "系统错误"),
    ;

    private final int code;
    private final String msg;
}

BizException 中持有 ErrorCode,便于前后端枚举同步。

# 3.2 与前端契约统一

  • REST 接口建议 HTTP Status ≈ 协议层错误

    • 4xx → 客户端请求有误(400 参数错 / 401 未登录 / 403 无权限 / 404 资源不存在)
    • 5xx → 服务端错误
  • 业务错误用 响应体内的错误码 & message 描述,保持统一 JSON 结构。

# 3.3 处理异步线程异常

@ExceptionHandler 仅能捕捉 Spring MVC 调用线程 抛出的异常,若使用 @Async 或自定义线程池,可通过:

@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // ...
        executor.setTaskDecorator(r -> () -> {
            try { r.run(); }
            catch (Exception ex) { GlobalAsyncExceptionHandler.handle(ex); }
        });
        return executor;
    }
}

或给线程池设置 setRejectedExecutionHandler / setThreadFactory 收拢异常。

# 3.4 响应式 WebFlux

  • WebFlux 下仍可用 @RestControllerAdvice;返回类型可以是 Mono<ApiResponse<?>>

  • 若用 RouterFunction, 则需注册 onError 处理器:

    RouterFunctions.route()
        .GET("/api/x", handler::foo)
        .onError(BizException.class, (ex, req) -> ServerResponse
              .status(HttpStatus.BAD_REQUEST)
              .bodyValue(ApiResponse.of(ex.getCode(), ex.getMessage(), null)))
    

# 3.5 国际化 / 多语言

GlobalExceptionHandler 中注入 MessageSource,通过 LocaleContextHolder.getLocale() 获取当前语言,将错误码映射为多语言消息后返回。


# 4. 常见陷阱

  1. 异常被拦在过滤器 / 拦截器

    • OncePerRequestFilter 里手动 response.getWriter().write(...),Spring MVC 将不再进入异常解析链。
    • 解决:把异常往上抛或统一在 Filter 里构造 JSON。
  2. 返回流/文件下载

    • Controller 方法直接输出文件流时如果抛异常,可能已写入响应头导致无法再改 HTTP status;需在业务层提前校验,或使用自定义异常提示前端“下载失败”。
  3. 重复包装异常

    • 建议业务层只抛 业务异常 或直接抛原生异常,不要层层 try/catch 再 throw new RuntimeException,会丢失原始堆栈;在全局处理器里统一记录原始 cause。
  4. 未对接监控告警

    • 全局异常处理做好了,但若没有把严重异常接入 APM / Prometheus Alertmanager,会导致后台静默报错无法发现。
    • 建议在 @ExceptionHandler(Exception.class) 里增加异常计数埋点或调用告警接口。

# 5. 小结 Checklist ✅

  • [x] 使用 @RestControllerAdvice + 多个 @ExceptionHandler
  • [x] 设计统一 ApiResponseErrorCode前后端对齐
  • [x] 记录日志:业务异常用 warn,系统异常用 error
  • [x] 捕获 MethodArgumentNotValidException / ConstraintViolationException 解决校验提示。
  • [x] 若有异步 / 线程池,保证异常能上报。
  • [x] 对接监控与告警,让严重错误第一时间可见。

按上面思路落地,全局异常处理即可覆盖 Web、REST、接口校验、业务错误 等大部分场景,在生产中兼顾 一致性、可维护性、可观测性

上次更新: 2025/5/11 10:28:00