
为什么@ControllerAdvice不生效?
常见错误是只加了@ControllerAdvice,但没配扫描路径或漏了@ResponseBody。Spring Boot 默认只扫描主启动类同包及子包,如果切面类放在其他模块或父包下,必须显式配置@ComponentScan或在@SpringBootApplication里指定scanBasePackages。
另一个坑是返回值类型:若方法返回String(比如"error"),又没配@ResponseBody,Spring 会当成视图名跳转,而不是 JSON 响应。统一异常处理基本都走 REST 接口,所以务必加@ResponseBody,或者直接用@RestControllerAdvice——它等于@ControllerAdvice + @ResponseBody。
如何让不同异常返回不同状态码和结构?
别把所有异常都塞进一个@ExceptionHandler(Exception.class)。应该按异常类型分层捕获:
- @ExceptionHandler(BindException.class) 处理参数校验失败,返回400
- @ExceptionHandler(NullPointerException.class) 属于程序 bug,返回500并打日志,**不暴露堆栈给前端**
- 自定义业务异常如BusinessException,统一返回200但带code=1001这类语义化错误码
@ExceptionHandler(Exception.class)。应该按异常类型分层捕获:
- @ExceptionHandler(BindException.class) 处理参数校验失败,返回400
- @ExceptionHandler(NullPointerException.class) 属于程序 bug,返回500并打日志,**不暴露堆栈给前端**
- 自定义业务异常如BusinessException,统一返回200但带code=1001这类语义化错误码
注意ResponseEntity比直接返回对象更可控:
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
全局异常里怎么拿到原始请求信息?@ExceptionHandler方法参数支持注入HttpServletRequest、HttpServletResponse,甚至WebRequest。但要注意:
- HttpServletRequest.getInputStream()只能读一次,别在异常处理器里反复调用
- 想记录请求体(如 JSON body),得提前用ContentCachingRequestWrapper包装请求,通常在 Filter 中完成
- 日志里建议记录request.getRequestURL() + request.getQueryString() + request.getMethod(),别漏掉关键上下文
为什么AOP切面比@ControllerAdvice更适合某些场景?@ControllerAdvice只管 Controller 层抛出的异常;而 AOP 切面(比如用@Around)能拦截整个方法执行流程,适合做:
- 在 Controller 方法执行前校验登录态(非 JWT 这种标准方案时)
- 方法返回后自动包装统一封装结构(如{"code":0,"data":{...}}),避免每个 Controller 都写一遍
- 对特定注解(如@NeedAudit)的方法做操作日志记录
@ControllerAdvice只管 Controller 层抛出的异常;而 AOP 切面(比如用@Around)能拦截整个方法执行流程,适合做:
- 在 Controller 方法执行前校验登录态(非 JWT 这种标准方案时)
- 方法返回后自动包装统一封装结构(如{"code":0,"data":{...}}),避免每个 Controller 都写一遍
- 对特定注解(如@NeedAudit)的方法做操作日志记录
但别用 AOP 去替代异常处理——它没法优雅捕获未被捕获的运行时异常,且织入逻辑复杂后容易掩盖真实错误位置。真要统一兜底,还是@ControllerAdvice更稳。
实际项目里,@ControllerAdvice负责异常响应格式,AOP 负责横切增强,两者不是二选一,而是各干各的活。最容易被忽略的是:切面里抛出的新异常,如果不被@ControllerAdvice覆盖,就会变成 500 页面。










