异常处理:@ExceptionHandler/@ControllerAdvice/@RestControllerAdvice

Service 层抛出的异常不该让容器吐一屏堆栈给用户。Spring MVC 把「异常 → 响应」的翻译工作集中到 @ExceptionHandler 方法上,配合 @ControllerAdvice 即可全局统一处理。

DispatcherServlet 的异常处理链

处理器方法抛异常后,DispatcherServlet 不会直接 500,而是按顺序尝试一组 HandlerExceptionResolver,把异常转成 ModelAndView/响应:

解析器处理对象
ExceptionHandlerExceptionResolver@ExceptionHandler 注解方法(主力)
ResponseStatusExceptionResolver@ResponseStatus 标注的异常、ResponseStatusException
DefaultHandlerExceptionResolverSpring 内建异常 → 标准状态码(如 405/415)

控制器内局部处理

@ExceptionHandler 写在控制器类里,只对本控制器生效:

@RestController
public class UserApi {
    @GetMapping("/api/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id); // 可能抛 UserNotFoundException
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleNotFound(UserNotFoundException e) {
        return ResponseEntity.status(404).body(e.getMessage());
    }
}

@ExceptionHandler 的方法参数可以声明要接的异常(不写类型则按方法参数推断),返回值支持与普通处理方法相同的类型(@ResponseBody、ResponseEntity、视图名等)。

@ControllerAdvice:全局异常处理

把 @ExceptionHandler 提升到全局,一个类管所有控制器:

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArg(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body("参数不合法:" + e.getMessage());
    }

    @ExceptionHandler(Exception.class) // 兜底,避免裸堆栈
    public ResponseEntity<String> handleOther(Exception e) {
        return ResponseEntity.status(500).body("系统繁忙,请稍后重试");
    }
}

@ControllerAdvice 还可用于 @InitBinder、@ModelAttribute 等全局方法(定义所有控制器共享的模型属性/数据绑定),作用范围可以按包用 annotations/basePackages 收窄。

@RestControllerAdvice:专为接口而生

@RestControllerAdvice = @ControllerAdvice + @ResponseBody,方法返回值默认直接写响应体,不用每次包 ResponseEntity(想控制状态码时仍可返回 ResponseEntity):

@RestControllerAdvice
public class ApiExceptionAdvice {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<Void> handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
                .map(f -> f.getField() + " " + f.getDefaultMessage())
                .collect(Collectors.joining("; "));
        return ApiResponse.fail(400, msg);
    }
}
// 前端收到:{"code":400,"message":"name 用户名不能为空; age 年龄不能为空"}

匹配优先级

同一个异常被多个 @ExceptionHandler 命中时按「最具体」原则匹配;多个 @ControllerAdvice 之间可用 @Order 排序,控制器自身的方法优先于全局 advice。精确次序以官方文档为准。

Spring 6 新姿势:ProblemDetail(RFC 7807)

Spring 6 起错误响应可遵循 RFC 7807 标准结构(title/status/detail/instance),方式包括抛 ResponseStatusException、在异常上标注、或继承 ResponseEntityExceptionHandler 复用内建映射。标准形态利于跨语言对接,选择以官方文档为准。

生产建议清单

  • 兜底 @ExceptionHandler(Exception.class) 必须存在,但记日志再吐统一文案,别把堆栈发给客户端。
  • 业务异常设计成自定义异常(如 BizException),全局只处理它 + 框架异常,别到处写 try/catch。
  • 校验异常(MethodArgumentNotValidException)、类型不匹配(MethodArgumentTypeMismatchException)等框架异常单独映射成 400。

一句话:异常处理 = 自定义业务异常(抛出点)+ @RestControllerAdvice(翻译点)+ 兜底 Exception(保底点)。三层搭好,接口永远返回你能预期的形状。

笔记加载中…