@ControllerAdvice + @ExceptionHandler 是全局异常处理最稳组合,需配合 ResponseEntity< Result<?> > 统一响应、按异常类型分层捕获、避免吞错/错码,并注意 WebFlux/Filter 等场景的适配。

@ControllerAdvice + @ExceptionHandler 是 Java Web 项目里做全局异常处理最稳的组合,但直接套用容易返回 500 页面、JSON 混乱、甚至吞掉关键错误信息。
为什么 @ExceptionHandler 单独写在 Controller 里不顶用
它只对当前 Controller 生效,业务分散在多个 Controller 时,重复写一堆 @ExceptionHandler 既难维护又容易漏覆盖。更麻烦的是,Spring Boot 默认的错误页面(Whitelabel Error Page)会在没匹配到处理器时兜底,导致前端收到 HTML 而不是预期的 JSON。
- 必须配合
@ControllerAdvice才能跨 Controller 生效 -
@ControllerAdvice默认扫描所有@Controller,但不会拦截@RestController的异常——除非显式加@ResponseBody或让类实现ResponseEntityExceptionHandler - 如果项目用了 WebFlux,这套注解完全不生效,得换
WebExceptionHandler
@ControllerAdvice 类该放在哪、怎么声明才不出错
位置和声明方式直接影响是否被 Spring 扫描到、是否处理静态资源或过滤器抛出的异常。
- 推荐放在主启动类同包或其子包下,避免因组件扫描路径遗漏
- 不要加
@ResponseBody注解在类上——它只对方法有效;正确做法是每个@ExceptionHandler方法自己加@ResponseBody或返回ResponseEntity<?> - 如果只想处理特定包下的 Controller,用
@ControllerAdvice(basePackages = "com.example.api") - 别在
@ControllerAdvice类里注入HttpServletRequest做参数解析——可能为空;改用WebRequest更安全
统一响应格式的关键:别只靠 @ExceptionHandler 返回 String 或 Map
直接 return new HashMap<>() 或字符串,会让 Jackson 序列化行为不可控,比如 null 字段不忽略、时间格式不对、缺少状态码。
立即学习“Java免费学习笔记(深入)”;
- 定义一个标准响应类,比如
Result<T>,含code、message、data字段,并加@JsonInclude(JsonInclude.Include.NON_NULL) - 所有
@ExceptionHandler方法统一返回ResponseEntity<Result<?>>,这样能精确控制 HTTP 状态码和响应头 - 对
Exception这种顶层异常,建议最后兜底,但别直接e.printStackTrace()——日志框架(如 SLF4J)要配%ex才能打堆栈 - 注意:
ResponseStatusException会被 Spring 自动转成对应状态码,但不会进你的@ExceptionHandler,属于“绕过者”
常见踩坑:400/404/500 全被塞进同一个 handler,前端没法区分
HTTP 状态码语义丢失是全局异常处理最常被忽视的问题。比如参数校验失败(400)、资源不存在(404)、服务内部错误(500),前端需要据此做不同交互。
- Spring Boot 的
MethodArgumentNotValidException对应 400,但默认返回字段级错误,需手动提取BindingResult并转成业务友好的 message -
NoHandlerFoundException不会进@ExceptionHandler,得在application.properties里开spring.mvc.throw-exception-if-no-handler-found=true,再配合@ControllerAdvice捕获 - 自定义异常建议继承
RuntimeException,并用@ResponseStatus(HttpStatus.BAD_REQUEST)标记,这样既能被@ExceptionHandler捕获,也能自动带状态码 - Filter 或 Interceptor 抛出的异常,默认不会被
@ControllerAdvice拦截,得确保它们在 DispatcherServlet 生命周期内
真正难的不是写几个注解,而是让每种异常都落到它该去的 handler 里,且状态码、响应体、日志三者对得上。少一个环节,排查时就得翻三处日志。










