核心结论
@RestControllerAdvice + @ExceptionHandler 属于 SpringMVC(DispatcherServlet)内部的异常处理器。
只要异常跑出了 DispatcherServlet 的管辖范围,全局异常处理器就抓不到。
先看完整请求时序 + 管辖边界:
Tomcat容器
↓
Filter 1 → Filter 2 ←【DispatcherServlet外面!Spring全局异常管不到Filter】
↓
DispatcherServlet.doDispatch() // SpringMVC 管辖范围开始
├─Interceptor.preHandle()
├─Controller方法
├─Interceptor.postHandle()
├─渲染视图
└─finally { Interceptor.afterCompletion() }
DispatcherServlet管辖范围结束
↓
Filter 链反向退出
↓
Tomcat容器
1、Filter过滤器抛出异常 → 抓不到
原因
Filter 是 Servlet 规范组件 ,执行在 DispatcherServlet 执行之前 。
此时 SpringMVC 还没接手请求。异常直接往上抛给 Tomcat / Web容器,根本不会走到 Spring 的全局异常处理链路。
@RestControllerAdvice 完全收不到。
现象:浏览器直接返回 Tomcat 默认500白页,不会返回你定义的JSON错误体。
✅ 解决办法:Filter 内部自己 try‑catch,手动写出错误响应
java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
//你的校验逻辑,可能抛异常
chain.doFilter(request,response);
}catch (Exception e){
HttpServletResponse resp = (HttpServletResponse)response;
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write("{\"code\":500,\"msg\":\"过滤器异常:"+e.getMessage()+"\"}");
}
}
先分清两个地方能干啥
- Filter(过滤器) :在 SpringMVC、拦截器之前执行。属于 Servlet 最外层。
- Interceptor.preHandle:已经进到 SpringMVC(DispatcherServlet) 里面了。
什么样的校验适合放 Filter?
适合越早拦截越好的校验:
- token / 签名校验
- 请求头校验
- 跨域
- 黑名单 IP
- 非法参数快速拒绝
为什么优先放 Filter?
- 校验失败直接拦住请求,连 Controller、Spring 都不会进,省资源;
- 但是!Filter抛出异常进不去 @RestControllerAdvice全局异常,所以 Filter 里面必须自己 try‑catch,手动返回 JSON;
preHandle 适合放什么校验?
适合需要 Spring 上下文、拿到控制器 HandlerMethod 的校验:
- 判断当前接口是不是加了某个注解(比如
@RequiresLogin) - 获取 Controller方法信息,做权限控制
这种校验你放 Filter 拿不到 Controller 方法信息,只能放拦截器 preHandle
preHandle 抛业务异常,不要手动写 response
❌错误写法(千万不要这么干)
java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 校验token失败,这个 if(true) 是我写着玩的,实际开发中,按你的条件判断即可
if(true){
// 自己手动输出响应
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
// 一旦往 response 写了内容,`response.isCommitted()=true`,全局异常处理器就失效了。
// 手写响应后,一定要写一个return false
return false;
}
return true;
}
1. if 里面的`return false` 明确告诉 Spring:停止后续所有流程,不去执行Controller
2. postHandle 不会执行;
3. afterCompletion 会执行
4. 如果 if 里面不写 return false,就会执行外面的 return true,
代码还会跑进 Controller 执行业务!白白浪费性能,甚至产生脏数据
Controller 里面又往 response 写数据 导致→ 报错:response 已经提交,不能重复写响应
问题在哪?
当你调用 response.getWriter().write(),Tomcat 就会把响应刷出去
response.isCommitted() = true → 响应已经提交
后面就算你再抛出异常:
java
throw new BusinessException(401,"未登录");
Spring 的全局异常处理器 @RestControllerAdvice 已经不能再修改返回结果了 。
响应流已经关闭,全局异常处理失效。
现象:有时候前端收到一半报文、或者乱码,或者异常处理器根本不走。
✅正确写法:抛出异常,交给全局异常处理
java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(true){
// 只抛异常,不碰response!!
throw new BusinessException(401,"未登录");
// return false; //永远执行不到,多余,可以删掉
"补充"
抛出异常的分支,不需要 return false,
`throw` 直接终止当前方法,不会继续往下,Spring 自动中断流程,不走 Controller。
}
return true;
}
特点:
- 不要手动往response写任何内容;
- DispatcherServlet 捕获这个异常;
- 异常进到你的
@RestControllerAdvice,统一格式化返回 JSON; - 返回 false 就不用写了,抛异常本身就中断流程。
⚠️注意:preHandle抛出异常之后,return 语句永远不会执行。
一张决策图告诉你校验放哪里
校验是否需要拿到Controller方法/注解?
├─是 → 放 Interceptor preHandle,抛异常,不要手动写response
└─否 → 放 Filter,校验失败try‑catch手动输出JSON
总结
- Filter :校验失败 → 自己写response返回JSON,不要抛异常
- Interceptor‑preHandle :校验失败 → 抛自定义异常,不要手动写response,交给@RestControllerAdvice处理
- afterCompletion:绝对不能向外抛异常,内部必须try‑catch