SPRING · SPRING MVC · SPRING BOOT · JDK 8 / 17
一个请求,要过几道关?
把 Web 项目想成一栋办公楼。先认识三个角色,再跟着一个请求走一趟。
Filter站在大楼门口。谁进楼都先过他。他还能给你换身衣服(包装 request)。
归 Servlet 规范 · Tomcat 调用Interceptor坐在公司里面。知道你要找哪位办事员。进门前、办完后、送走后,各看你一眼。
归 Spring MVCWebMvcConfigurer开张前读一遍。决定秘书站哪个门、外人能不能来(跨域)、仓库在哪(静态资源)。
归 Spring MVC · 只在启动时用跟着请求走一趟
选一个剧情,点「下一步」。看请求此刻在谁手里,谁被跳过。
Filter ×NDispatcherServletInterceptorController装修清单 WebMvcConfigurer:开张时读一次,把秘书安排在 /api/** 门口,放过 /api/login。请求来时它不出场。
GET /api/orders/42什么时候上班,什么时候下班
开张(启动)、营业(处理请求)、打烊(关闭)。看谁在哪个时刻出场。
FilterTomcat 调用InterceptorSpring BeanWebMvcConfigurerSpring 配置类DispatcherServletTomcat 调用pre / post / after = preHandle / postHandle / afterCompletion。addXxx() = addInterceptors、addCorsMappings、addResourceHandlers……多个 WebMvcConfigurer 会全部被调用、合并生效。
一个门卫,同时接待所有人
门卫、秘书、清单都是单例,所有请求线程共用同一个对象。别在字段里存“当前用户”。
前台等第一个客人才上岗
默认第一次请求才 init(),日志里会出现 Initializing Servlet 'dispatcherServlet'。想开张就上岗:spring.mvc.servlet.load-on-startup=1。
一张表看区别
前两位是关卡,第三位是配置入口。
| 对比项 | 门卫Filter |
秘书Interceptor |
装修清单WebMvcConfigurer |
|---|---|---|---|
| 出身 | Servlet 规范,Tomcat 调用 | Spring MVC | Spring MVC |
| 是不是关卡 | 是 | 是 | 不是,只是配置入口 |
| 站在哪 | DispatcherServlet 外面 | DispatcherServlet 里面 | 不站岗 |
| 管多宽 | 进 Tomcat 的所有请求,按 URL 挑 | 进了 DispatcherServlet 并找到处理者的请求(包括静态资源、/error),按路径挑 | — |
| 知道要调哪个方法 | 不知道 | 知道,看 handler 参数 | — |
| 能把 request 换掉 | 能,往下传一个 Wrapper | 不能 | — |
| 怎么拦下 | 不调用 chain.doFilter() | preHandle 返回 false | — |
| 抛的异常进 @ControllerAdvice | 不会 | 会(afterCompletion 里抛的除外) | — |
| 调用次数 | init ×1,doFilter 每次,destroy ×1 | 每次请求 pre / post / after | 启动时 ×1 |
| 多个时的顺序 | @Order 或 setOrder(),数字小的在外层 | 按注册顺序(或 .order());pre 正序,post、after 倒序 | 全部生效,合并 |
| 接口 | jakarta.servlet.FilterJDK 8 / Boot 2:javax.servlet.Filter | HandlerInterceptor | WebMvcConfigurer |
项目里的活儿派给谁
按“要不要知道目标方法”“要不要换 request”来分,基本不会派错。
Spring Security 本身就是一串 FilterMDC.put() → finally MDC.clear()HttpServletRequestWrapperHandlerMethod.hasMethodAnnotation()preHandle 记开始,afterCompletion 算耗时preHandle 放 ThreadLocal,afterCompletion remove()addCorsMappings();用了 Security 再开 http.cors()addResourceHandlers()addArgumentResolvers()@RestControllerAdviceResponseBodyAdviceAOP @AspectJDK 8 还是 JDK 17:只换了门牌
Java EE 交给 Eclipse 基金会后改名 Jakarta EE,包名跟着换。角色、顺序、生命周期一点没变。
SSM 老项目
JDK 8 常见- 大楼
- 外置 Tomcat,靠 web.xml 登记
- 门卫
- web.xml 的
<filter>;要用 Spring Bean 就配DelegatingFilterProxy - 秘书
<mvc:interceptors>,或@EnableWebMvc+ WebMvcConfigurer- 包名
javax.servlet
Spring Boot 2.x
JDK 8+- 大楼
- 内嵌 Tomcat 9,Spring 5
- 门卫
@Component、FilterRegistrationBean,或@WebFilter+@ServletComponentScan- 秘书
- WebMvcConfigurer 的
addInterceptors() - 包名
javax.servlet
Spring Boot 3.x / 4.x
JDK 17+- 大楼
- 内嵌 Tomcat 10.1 / 11,Spring 6 / 7
- 写法
- 和 Boot 2 一样
- 包名
jakarta.servlet- 退休
WebMvcConfigurerAdapter、HandlerInterceptorAdapter已删除。JDK 8 起接口有 default 方法,直接实现接口即可
import jakarta.servlet.*; // ← 只有包名随版本变
import jakarta.servlet.http.*;
// ① 门卫:每个进楼的请求都经过(OncePerRequestFilter 保证同一请求只过一次)
@Component
@Order(1)
public class TraceFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
MDC.put("traceId", UUID.randomUUID().toString()); // 放行前
try {
chain.doFilter(req, res); // 放行:后面的所有人
} finally {
MDC.clear(); // 放行后
}
}
}
// ② 秘书:只管进了 DispatcherServlet 的请求
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
if (req.getHeader("Authorization") != null) return true; // 放行
res.setStatus(401);
return false; // 拦下
}
}
// ③ 装修清单:启动时调用一次,把秘书安排到门口
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginInterceptor loginInterceptor;
public WebConfig(LoginInterceptor loginInterceptor) { // 注入 Bean,别 new
this.loginInterceptor = loginInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/login", "/error");
}
}
常踩的坑
每条都是真实项目里反复出现的。
门卫在公司外面。在 Filter 里自己写好错误响应。
那时响应已经写出去了。改响应体、加响应头,用 ResponseBodyAdvice。
Spring Boot 里它们都走 DispatcherServlet。用 excludePathPatterns 放过。
加了它,Boot 的 MVC 自动配置整体退场,静态资源映射、spring.jackson.* 设置都可能失效。实现 WebMvcConfigurer 就够了。
addInterceptor(new X()) 里的 X 不是 Bean。把它声明成 Bean 再注入。
要指定 URL,用 FilterRegistrationBean;要指定先后,用 @Order 或 setOrder()。
门卫可以先用 HttpServletRequestWrapper 缓存下来再放行。秘书换不了 request,做不到。
Tomcat 线程池会复用线程。在 afterCompletion 或 finally 里 remove()。