Skip to content

异常处理器

GlobalExceptionHandler 用一个 @RestControllerAdvice 类承担了 7 类异常的统一捕获与转换。

处理链路

@ExceptionHandler 命中后返回的 Result 会再经过 GlobalResponseAdvice,但后者发现 body 已是 Result 就会跳过二次包装,保留原对象输出。

7 个处理器逐一拆解

按照 GlobalExceptionHandler.java 中声明的顺序:

1. BusinessException

java
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK)
public Result<Void> handleBusinessException(BusinessException ex) {
    log.warn("业务异常: code={}, message={}", ex.getCode(), ex.getMessage());
    return Result.fail(ex.getCode(), ex.getMessage());
}

业务异常返回 HTTP 200,由 success=false 与 code 表达失败。这是一个有争议显式的选择:前端只需要看 success 与 code 两个字段,完全跳过 HTTP 状态码这个独立维度,简化客户端拦截逻辑。

2. MethodArgumentNotValidException

@RequestBody + @Valid 的字段级校验失败。处理器把所有 FieldError.getDefaultMessage(); 拼接成一条消息,HTTP 400,业务 code 400。

3. ConstraintViolationException

@Validated 与单参数(路径变量、Query 参数)校验失败时由 Hibernate Validator 抛出。处理逻辑与上一项类似,但读的是 ConstraintViolation.getMessage()

4. BindException

表单 / Query 参数绑定到 DTO 失败时抛出。同样拼接 FieldError 列表,状态码 400。

5. NoResourceFoundException

Spring Boot 3.x 在静态资源 404 的情况下抛出的标准异常。本处理器特意不打日志

java
@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<Void> handleNoResourceFoundException(NoResourceFoundException ex) {
    return Result.fail(CommonConstants.HTTP_NOT_FOUND, CommonConstants.RESOURCE_NOT_FOUND_MESSAGE);
}

原因:Spring Boot 3 升级后,连 favicon.ico 与各种 Robots 探测请求都会抛 NoResourceFoundException,再打 ERROR 日志会让生产环境日志被噪音淹没。

6. RuntimeException

未被前面拦下的所有 RuntimeException。以 ERROR 级别打日志(含完整堆栈),对外脱敏返回。

7. Exception 兜底

java
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleGlobalException(Exception ex) {
    log.error("系统异常: {}", ex.getMessage(), ex);
    return Result.fail(CommonConstants.HTTP_INTERNAL_SERVER_ERROR, CommonConstants.SERVER_ERROR_MESSAGE);
}

返回消息固定为 CommonConstants.SERVER_ERROR_MESSAGE(「服务器开小差了,请稍后重试」),不会把堆栈信息暴露给客户端。

状态码选择的取舍

异常HTTP 状态码选择备选项选定理由
BusinessException2004xx / 5xx简化前端单维度判断;牺牲一部分 RESTful 纯度
校验异常400422400 在国内项目更常见,工具链 / 监控对其语义识别更稳定
静态资源 404404200 + 自定义提示保留语义正确;改成 200 会与正常响应混淆
兜底异常500200 + code 区分区分「业务可恢复」与「系统不可恢复」,便于上层链路监控告警

与业务自定义 ControllerAdvice 共存

业务方完全可以再注册自己的 @RestControllerAdvice,只要满足:

  • 业务方处理的异常类型比框架更具体(例如业务方处理 MyAuthException,框架处理 RuntimeException)。Spring 会优先匹配更具体的处理器
  • 如果业务方想覆盖框架的某个 handler(例如自己处理 BusinessException),可以在自己的 Advice 类上加 @Order(Ordered.HIGHEST_PRECEDENCE),让 Spring 优先选中业务方的 Advice

别在业务 Advice 里再次抛异常

@ExceptionHandler 方法本身抛异常不会被同一个 Advice 链再次捕获,会直接走 Spring 的默认错误页或 ErrorController,破坏统一响应结构。

扩展点

想新增一类异常的统一处理(例如 OAuth2 的 InvalidTokenException)有两个选择:

  1. 在业务侧自己写一个 @RestControllerAdvice,处理自己关心的异常类型,与本模块共存
  2. 如果是框架级别的通用异常,向 GlobalExceptionHandler 提交 PR 加 @ExceptionHandler 方法即可

不需要改动 GlobalResponseAdvice —— 异常处理器返回的 Result 会被自动放行,不会被二次包装。