news 2026/10/17 9:59:55

WebMvcConfigurer vs WebMvcConfigurationSupport:Spring Boot配置选择与原理解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WebMvcConfigurer vs WebMvcConfigurationSupport:Spring Boot配置选择与原理解析

1. 这个经典问题到底在问什么

做 Spring Boot 开发的朋友,只要项目里用到 SpringMVC,迟早会遇到这个选择:要自定义 SpringMVC 配置,到底该继承WebMvcConfigurer,还是该继承WebMvcConfigurationSupport?特别是第一次做 Web 项目的人,在网上搜教程时经常看到两种截然不同的写法,有人用WebMvcConfigurer接口,有人继承WebMvcConfigurationSupport,而且很多老教程还告诉你“实现 WebMvcConfigurer 或继承 WebMvcConfigurationSupport 都可以”,这句话坑了相当多人。

先说结论:绝大多数场景下你应该实现WebMvcConfigurer接口,而不是继承WebMvcConfigurationSupport类。如果项目不是极其特殊的定制需求,一旦你选择了继承WebMvcConfigurationSupport,Spring Boot 的 WebMvc 自动配置会直接失效,紧接着就是一连串的“返工”:静态资源找不到了、拦截器注册不上了、消息转换器配置不生效了、跨域配置不管用了。很多刚入门的同学在本地跑得好好的,一部署就发现资源 404,排查半天最后发现是继承类选错了,这种经历我见过太多次了。

要弄清楚为什么会有这么明显的差异,就得先理解 Spring Boot 在 WebMvc 这部分到底做了什么、自动配置的开关在哪、两种写法分别对应什么机制。这篇文章不讲虚的,直接把继承选择背后的原理、两种方式的区别、各自的使用场景和踩坑记录完整梳理一遍,不管你是第一次写 Web 配置的新手,还是已经写过一段时间但一直没搞明白的老开发,看完应该都能把这块彻底吃透。

2. 两种方式的本质区别:一个是“定制”,一个是“接管”

2.1 先搞清楚 WebMvcConfigurer 和 WebMvcConfigurationSupport 到底是什么

从名字上就能看出一点端倪:WebMvcConfigurer是接口,WebMvcConfigurationSupport是类。接口的意思是“你要往已有的配置里加东西”,类继承的意思是“你要自己搭一套配置体系”。

WebMvcConfigurer接口定义了一堆带默认实现的回调方法,比如addInterceptors注册拦截器、addResourceHandlers配置静态资源映射、addCorsMappings配置跨域、configureMessageConverters配置消息转换器,等等。Spring Boot 内部的自动配置类会先帮你把一套完整的 WebMvc 基础设施搭好,比如静态资源映射、视图解析器、消息转换器、数据绑定器这些东西都默认配好了,然后你实现的这个WebMvcConfigurer会被当成一个“后置回调”,在自动配置类完成初始化之后,再把你的定制内容合并进去。

WebMvcConfigurationSupport就不一样了,它是 SpringMVC 提供的一个“基类”,里面包含了完整的、基于 Java 配置的 SpringMVC 初始化流程。当你自己写一个@Configuration类去继承它,并且这个配置类被 Spring 扫描到了,那么 Spring 就会认为“用户已经接管了 WebMvc 的整个配置工作”,默认的那套自动配置就不再生效了。

2.2 为什么要设计两个东西,这不是故意坑人吗

其实这两个东西的分工从 Spring 框架时代就有了。在没有 Spring Boot 的时候,用传统 Spring + SpringMVC 做 Java 配置,标准做法就是继承WebMvcConfigurationSupport(或者实现WebMvcConfigurer,但作用有限),然后在这个类里把视图解析器、资源映射、拦截器这些东西全部手动配一遍。那时候这是唯一的正规路径,因为框架默认什么都没帮你配。

Spring Boot 出现之后,为了“自动配置”的体验,官方在spring-boot-autoconfigure里提供了一个WebMvcAutoConfiguration,它会判断容器里有没有WebMvcConfigurationSupport这个类型的 Bean,如果有,就认为用户要自己来了,于是自动配置直接让步。这个判断逻辑是关键中的关键。

用代码来讲,WebMvcAutoConfiguration上有这么几个重要的条件注解:

@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class }) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) public class WebMvcAutoConfiguration { // ... }

注意看@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)这一行,含义是:当容器里不存在WebMvcConfigurationSupport类型的 Bean 时,这个自动配置类才会生效。一旦你继承了WebMvcConfigurationSupport并声明成了@Configuration,这个条件就不满足了,整个WebMvcAutoConfiguration直接跳过,包括它内部帮你配好的各种东西,全部不存在。

这就是为什么两种写法带来的结果天差地别:实现WebMvcConfigurer,自动配置还在,你是在“默认配置之上做增量定制”;继承WebMvcConfigurationSupport,自动配置整个被关闭,你变成了“从零开始搭一套 WebMvc 配置”,所有 Spring Boot 原来帮你做的默认行为都需要自己补齐。

2.3 用一个生活类比帮助理解

可以把 Spring Boot 的 WebMvc 自动配置想象成一套精装修的房子。家具、水电、网络、灯具都是开发商装好的,你拿到房子只需要拎包入住。WebMvcConfigurer就相当于你在精装修基础上添置一些软装:挂个壁画、换盏灯、添个书架,开发商的基础设施都保留着。

继承WebMvcConfigurationSupport则像是你把精装修全拆了,变成毛坯房,然后自己从水电改造、铺地砖开始重新装修。不是不行,但你得把所有事情都自己搞定,墙面、地面、管线、开关插座一个都不能漏。很多人不理解这一点,选了继承,结果发现房子变毛坯之后,连灯都没了,自然各种功能失效。

3. 实现 WebMvcConfigurer 的写法与适用场景

3.1 具体代码长什么样

最常见的写法是这样:

@Configuration public class MyWebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/register"); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceLocations("file:/data/uploads/"); } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://example.com") .allowedMethods("GET", "POST", "PUT", "DELETE"); } }

这种写法下,Spring Boot 的默认配置照常工作,你只是在特定节点上做了扩展。比如你只加了拦截器,那静态资源、视图解析器、默认的消息转换器统统还在,完全不用你关心。

3.2 哪些场景用这个就够了

绝大多数业务项目,实现WebMvcConfigurer就足够了。常见的场景包括:

  • 注册自定义拦截器,做登录态校验、接口访问日志、接口耗时统计。
  • 添加静态资源映射,把外部文件目录映射成 URL 路径。
  • 配置跨域规则,前后端分离项目几乎必配。
  • 自定义参数解析器(HandlerMethodArgumentResolver),实现当前用户注入、分页参数解析等。
  • 定制消息转换器(比如统一处理日期格式、追加自定义序列化配置)。
  • 配置视图控制器(ViewController),把某个 URL 直接映射到一个页面,不需要写 Controller。
  • 配置HttpMessageConverter的序列化规则,比如定制 Jackson 的 ObjectMapper。

3.3 为什么默认情况下推荐用这个

因为我个人的观点是:除非你确实需要彻底替换 SpringMVC 的默认初始化逻辑,否则没必要把整个自动配置关掉。用WebMvcConfigurer的好处至少有这么几点:

第一,自动配置里的默认值都是经过大量项目验证的,静态资源映射、消息转换器、内容协商、视图解析器这些默认行为日常够用,没必要自己重造轮子。

第二,增量定制定位清晰,代码意图明了,后来接手的人一看就知道这个配置类只是在默认基础上加了哪些东西,维护成本低。

第三,不容易踩坑,因为 Spring Boot 的默认行为都还在,不会发生“配置完拦截器却发现静态资源 404”这种连带问题。

第四,后续升级 Spring Boot 版本时,自动配置的内部实现可能会调整,但只要你还用WebMvcConfigurer,框架负责把兼容性处理好,你不需要跟着改太多。

4. 继承 WebMvcConfigurationSupport 到底会关掉什么

4.1 自动配置被关掉后,具体少了哪些能力

这一步要说清楚,不然很多人不理解“自动配置失效”到底有多严重。我直接列一个表格,把两种方式下常见功能的可用性对比出来。

功能点实现 WebMvcConfigurer继承 WebMvcConfigurationSupport
默认静态资源映射(classpath:/static 等)保留丢失,需手动配置
默认首页 index.html 跳转保留丢失
消息转换器(Jackson、String、ByteArray 等)保留并可以追加只有最基础的转换器,需手动注册
内容协商(返回 JSON / XML 等按请求头决定)保留需手动配置
视图解析器(InternalResourceViewResolver 等)保留需手动配置
类型转换器、格式化器(日期格式化等)保留需手动配置
拦截器注册支持addInterceptors支持addInterceptors
静态资源自定义映射支持addResourceHandlers支持addResourceHandlers
跨域配置支持addCorsMappings支持addCorsMappings
参数解析器支持addArgumentResolvers支持addArgumentResolvers

看出来没有?继承WebMvcConfigurationSupport后,拦截器、资源映射、跨域这些“自定义回调”还是可以写的,因为WebMvcConfigurationSupport本身提供了这些方法,但 Spring Boot 自动配置帮你初始化好的“默认基础设施”没了,比如常见的静态资源目录映射、默认的消息转换器、视图解析器,这些需要你用代码手动补回来。

说得直白一点:WebMvcConfigurationSupport提供的是一套“空架子”的初始化回调,它不会帮你把静态资源默认路径配好,不会帮你把常用的消息转换器注册全,不会帮你做内容协商,也不会帮你配视图解析器。整套体系需要你自己搬砖。

4.2 继承后会引发的经典连锁故障

继承了WebMvcConfigurationSupport而又没有手动补全配置,最常见的是如下三种故障:

第一种,静态资源 404。项目里放在src/main/resources/static下的css、js、图片全部访问不到。这是因为默认资源映射没了。很多同学会特别困惑,明明 devtools 热更新都正常,页面能加载 HTML,但 CSS 全挂了,多半就是这个原因。

第二种,返回 JSON 报错或者格式不对。因为MappingJackson2HttpMessageConverter没注册,接口返回对象时 SpringMVC 没法把对象序列化成 JSON。实际上 Spring Boot 的自动配置里注册了一个非常完整的转换器集合,包含StringHttpMessageConverter、MappingJackson2HttpMessageConverter、ByteArrayHttpMessageConverter等,而且给MappingJackson2HttpMessageConverter配好了一个定制过 ObjectMapper。你继承之后这些默认的都不加载了,容器里只剩下 SpringMVC 最基础的那些转换器,JSON 自然就死了。

第三种,日期格式化失效。WebMvcAutoConfiguration里还注册了一堆格式化器,支持@DateTimeFormat注解、支持默认日期格式,继承后这些也没了。

4.3 如果真的需要继承,怎么把默认能力补回来

有些场景确实需要继承,比如你打算完全替换消息转换器体系,或者想关闭 SpringMVC 的某些默认行为,又或者项目的定制程度高到必须自己控制整个初始化流程。这种时候你可以这么做:让配置类继承WebMvcConfigurationSupport,然后手动把需要的能力补齐。

例如手动补上静态资源映射:

@Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); registry.addResourceHandler("/uploads/**") .addResourceLocations("file:/data/uploads/"); }

手动补上默认视图解析器:

@Override protected void configureViewResolvers(ViewResolverRegistry registry) { registry.viewResolver(new InternalResourceViewResolver("/WEB-INF/views/", ".jsp")); }

手动补上 JSON 转换器:

@Override protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new MappingJackson2HttpMessageConverter()); }

但注意:如果你调用了configureMessageConverters,默认转换器会被清空重来,你加了什么就有什么。如果只是想追加一个自定义的转换器,同时保留默认的,应该重写extendMessageConverters方法,而不是configureMessageConverters。这个差异非常容易踩坑,后面我会单独再讲。

还需要注意一个问题:继承了WebMvcConfigurationSupport之后,很多第三方 starter 的自动配置也会连带失效。因为不少组件库会基于WebMvcConfigurer来做自动配置扩展,比如某些安全框架、某些接口文档组件,它们可能在自动配置里往容器里注册一个WebMvcConfigurerBean,来追加对 SpringMVC 的定制。可一旦你继承WebMvcConfigurationSupport导致自动配置关掉,这些扩展自然也都不生效了。这一点在引入第三方组件时尤其头疼,整了半天发现别人的自动配置根本没执行,源头就是你这一个@Configuration。

5. 核心原理深挖:条件注解与自动配置的运行机制

5.1 @ConditionalOnMissingBean 的精确匹配逻辑

回到关键点:@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)。这个注解的工作机制是,Spring 容器在注册 Bean 的过程中,判断当前容器里是否已经存在指定类型的 Bean 定义。如果存在,条件不满足,当前这个配置类/Bean 就不加载。

但这里有个细节需要注意:@ConditionalOnMissingBean判断的是 Bean 的类型,不是父类或接口子类。所以如果你的配置类继承的是WebMvcConfigurationSupport,那么 Spring 扫描到这个类,容器里就出现了一个类型为WebMvcConfigurationSupport子类的 Bean。@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)就会认为条件不满足,自动配置类整体失效。

这里还有一个容易被忽略的点:WebMvcConfigurationSupport 本身是一个@Configuration类。也就是说,Spring Boot 不只是通过@ConditionalOnMissingBean去检查,还涉及到@Configuration类解析顺序、BeanDefinition 注册顺序等细节。不过可以简单理解:只要你声明了一个继承自WebMvcConfigurationSupport的配置类,自动配置就会退出。

5.2 WebMvcAutoConfiguration 内部包含哪两层结构

翻看WebMvcAutoConfiguration的源码,可以看到它内部有两个嵌套的配置类,一个叫WebMvcAutoConfigurationAdapter,一个叫EnableWebMvcConfiguration,它们还有不同的职责。

WebMvcAutoConfigurationAdapter实现了WebMvcConfigurer,这才是自动配置真正干杂活的地方。它往容器里注册了默认的HandlerMapping、HandlerAdapter、ContentNegotiatingViewResolver、LocaleResolver、ThemeResolver,以及RequestMappingHandlerAdapter相关的定制参数,比如消息转换器列表、参数解析器列表等。这个适配器还配置了ResourceProperties相关的默认资源映射。

EnableWebMvcConfiguration继承自DelegatingWebMvcConfiguration,而DelegatingWebMvcConfiguration又继承自WebMvcConfigurationSupport。注意这里有个很有趣的点:Spring Boot 自动配置内部其实是继承WebMvcConfigurationSupport的,但它做了大量扩展和默认补充,还通过DelegatingWebMvcConfiguration把所有容器里的WebMvcConfigurerBean 收集起来、统一委托调用。

这也是为什么实现WebMvcConfigurer的定制能生效——DelegatingWebMvcConfiguration会把所有WebMvcConfigurer实现类的回调方法按顺序合并执行。

5.3 为什么说“实现接口”其实是“自动配置内部替你继承了类”

很多教程说“实现 WebMvcConfigurer 和继承 WebMvcConfigurationSupport 两种方式都可以”,这句话造成的误解就在这里。从最终效果看,WebMvcConfigurer的回调最终会由一个继承自WebMvcConfigurationSupport的类来统一调用。Spring Boot 自动配置内部用了DelegatingWebMvcConfiguration,这个类负责把用户自定义的WebMvcConfigurer收集起来,并在适当的时机调用它们。

也就是说,实现接口的底层,最终还是走了继承类的逻辑,但这个继承类是 Spring Boot 自动配置内部帮你完成的一个受控版本,它保留了所有默认行为,并且把你的定制作为扩展点合并进去。而你自己去继承WebMvcConfigurationSupport,就破坏了这套机制,相当于把自动配置内部的那个受控版本给顶掉了。

理解了这层机制,再回头看“到底继承谁”这个问题,答案就非常清晰:

  • 默认情况,用WebMvcConfigurer,交给 Spring Boot 自动配置内部去处理。
  • 除非你确定要关闭自动配置、完全掌控初始化流程,才考虑继承WebMvcConfigurationSupport。

6. 实操中的选择建议与完整示例

6.1 什么样的项目适合实现 WebMvcConfigurer

从我接触过的项目来看,下面这些场景用WebMvcConfigurer准没错。

第一个是标准的 RESTful API 后端项目。需要配置拦截器、CORS、参数解析器、消息转换器,但整体还是希望 Spring Boot 的默认行为兜底。

第二个是前后端分离项目的前端资源工程。有时候需要把 Spring Boot 当作静态文件服务器,加自定义资源映射把本地目录映射出来,同时还要支持跨域,用WebMvcConfigurer就够了。

第三个是微服务架构中的 BFF(Backend for Frontend)层。这种服务往往需要定制与前端约定的特殊参数解析逻辑、报文格式转换等,但对 SpringMVC 其它默认行为没有特殊要求。

6.2 什么情况下才真的需要继承 WebMvcConfigurationSupport

说实话,90% 以上的项目不需要继承这个类。但如果你遇到下面这些情况,可能才需要。

第一种情况,你需要完全替换消息转换器体系。比如自定义一套二进制协议,或者强制移除 Spring 默认注册的某些消息转换器,让接口只接受某一种特定格式。这个时候在WebMvcConfigurer.configureMessageConverters里做也可以生效,但如果你想连StringHttpMessageConverter都去掉,而configureMessageConverters这个方法是追加式的,你得先清空列表再添加,处理起来不优雅。继承之后重写configureMessageConverters就干净利落。

第二种情况,项目中大量使用 XML 配置或者老式 JSP,需要完全自定义视图解析器,同时不想要默认的ContentNegotiatingViewResolver等复杂行为。

第三种情况,你开发的是一个基础组件,或者说是一个自定义的 SpringMVC 扩展框架,需要替换RequestMappingHandlerMapping或RequestMappingHandlerAdapter这种核心组件,光靠WebMvcConfigurer回调是不够的。这种情况在业务项目里很少见,基本都是做中间件或框架的人才会遇到。

6.3 一个完整示例:带拦截器、CORS、参数解析器的 WebMvcConfigurer 配置

我来写一个我在实际项目里常用的配置模板,你可以直接参考改造。

@Configuration public class WebConfig implements WebMvcConfigurer { /** * 注册自定义拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 登录态校验拦截器 registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns( "/api/auth/login", "/api/auth/captcha", "/error" ); // 接口耗时统计拦截器 registry.addInterceptor(new AccessLogInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/static/**", "/error"); } /** * 静态资源映射:把本地磁盘目录映射成 URL */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 注意最后一个斜杠必须写,否则路径拼接会出问题 registry.addResourceHandler("/files/**") .addResourceLocations("file:/data/app/files/"); } /** * 跨域全局配置 */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } /** * 自定义参数解析器:把当前登录用户直接注入到 Controller 参数 */ @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new CurrentUserArgumentResolver()); } /** * 追加消息转换器,不影响默认的 */ @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { // 给已有的 Jackson 转换器加 ObjectMapper 定制 for (HttpMessageConverter<?> converter : converters) { if (converter instanceof MappingJackson2HttpMessageConverter) { MappingJackson2HttpMessageConverter jsonConverter = (MappingJackson2HttpMessageConverter) converter; ObjectMapper objectMapper = jsonConverter.getObjectMapper(); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); } } } }

6.4 如果选继承,一个能跑通的最小配置长什么样

假设你确实要继承WebMvcConfigurationSupport,那我建议至少把下面这些基础能力手动补上,不然项目几乎没法用。

@Configuration public class CustomWebMvcConfig extends WebMvcConfigurationSupport { /** * 静态资源映射:必须手动补,不然 /static 下的文件全 404 */ @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); registry.addResourceHandler("/files/**") .addResourceLocations("file:/data/app/files/"); } /** * 消息转换器:必须手动补 Jackson,否则返回 JSON 直接报错 */ @Override protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new StringHttpMessageConverter()); converters.add(new MappingJackson2HttpMessageConverter()); converters.add(new ByteArrayHttpMessageConverter()); } /** * 拦截器:这个方法和 WebMvcConfigurer 里的一样 */ @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login"); } /** * 跨域配置:需要手动补 */ @Override protected void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE"); } }

注意这里有个非常关键的问题:在继承WebMvcConfigurationSupport的写法里,虽然方法名和WebMvcConfigurer里是一样的,比如addInterceptors、addCorsMappings,但这些方法的访问修饰符是protected,这是两类 API 的一个典型差异。另外父类方法名虽然一样,但这些是从WebMvcConfigurationSupport基类里继承来的一堆 protected 回调,含义和WebMvcConfigurer里的同名方法不一定完全一致。比如configureMessageConverters在两种方式里的行为就完全不同,后面我会单独把这部分展开。

7. 两种体系下的几个关键差异点详解

7.1 configureMessageConverters、extendMessageConverters 的行为差异

这是一个高频易错点,一定要单独拎出来讲清楚。在实现WebMvcConfigurer的体系中,你重写configureMessageConverters追加转换器时,默认的转换器并不会被你重写的方法覆盖,因为自动配置内部把转换器列表组装好后,会先调用用户自定义WebMvcConfigurer.configureMessageConverters对它进行追加,接着再调用extendMessageConverters做二次扩展。顺序可以理解为:

  1. Spring Boot 自动配置内部创建默认的消息转换器列表。
  2. 依次调用所有WebMvcConfigurer.configureMessageConverters。
  3. 依次调用所有WebMvcConfigurer.extendMessageConverters。

所以你在WebMvcConfigurer.configureMessageConverters里往converters列表添加自定义转换器,默认转换器不会消失,只是被追加了。

但如果你继承WebMvcConfigurationSupport,并且重写configureMessageConverters,那含义就变了。在WebMvcConfigurationSupport里,configureMessageConverters是负责填充整个转换器列表的,默认情况下父类里这个方法会添加一批基础的转换器,如果你重写了它,父类的基础转换器就不会被加了,等于说你用什么就有什么,列表会被清空重建。

所以在继承体系里也有两个方法:

  • configureMessageConverters:全量设置,重写它意味着抛弃默认列表。
  • extendMessageConverters:追加设置,在默认列表基础上再扩展,默认的还在。

这个差异是很多人从WebMvcConfigurer换到继承写法后最容易踩的坑。你在WebMvcConfigurer里写习惯了,换成继承后也往configureMessageConverters里加 JSON 转换器,结果默认的 String、ByteArray 转换器全没了,那个接口连返回一个 String 都会出问题。

7.2 方法可见性的差异

前面提过一点,这里再详细说一下。WebMvcConfigurer接口里所有方法的默认实现都是 public,因为接口方法都是 public 的。而WebMvcConfigurationSupport里对应的回调方法大多是 protected。

这个差异在实战中影响不大,因为重写方法时可以扩大可见性,但不能缩小。如果你继承WebMvcConfigurationSupport的配置类里把addInterceptors写成 public,这是可以的,因为父类里该方法是 protected,子类扩大成 public 合法。但如果你在实现WebMvcConfigurer时把方法写成 private,编译直接报错。

平时写代码时,建议保持和父类一致的可见性修饰符,减少困惑。实现接口就用 public,继承基类就用 protected。

7.3 是否启用 @EnableWebMvc 也有影响

顺便提醒一个容易混淆的点:Spring Boot 配置中还有一个@EnableWebMvc注解。这个东西需要注意,它的作用是导入DelegatingWebMvcConfiguration,把 SpringMVC 切换成基于WebMvcConfigurationSupport的模式。

在 Spring Boot 项目中,如果你只是实现WebMvcConfigurer,不加@EnableWebMvc,那默认行为是 Spring Boot 自动配置驱动的一整套 WebMvc 配置。如果加了@EnableWebMvc,那 Spring MVC 的很多默认配置也会被改掉,因为@EnableWebMvc会让WebMvcConfigurationSupport类的配置生效,而产生的影响和继承这个类是非常类似的。实际上@EnableWebMvc的导入逻辑就是导入DelegatingWebMvcConfiguration,它继承自WebMvcConfigurationSupport。

所以网上很多老教程里写“继承 WebMvcConfigurationSupport 或者加 @EnableWebMvc 二选一”,本质是同一个层面的操作。你不要把@EnableWebMvc和WebMvcConfigurer搞混了,前者是开关级别的操作,后者只是定制回调。

7.4 多个 WebMvcConfigurer Bean 的执行顺序

还有一点值得了解:在一个 Spring Boot 项目里,你可能有多个实现了WebMvcConfigurer的配置类,比如一个项目模块自己写一个,第三方 starter 也通过自动配置注入一个。它们都会被DelegatingWebMvcConfiguration收集起来,然后按顺序执行。

顺序的默认规则是:实现了Ordered接口或用@Order注解的,按优先级从高到低执行,没有指定的按发现顺序。实际项目里多数场景不关心执行顺序,但如果多个配置类同时加了同一个路径的拦截器,顺序可能影响拦截逻辑。建议所有自定义的配置类都加上@Order注解,避免隐形依赖。

8. 常见报错与排查思路

8.1 报错:No converter found for return value of type

这个报错是 JSON 序列化相关的最经典报错之一,现象是访问一个返回对象的接口,直接 500,控制台提示找不到消息转换器。

排查步骤如下:

  1. 先确认你有没有继承WebMvcConfigurationSupport,或者加@EnableWebMvc,如果有,那大概率是默认的MappingJackson2HttpMessageConverter没注册。
  2. 检查项目里是否引入了spring-boot-starter-web,正常情况下它会带上jackson-databind。
  3. 如果两者都没问题,再看是不是在configureMessageConverters里覆盖掉了默认转换器。

最简单的方式:在配置类里临时注释掉WebMvcConfigurationSupport的继承关系,改成实现WebMvcConfigurer,再跑一次接口,如果正常了,问题就出在这里。

8.2 静态资源全部 404

静态资源 404 的排查逻辑比较固定:

  1. 看 URL 路径是否正确,默认静态资源路径是classpath:/static/、classpath:/public/、classpath:/resources/、classpath:/META-INF/resources/。
  2. 确认有没有配置类继承WebMvcConfigurationSupport或者加@EnableWebMvc。有的话,这些默认路径全部失效。
  3. 检查自定义的addResourceHandlers是否写了/**这个映射,如果只映射了特定路径,那static下的资源还是访问不到。

8.3 配置了拦截器但请求没进拦截器

这种情况多半不是继承选择的问题,而是拦截器注册方式不对。检查一下:

  1. 配置类是否被 Spring 扫描到,确保有@Configuration注解。
  2. 拦截器的addPathPatterns是否匹配目标路径,注意/**和/*的区别,/*只匹配一级路径,/**匹配多级。
  3. 如果项目里同时有多个WebMvcConfigurer,确认是哪个生效,别在自己写的配置类里加了又因为@Order原因被其他配置覆盖。

8.4 日期格式化失效

日期格式化失效常见于继承WebMvcConfigurationSupport之后,因为自动配置里注册的格式化器失效了。

处理办法有两个。一个是回到WebMvcConfigurer实现,在extendMessageConverters里给 Jackson 转换器配日期格式。另一个如果你坚持继承,那需要手动注册格式化器:

@Override public void addFormatters(FormatterRegistry registry) { DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd")); registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); registrar.registerFormatters(registry); }

这里多说一句:重写 addFormatters 时,如果只是registry.addConverter(...)而不管日期格式化,那@DateTimeFormat的行为可能不完整,用DateTimeFormatterRegistrar是最稳的。

8.5 配置了 CORS 但还是跨域报错

跨域排查有一点经常被忽略:不是只有接口层需要允许跨域,如果接口经过拦截器,拦截器返回 401 或者修改了响应头,CORS 也可能异常。另一种情况是你们使用了网关层,网关和 SpringMVC 两层都做了跨域配置,导致重复响应头,浏览器也报错。

回到本文主题,如果你继承WebMvcConfigurationSupport导致自动配置失效,CORS 配置也没了,那就得在配置类里手动addCorsMappings。但如果你不希望全局配置,也可以用@CrossOrigin注解在 Controller 上局部配置。

9. 我踩过的一些坑和最终建议

9.1 一个项目里混用两种写法是什么后果

有些项目一开始用WebMvcConfigurer,后来某个同事因为一个特殊需求改成了继承WebMvcConfigurationSupport,再后来另一个人又在原来配置类基础上加各种WebMvcConfigurer,结果发现某些配置失效了,花一整天排查才定位到根因。混用是灾难的起点,因为一旦出现WebMvcConfigurationSupport子类,整个自动配置退出,所有WebMvcConfigurer虽然还能被扫描到,但谁来调用它们取决于有没有DelegatingWebMvcConfiguration在容器里。所以项目里最好统一规范,明确只用一种方式。

9.2 关于自定义拦截器的执行时机

我实际测试过一个很小的差异:继承WebMvcConfigurationSupport后,如果你手动补全的 Bean 顺序和自动配置不同,拦截器的执行顺序都可能不一样。WebMvcAutoConfiguration里对RequestMappingHandlerMapping的 order 是有默认设置的,直接继承后如果你自己创建RequestMappingHandlerMapping,这个 order 值得手动调整,否则可能出现 HandlerMapping 匹配顺序不对、接口 404 找不到 Handler 的问题。

9.3 我的最终选择建议

写到现在,结论已经非常清晰了:

默认项目用 WebMvcConfigurer,原因很简单:保留 Spring Boot 自动配置的完整默认能力,你的定制是增量式的,框架升级也不会影响太大。继承WebMvcConfigurationSupport是重武器,只用于你确实需要完全接管 SpringMVC 初始化流程的场景,不要因为它“看起来更底层、更强大”就选它,因为它同时意味着你要承担所有默认配置的维护工作。

如果你刚接手一个老项目,发现里面用了继承写法,而项目本身又没有那么极端的定制需求,我建议你评估一下能否迁移回WebMvcConfigurer方案。迁移成本通常不高:把继承改为接口实现,把 private/protected 改成 public,然后手动补的那些默认配置可以删掉大部分,保留真正定制的部分。迁移后你会发现,之前因为“继承后静态资源 404”“JSON 序列化要手动配”而加的补救代码都能删掉了。

9.4 最后的实用技巧

最后分享一个做这个决策时的自我提问方法。遇到“自定义 SpringMVC 配置”的需求,先问自己三个问题:

  1. 我需要 Spring Boot 默认的静态资源映射、消息转换器、格式化器、内容协商这些基础设施吗?——绝大多数项目都需要,所以走WebMvcConfigurer。
  2. 我是要“在默认基础上加一点东西”,还是要“换掉整个基础”?——前者走接口,后者才考虑继承。
  3. 我是否需要替换RequestMappingHandlerMapping、RequestMappingHandlerAdapter这种核心 Bean?——业务项目基本不会,需要的话才考虑继承体系。

这三个问题一问,方向就不会跑偏。我自己在写新项目时默认模板就是implements WebMvcConfigurer,只有做一个需要完全自定义协议转换的中间件项目时才用过继承写法,那是真正需要从根基掌控 SpringMVC 的时候。把这两种写法的边界理解清楚,以后看到项目里奇怪的 404、JSON 报错、配置不生效,你就能快速判断是不是“继承选错”导致的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 9:59:26

Scratch 3.0指令清单:从积木到项目的实战指南

简介&#xff1a;Scratch 3.0指令大全是一份面向少儿编程学习者、家长与教师的系统参考资料。Scratch由美国麻省理工学院专为8至16岁孩子开发&#xff0c;采用类似搭积木的方式编写程序&#xff0c;极大降低了编程门槛&#xff0c;已广泛用于全球中小学课堂&#xff1b;文档覆盖…

作者头像 李华
网站建设 2026/10/17 9:59:10

大型集团指标体系构建:四层框架与平衡计分卡实战方案

简介&#xff1a;一套面向大型集团企业管理层、战略规划与运营管理人员的指标体系建设项目方案PPT&#xff0c;内容覆盖指标构建背景意义、SMART原则&#xff0c;并搭建战略目标层、业务板块层、运营管理层、支持保障层四层设计框架。在关键绩效指标设定方面&#xff0c;方案兼…

作者头像 李华
网站建设 2026/10/17 9:59:06

储能电池主动均衡方案:MP2642GR-Z与MKV44F256VLH16实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 9:58:48

MP2642GR-Z+PIC18F4680双芯主动均衡方案设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 9:58:30

C# Winform串口通讯上位机实战:协议解析、粘包处理与避坑指南

简介&#xff1a;这是一份面向C#初学者与工控上位机开发入门者的串口通讯实战项目源码&#xff0c;基于Winform实现完整的串口收发上位机&#xff0c;可解决设备控制与数据传输场景下的串口调试需求。压缩包共53个文件&#xff0c;约138KB&#xff0c;以cs源码、resx资源、cspr…

作者头像 李华
网站建设 2026/10/17 9:57:34

DenseNet核心解析与PyTorch手写实现:从原理到面试高频考点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华