LarryDpk
发布于 2025-05-10 / 7 阅读
0

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

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 + @ResponseBodypublic 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。
  1. 返回流/文件下载
  • Controller 方法直接输出文件流时如果抛异常,可能已写入响应头导致无法再改 HTTP status;需在业务层提前校验,或使用自定义异常提示前端“下载失败”。
  1. 重复包装异常
  • 建议业务层只抛 业务异常 或直接抛原生异常,不要层层 try/catch 再 throw new RuntimeException,会丢失原始堆栈;在全局处理器里统一记录原始 cause。
  1. 未对接监控告警
  • 全局异常处理做好了,但若没有把严重异常接入 APM / Prometheus Alertmanager,会导致后台静默报错无法发现。
  • 建议在 @ExceptionHandler(Exception.class) 里增加异常计数埋点或调用告警接口。

5. 小结 Checklist ✅

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

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