1. 一个让人抓狂的 404:HTML 能开,CSS/JS 全挂
如果你正在维护一个从老项目迁移过来的 Spring Boot 工程,很可能遇到过这种诡异现象:浏览器里访问index.html一切正常,页面骨架能出来,但样式全丢、图标不显示、控制台一堆 404,后端日志里反复刷着o.s.web.servlet.PageNotFound : No mapping for GET /plugins/mdui/css/mdui.min.css。更迷惑的是,你翻遍application.yml,静态资源路径没动过,src/main/resources/static目录下文件明明躺着,可就是访问不到。
这个问题的核心检索词就是Spring Boot 静态资源 404、PageNotFound No mapping for、WebMvcConfigurationSupport与WebMvcConfigurer的取舍。它适合谁?适合正在做老项目升级、接手别人代码、或者自己写了个@Configuration类之后突然发现静态资源全挂的开发者。我试过在一个迁移项目里排查了整整一个下午,最后发现罪魁祸首是一个继承自WebMvcConfigurationSupport的日期格式化配置类——它把 Spring Boot 的 MVC 自动配置整个顶掉了。
这篇文章会带你走完完整链路:先讲清楚为什么会 404,再给出可复制的WebMvcConfigurer配置骨架,然后用curl和日志验证修复效果,最后把常见坑一个个列出来。中途我也会说明怎么用 TaoToken 统一 Key 和 API 通道,把 AI 辅助排查工具接进来,减少来回切换账号的麻烦。
2. 根因定位:WebMvcConfigurationSupport 为什么会让静态资源失效
2.1 两种配置方式的行为差异
Spring Boot 对 Spring MVC 做了大量自动配置,比如静态资源映射、消息转换器、视图解析器、拦截器等。这些自动配置生效的前提是:容器里没有一个WebMvcConfigurationSupport类型的 Bean。一旦你写了一个类继承WebMvcConfigurationSupport,Spring Boot 的WebMvcAutoConfiguration就会因为条件注解不满足而整体退避,默认的静态资源处理链随之消失。
而WebMvcConfigurer是一个接口,它只是扩展默认配置,不会触发自动配置退避。你实现它,等于在原有默认行为上追加自己的规则,静态资源映射依然保留。
| 对比项 | WebMvcConfigurationSupport | WebMvcConfigurer |
|---|---|---|
| 作用 | 完全接管 Spring MVC 配置 | 在默认配置上做扩展 |
| 对自动配置的影响 | 导致 MVC 自动配置整体失效 | 不影响,自动配置继续生效 |
| 静态资源处理 | 必须手动配置所有路径 | 默认路径保留,只需追加 |
| 视图解析器 | 需手动配置 | 自动配置 + 扩展 |
| 拦截器 | 需手动注册 | 自动配置 + 扩展 |
| 推荐场景 | 需要彻底自定义 MVC 行为 | 绝大多数业务扩展场景 |
2.2 老项目里最典型的触发代码
很多老项目会写一个日期格式化配置类,早期教程里常见这种写法:
@Configuration public class DateTimeConfig extends WebMvcConfigurationSupport { private static final Logger logger = LoggerFactory.getLogger(DateTimeConfig.class); @Bean public FormattingConversionService mvcConversionService() { logger.info("mvcConversionService 执行了"); DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false); DateTimeFormatterRegistrar dateTimeRegistrar = new DateTimeFormatterRegistrar(); dateTimeRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd")); dateTimeRegistrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); dateTimeRegistrar.registerFormatters(conversionService); DateFormatterRegistrar dateRegistrar = new DateFormatterRegistrar(); dateRegistrar.setFormatter(new DateFormatter("yyyy-MM-dd")); dateRegistrar.registerFormatters(conversionService); return conversionService; } }这段代码本身逻辑没问题,问题出在extends WebMvcConfigurationSupport。它一出现,classpath:/static/、classpath:/public/、classpath:/resources/、classpath:/META-INF/resources/这些默认静态资源位置全部不再自动映射,于是/plugins/mdui/css/mdui.min.css这类请求就落到了DispatcherServlet上,找不到对应 handler,抛出PageNotFound。
注意:
WebMvcConfigurerAdapter在 Spring 5 之后已被标记为@Deprecated,新代码不要再继承它,直接实现WebMvcConfigurer接口即可。
3. 可复制配置:用 WebMvcConfigurer 重写并保留静态资源
3.1 改造日期格式化配置类
把原来的继承关系换成实现接口,方法签名从@Bean改成addFormatters重写:
@Configuration public class DateTimeConfig implements WebMvcConfigurer { private static final Logger logger = LoggerFactory.getLogger(DateTimeConfig.class); @Override public void addFormatters(FormatterRegistry registry) { logger.info("DateTimeConfig: 注册自定义日期时间格式化器"); DateTimeFormatterRegistrar dateTimeRegistrar = new DateTimeFormatterRegistrar(); dateTimeRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd")); dateTimeRegistrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); dateTimeRegistrar.registerFormatters(registry); DateFormatterRegistrar dateRegistrar = new DateFormatterRegistrar(); dateRegistrar.setFormatter(new DateFormatter("yyyy-MM-dd")); dateRegistrar.registerFormatters(registry); } }这样日期格式化能力保留,同时 Spring Boot 的静态资源自动配置不受影响。
3.2 追加自定义静态资源映射
如果你的静态资源不在默认目录,比如放在classpath:/static/plugins/下,需要额外映射/plugins/**,可以单独写一个配置类:
@Configuration public class WebResourceConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/plugins/**") .addResourceLocations("classpath:/static/plugins/") .setCachePeriod(0); } }setCachePeriod(0)在开发阶段很有用,避免浏览器缓存旧文件导致你以为没生效。生产环境可以改成3600或交给 Nginx 处理。
3.3 开启静态资源调试日志
在application.yml里加上:
logging: level: org.springframework.web: DEBUG org.springframework.web.servlet.resource: DEBUG启动后访问静态资源,正常应该看到类似日志:
DEBUG o.s.w.s.r.ResourceHttpRequestHandler - Resource found: class path resource [static/plugins/mdui/css/mdui.min.css]如果看到的是No mapping for GET,说明映射链还是没生效,回到第 2 节检查是否还有别的类继承了WebMvcConfigurationSupport。
4. 验证请求:用 curl 和 jar 包内容确认修复
4.1 先确认资源真的打进了 jar
很多时候 404 不是配置问题,而是资源根本没被 Maven 打包进去。用下面命令检查:
jar -tvf target/*.jar | grep "mdui.min.css"正确输出应该类似:
BOOT-INF/classes/static/plugins/mdui/css/mdui.min.css如果这里没有输出,说明pom.xml的resources配置或build插件把静态目录排除了,先修打包再谈映射。
4.2 用 curl 验证静态资源
启动应用后执行:
curl -v http://localhost:8080/plugins/mdui/css/mdui.min.css预期响应头:
HTTP/1.1 200 Content-Type: text/css Content-Length: 12345如果返回404,再看响应体里有没有No mapping for GET字样,有的话就是映射问题;如果返回200但内容为空,检查addResourceLocations路径末尾的斜杠——classpath:/static/plugins/结尾必须带/,否则会拼错路径。
4.3 验证接口映射是否也正常
静态资源修好后,顺手确认业务接口没被影响:
curl -v http://localhost:8080/api/user/1预期返回 JSON 数据。如果接口也 404,说明你的WebMvcConfigurer实现里可能覆盖了configurePathMatch或addInterceptors导致路径匹配异常,逐个方法排查。
4.4 用 TaoToken 统一通道接入 AI 排查工具
排查这类问题时,我习惯把日志片段和配置代码丢给 AI 工具做交叉分析。但多个工具各自要 Key、各自要配 Base URL,切换起来很烦。TaoToken 提供统一的 API 通道,把模型对话、编码辅助等能力收敛到一个 Key 上。
接入方式很简单,在需要配置 Base URL 的地方填:
https://taotoken.net/api然后在对应工具的配置里填入你在控制台生成的 Key。控制台地址是https://taotoken.net/console,Key 管理在https://taotoken.net/api-keys。如果你用的是 Claude Code 这类编码 Agent,可以参考https://taotoken.net/ClaudeCodeAnthropic的接入说明;想先验证模型是否通,直接打开https://taotoken.net/models对话测试即可。长期做编码和 Agent 任务的话,Coding Plan 页面https://taotoken.net/coding-plan有更细的配置说明。
提示:TaoToken 只是统一 Key 和 API 通道,不替代你的编辑器或 IDE,排查逻辑还是得自己走一遍。
5. 本篇常见错排查清单
5.1 还有别的类继承了 WebMvcConfigurationSupport
这是最常见的漏网之鱼。全局搜索:
grep -rn "extends WebMvcConfigurationSupport" src/main/java只要还有一个,静态资源就继续 404。全部改成implements WebMvcConfigurer。
5.2 addResourceLocations 路径末尾漏斜杠
// 错误写法,会导致路径拼接异常 .addResourceLocations("classpath:/static/plugins") // 正确写法 .addResourceLocations("classpath:/static/plugins/")5.3 拦截器把静态资源也拦了
如果你注册了拦截器且addPathPatterns("/**"),静态资源请求也会被拦截。需要在excludePathPatterns里排除:
registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/static/**", "/plugins/**", "/css/**", "/js/**");5.4 Spring Security 默认拦截静态资源
引入 Spring Security 后,默认所有请求都要认证,静态资源也会被拦。需要在配置里放行:
http.authorizeRequests() .antMatchers("/plugins/**", "/css/**", "/js/**", "/images/**").permitAll() .anyRequest().authenticated();5.5 打包时资源被过滤
检查pom.xml的<resources>配置,确保src/main/resources下的静态文件没有被excludes排除。默认 Spring Boot Starter Parent 已经处理好,但老项目手动配置过就容易出问题。
5.6 用了 @EnableWebMvc 注解
@EnableWebMvc同样会导入DelegatingWebMvcConfiguration,它继承自WebMvcConfigurationSupport,效果和直接继承一样——自动配置全失效。检查启动类或配置类上有没有这个注解,有的话删掉。
6. 把 AI 排查接进你的工作流
静态资源 404 这类问题,本质是配置优先级和自动配置退避机制在作怪。记住一条铁律:在 Spring Boot 里做 MVC 扩展,永远优先实现WebMvcConfigurer,不要继承WebMvcConfigurationSupport,也不要加@EnableWebMvc。只有当你确实需要彻底接管 MVC 行为、放弃所有自动配置时,才考虑后者。
排查流程可以固化成三步:先用jar -tvf确认资源打进了包,再用curl -v确认 HTTP 状态码和响应头,最后开 DEBUG 日志看ResourceHttpRequestHandler有没有找到资源。三步走完,问题基本定位。
如果你在多个 AI 工具之间切换做代码分析,可以用 TaoToken 把 Key 和 API 通道统一起来,减少配置成本。接入文档在https://taotoken.net/doc,模型验证在https://taotoken.net/models,编码 Agent 场景看https://taotoken.net/coding-plan。把精力留给真正的排查逻辑,而不是反复填 Key。