Spring Boot 的自动配置让 Web 应用开箱即用,但真实业务里几乎一定会遇到需要定制 Spring MVC 行为的场景:比如给所有后台接口加一个登录校验的拦截器、放开某些静态资源路径、统一处理跨域请求,或者注册自定义的日期格式化器。在老版本 Spring 中,大家习惯继承 WebMvcConfigurerAdapter 或者直接继承 WebMvcConfigurationSupport,而现在的推荐做法非常简单——直接实现 WebMvcConfigurer 接口。这篇文章就来把这个接口的常用扩展点讲透,重点演示拦截器的完整实现流程。

一、为什么是 WebMvcConfigurer 而不是 WebMvcConfigurationSupport
先厘清一个概念。Spring Boot 关于 MVC 的自动配置类是 WebMvcAutoConfiguration,它的头部有一个条件注解 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class),意思是:只要容器中出现了 WebMvcConfigurationSupport 类型的 Bean,自动配置就整体失效。所以如果你为了加一个拦截器去继承 WebMvcConfigurationSupport,代价是把 Spring Boot 默认提供的静态资源映射、Jackson 消息转换器、路径匹配等全部配置都丢掉了,得不偿失。
WebMvcConfigurer 的定位则完全不同,它是一个纯回调接口,Spring Boot 在执行自动配置时会收集容器中所有 WebMvcConfigurer 类型的 Bean,逐个调用其中的方法来做增量式定制。用一句话概括:WebMvcConfigurationSupport 是全量接管,WebMvcConfigurer 是锦上添花。除非你确实需要彻底重写 MVC 行为,否则永远优先选择后者。
另外提一下历史包袱:Spring 5 之前由于 Java 8 之前的接口不能有默认方法,所以官方提供了 WebMvcConfigurerAdapter 抽象类作为过渡。Spring 5 之后接口方法都有了 default 实现,这个 Adapter 已经被标记为废弃,新项目里不要再用了,直接实现接口、只覆写需要的方法即可。
二、实现 WebMvcConfigurer 定制基础配置
先看一个基础骨架。定义一个配置类,标注 @Configuration 并实现 WebMvcConfigurer,之后想定制什么就覆写对应的方法:
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 定制跨域策略
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
// 定制静态资源映射
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/files/**")
.addResourceLocations("file:D:/upload/");
}
// 注册自定义格式化器
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new DateFormatter("yyyy-MM-dd"));
}
}这段代码里覆写了三个常用方法。addCorsMappings 处理跨域,注意如果要携带 Cookie(allowCredentials 为 true),不能再用 allowedOrigins("*"),必须改用 allowedOriginPatterns,否则启动时会直接抛异常,这是很多人踩过的坑。
addResourceHandlers 常用于把本地磁盘目录映射成可访问的 URL。写本地路径时注意前缀必须是 file:,Windows 下路径形如 file:D:/upload/,结尾的斜杠建议保留,表示按目录解析。如果只是想把静态资源放到 classpath 下的自定义目录,则用 classpath:/static/xxx/ 这样的写法。
addFormatters 允许注册全局的类型转换器,比如上面注册的日期格式化器,可以让前端传 2024-05-01 这种字符串时自动绑定到 Date 类型的参数上,省去了在每个接口里手动解析的麻烦。类似的还有 configureMessageConverters 可以替换或追加 HttpMessageConverter,configurePathMatch 可以调整路径匹配策略,按需选用即可。
三、自定义拦截器并注册到拦截链
拦截器是 WebMvcConfigurer 使用频率最高的场景。完整流程分两步:先写拦截器,再注册。拦截器本身实现 HandlerInterceptor 接口,它有三个可覆写的方法:preHandle 在控制器方法执行前调用,返回 false 会中断后续流程;postHandle 在控制器执行后、视图渲染前调用;afterCompletion 在整个请求结束后调用,适合做资源清理和耗时统计。
下面写一个典型的登录校验拦截器,从请求头取 token 并校验:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
return false; // 中断请求,不再进入控制器
}
// 校验通过后可以把用户信息放进请求属性,供后续使用
request.setAttribute("userId", parseUserId(token));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
long cost = System.currentTimeMillis()
- (Long) request.getAttribute("startTime");
System.out.println("接口耗时:" + cost + "ms");
}
}有了拦截器类,接着在配置类中通过 addInterceptors 方法把它注册进拦截链:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/api/**") // 拦截的路径
.excludePathPatterns("/api/login", "/api/register"); // 放行的路径
registry.addInterceptor(new LogInterceptor())
.addPathPatterns("/**")
.order(1); // 数值越小优先级越高
}
}注册时有几个细节值得注意。第一,路径规则是 Ant 风格通配符,/** 匹配多层路径,/* 只匹配一层,别混淆。第二,excludePathPatterns 的优先级高于 addPathPatterns,被排除的路径一定不会进拦截器,所以登录、注册这类匿名接口要在这里显式放行,否则用户永远登不上去。第三,如果多个拦截器都命中同一路径,执行顺序默认按注册顺序,也可以用 order 方法显式指定,数值小的先执行。
多拦截器下 preHandle 按顺序正向执行,postHandle 和 afterCompletion 则按逆序执行,形成一个可预测的洋葱模型。如果某个 preHandle 返回了 false,前面已通过的所有拦截器的 afterCompletion 依然会被调用,这一点对于释放资源很重要,可以放心把清理逻辑写在 afterCompletion 里。
四、拦截器的常见坑与替代方案
实际使用中有几个容易出问题的地方。一是拦截器无法直接注入到没有注册为 Bean 的场景,如果拦截器里需要用到 Service,建议把拦截器本身声明为 Bean,通过构造器注入依赖,然后在 addInterceptors 里传入对象而非 new 出来的实例:
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginInterceptor loginInterceptor;
public WebConfig(LoginInterceptor loginInterceptor) {
this.loginInterceptor = loginInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**");
}
}
@Component
public class LoginInterceptor implements HandlerInterceptor { ... }二是拦截器与 Filter 的选择问题。Filter 是 Servlet 规范层的组件,在拦截器之前执行,能拿到原始的请求和响应,适合字符编码、全局日志这类与业务无关的通用处理;拦截器处于 Spring MVC 上下文中,能拿到对应的 HandlerMethod,知道请求最终会打到哪个控制器的哪个方法,适合做权限校验、登录检查这类业务级拦截。两者不是互斥关系,按层次各司其职即可。
三是异步请求的处理。如果接口返回 DeferredResult 或使用 SSE,postHandle 可能不会按预期执行,此时应关注 AsyncHandlerInterceptor 的 afterConcurrentHandlingStarted 回调,避免在异步场景下统计或清理逻辑失效。
整体来看,WebMvcConfigurer 提供的是一套声明式的 MVC 定制入口,理解了它增量扩展的设计思路,再掌握 addInterceptors、addCorsMappings、addResourceHandlers 这几个高频方法,日常开发中百分之九十的 MVC 定制需求都能优雅地解决,而且完全不会破坏 Spring Boot 的自动配置红利。
WebMvcConfigurerSpring Boot拦截器MVC配置修改时间:2026-09-03 01:46:55