只要写过几天 Spring Boot,十有八九会撞上这么一行日志:No mapping for GET /index。第一次遇到时,我第一反应是查文件是不是放错了位置,结果项目里明明有static/index.html,浏览器依然给你一个 404。后来才彻底想明白,这句话并不是在说“文件不存在”,而是在说“Spring MVC 的映射器列表里,没有任何一个 Handler 接住了这个请求”。这篇文章专门讲 Spring Boot 2.x 下这个报错的定位逻辑和处理思路,包括默认静态资源映射的原理、手动注册资源处理器、Controller 抢占路由的兜底方案,最后给一份可以直接照着做的排查清单。适合刚接触 Spring Boot 的新人,也适合在旧项目里被 404 折磨过一阵子的老手。
1. 先拆解这句话:No mapping for GET XXXX 到底是谁在喊
1.1 映射表里没找到处理器,不等于文件不存在
Spring MVC 处理一个请求的入口是DispatcherServlet,我们可以把它理解成一个前台调度员。浏览器把/index这个请求丢进来之后,DispatcherServlet会拿着这个路径去问所有HandlerMapping:你们谁有办法处理?RequestMappingHandlerMapping负责所有@Controller和@RequestMapping注解的映射,ResourceHandlerMapping负责静态资源映射,WelcomePageHandlerMapping负责欢迎页。如果一轮问下来,所有 HandlerMapping 都返回 null,DispatcherServlet就会记录一条类似No mapping for GET /index的日志,然后返回 404。
所以这句话的本质是“没有任何控制器或者资源处理器认领这个请求”。它不等于文件系统里没有这个文件,也不等于文件内容坏了。这是排查时最容易走入误区的地方,很多人一看到日志就打开资源目录反复确认,却忽略了真正的问题可能是「路径不匹配 Controller」或「静态资源的映射本身被关了」。
1.2 静态资源在 Spring MVC 里也是被一个 Handler 处理的
很多初级开发者以为静态资源是 Tomcat 直接返回的,实际上在 Spring Boot 2.x 中,静态资源也是走 Spring MVC 处理的,它由一个ResourceHttpRequestHandler接手。Spring Boot 自动配置会注册一个匹配路径/**的 ResourceHandler,把请求映射到固定的几个 classpath 目录。因此,“资源文件存在但访问不到”这件事,同样会被 Spring MVC 当成“没有 Handler 匹配”,从而出现No mapping日志。
这个设计的含义是:如果你把资源放到了默认目录之外,或者自定义配置时把默认的 ResourceHandler 覆盖掉了,即使文件躺在项目里,Spring MVC 也根本不知道它的存在。所以处理这个报错,不能只盯着文件,要看“请求路径 -> HandlerMapping -> 资源位置”这条完整的链路。
1.3 最常见的几种触发场景
为了省时间,先把高频场景列出来,大家可以对号入座:
| 触发场景 | 典型现象 | 根因方向 |
|---|---|---|
| 资源放错目录 | 文件在resources/templates,但访问/xxx.html | 默认静态资源目录里没有它 |
| 请求路径写错 | 文件在static/css/app.css,请求/static/css/app.css | 默认情况下路径不需要static前缀 |
| 自定义配置覆盖默认 | 加了@EnableWebMvc或手动注册了/** | 自动配置静态映射失效 |
| 监听端口或上下文路径 | 项目配置了server.servlet.context-path | URL 前面需要加前缀 |
| 构建产物缺少文件 | IDEA 里能看到,但target/classes里没有 | Maven 资源拷贝或 IDE 增量编译问题 |
| 路由被 Controller 抢占 | 同时存在@GetMapping("/report")和static/report.html | Controller 优先级更高 |
把场景分清楚之后,再往下看原理,思路会更顺。
2. Spring Boot 默认静态资源映射:四个目录和一个自动配置开关
2.1 默认位置和匹配规则
Spring Boot 2.x 的自动配置类WebMvcAutoConfiguration会给静态资源注册一个默认的 ResourceHandler,匹配路径是/**,并按照顺序去这几个 classpath 目录查找文件:
classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public/
换句话说,你新建一个 Spring Boot 项目时,默认的 Maven 工程结构里会有一个src/main/resources/static目录,这个目录就是官方留给你的静态资源目录。所有放到它下面的文件,理论上都可以通过根路径直接访问。
一个标准的目录结构可以是这样的:
src/main/resources/ ├── static/ │ ├── index.html │ ├── css/ │ │ └── app.css │ └── js/ │ └── app.js对应关系就是:
| 请求地址 | 实际磁盘位置 |
|---|---|
/index.html | src/main/resources/static/index.html |
/css/app.css | src/main/resources/static/css/app.css |
/js/app.js | src/main/resources/static/js/app.js |
这里有一个关键点:请求路径不要写成/static/css/app.css。除非你额外配置了spring.mvc.static-path-pattern,否则默认的匹配模式/**会把/static/css/app.css解析成classpath:/static/static/css/app.css,当然找不到。
另外还要注意,src/main/resources/templates不是默认静态资源目录。这个目录是给模板引擎准备的,比如 Thymeleaf、Freemarker。把demo.html放到templates下,然后直接访问/demo.html,同样会出现No mapping for GET /demo.html,除非你配置了视图解析器并通过 Controller 返回对应视图名。
2.2 自动配置的条件:一点开关就能让它不生效
Spring Boot 之所以“开箱即用”,不是因为什么黑魔法,而是因为自动配置类带了一堆@Conditional条件。静态资源默认映射也不是无条件生效的,下面两个情况最容易让它失效。
第一个是配置项:
spring.web.resources.add-mappings=false这个配置直接告诉 Spring Boot:不要给静态资源注册默认的 HandlerMapping。有些项目为了统一做权限控制,会刻意关掉它,然后自己写一套资源分发逻辑。如果你在一个老项目里看到No mapping for GET ...,优先去application.properties或application.yml里搜一下这个配置,把它改成true或者删掉。
第二个是@EnableWebMvc注解。这一点非常隐蔽,因为一个模块里可能不是你自己加的,而是某个公共配置类上加了它。
@Configuration @EnableWebMvc public class SomeMvcConfig { // ... }@EnableWebMvc会导入DelegatingWebMvcConfiguration,而它继承自WebMvcConfigurationSupport。WebMvcAutoConfiguration上有类似@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)的条件,一旦项目里出现WebMvcConfigurationSupport,自动配置就会退出。结果就是 Spring Boot 默认注册的静态资源映射全部失效。
这个坑我印象很深,因为很多时候你并没有想关掉静态资源映射,只是想加一个拦截器或者消息转换器,结果网上抄了一段@EnableWebMvc的配置,整个项目的静态资源就全 404 了。Spring Boot 官方建议的方式是实现WebMvcConfigurer接口,而不是使用@EnableWebMvc。
2.3 版本差异:2.x 和旧版本配置项不一样
Spring Boot 1.x 里配置静态资源目录用的是spring.resources.static-locations。到了 Spring Boot 2.x,这个配置被移到了spring.web.resources.static-locations下面。
很多从老项目升级上来的朋友,会把旧配置原封不动地复制到application.properties里。复制之后没有任何报错,但静态资源依然找不到,因为配置项压根没有生效。下面这两个写法是常见误区:
# 错误示例:1.x 的配置,在 2.x 中不一定生效 spring.resources.static-locations=classpath:/static/# 正确示例:Spring Boot 2.x 的配置 spring.web.resources.static-locations=classpath:/static/不要小看这个前缀差异,遇到静态资源问题先检查一下配置键名,往往能省很多时间。
另外,spring.mvc.static-path-pattern这个配置在 2.x 中仍然可以用。它控制的是静态资源匹配的 URL 模式,默认是/**。如果你把它改成/assets/**,那么原来访问/css/app.css就需要变成/assets/css/app.css。通常情况下不建议乱改,除非你有强烈的路径隔离需求。
2.4 自定义配置时最坑的写法:手动覆盖/**
有些朋友在自定义WebMvcConfigurer时会这样做:
@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); } }这样写的后果是什么?@EnableWebMvc已经让自动配置失效了,然后你手动注册了一个/**的 ResourceHandler,只指向classpath:/static/。看起来好像实现了“静态资源映射”,但实际上默认的classpath:/public/、classpath:/resources/、classpath:/META-INF/resources/全部丢掉了,而且后续维护的人很容易忘记这里做过什么。
更安全的做法是不要加@EnableWebMvc,只在配置类里实现WebMvcConfigurer,并且不要动/**,只新增你自己业务需要的前缀映射。这样做的好处是 Spring Boot 默认的静态资源映射还在,你只是在它之外额外增加了一组映射。
3. 最省事的解法:把资源放对目录,并确认它进入构建产物
3.1 标准目录对应关系:直接用 static 就好
如果你是在一个新项目里遇到No mapping for GET XXXX,先看一下自己的资源文件放在哪里。最省事、最不会出错的方案就是放在src/main/resources/static下面,按照请求路径组织目录。
举个具体例子,如果你有一个文件路径是:
src/main/resources/static/2025/logo.png那么浏览器访问地址就是:
http://localhost:8080/2025/logo.png这里不要想当然地在路径前面加/static,也不要加resources。Spring Boot 的静态资源定位逻辑是“URL 去掉 context-path 和 static-path-pattern 后,直接到所有配置的 resource locations 里找”,而不是把 URL 当成工程文件路径来映射。
3.2 自定义 static-locations 的正确姿势
如果你的项目结构比较特殊,比如多个模块共享静态资源,或者资源不在默认目录里,可以通过spring.web.resources.static-locations自定义目录列表。这里要记住一个原则:这个配置是整体替换,不是追加。
spring.web.resources.static-locations=classpath:/static/,classpath:/public/,file:/data/static/上面这个例子告诉 Spring Boot:静态资源去classpath:/static/、classpath:/public/以及磁盘上的/data/static/目录找。如果写成下面这样,默认的四个目录就不会生效:
spring.web.resources.static-locations=classpath:/custom/那默认的classpath:/public/、classpath:/resources/全部没了,项目里原有的其它静态文件就可能突然 404。
还需要注意:file:指向的外部目录必须以/结尾,否则 Spring 的ResourceHttpRequestHandler在拼接相对路径时会出问题。多个目录直接用逗号分隔,在 YAML 中则写成数组。
spring: web: resources: static-locations: - classpath:/static/ - file:/data/static/3.3 验证资源是否真正进入构建产物
这是排查静态资源问题最重要的一步。有时候你用的是 IDEA,项目窗口里能看到static目录和文件,但一启动浏览器就是 404。原因很可能是 IDEA 的增量编译没有把新加的资源拷贝到target/classes,或者 Maven 构建时因为资源过滤配置把某些文件排除了。
先看目录,最简单的方法是打开项目编译输出目录:
target/classes/static/如果里面没有你刚才新建的文件,先执行一次mvn clean或者Build -> Rebuild Project。很多人遇到静态资源 404,其实只是 IDE 缓存问题,clean 一次之后就好了。
打成 jar 包后,也可以用命令确认:
jar tf app.jar | grep static如果 jar 包里没有对应的静态资源文件,那就不是 Spring Boot 映射的问题,而是构建过程没有把资源打进去。这时候去检查 pom.xml 里有没有对<resources>做过特殊配置,比如把**/*.html排除在外。
4. 更可控的解法:通过 addResourceHandlers 手动绑定路径
4.1 最简单的代码示例
当默认目录不够用,或者你希望某个 URL 前缀单独指向一个目录时,可以自己实现WebMvcConfigurer的addResourceHandlers方法。这是 Spring Boot 2.x 推荐的扩展方式,不用加@EnableWebMvc。
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class StaticResourceConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc/**") .addResourceLocations("classpath:/doc/", "file:/data/doc/") .setCachePeriod(3600); } }这段代码的含义是:所有/doc/开头的请求,先到classpath:/doc/目录里找文件,找不到再去磁盘的/data/doc/目录找。setCachePeriod(3600)是可选配置,表示浏览器缓存秒数。
假设磁盘上有这样一个文件:
/data/doc/manual.pdf那么访问:
http://localhost:8080/doc/manual.pdf就能拿到文件。这种方式特别适合对接上传目录、导出报表目录、外部存储目录。
4.2 两个容易踩的细节:结尾斜杠和 @EnableWebMvc
第一个细节是addResourceLocations里的路径必须以/结尾。Spring 在拼接资源路径时,会把/doc/**里的**匹配到的部分直接追加到 location 后面。如果 location 写成了classpath:/doc,最终解析就会变成类似classpath:/docmanual.pdf的错误地址,或者干脆匹配不到文件。这个斜杠问题非常隐蔽,出错后不会有明显异常,只表现为 404。
第二个细节是不要顺手加@EnableWebMvc。如前面说的,这个注解会让 Spring Boot 的WebMvcAutoConfiguration整体失效。你只是想加一个资源映射,结果把默认映射也关了。正确姿势是只写@Configuration和implements WebMvcConfigurer。
另外,如果你在自定义配置里也注册了/**,并且没有把默认目录全部列出来,那等于手动制造了一个 bug。我一般建议自定义ResourceHandler时使用有业务含义的前缀,比如/doc/**、/upload/**,避免和默认的/**冲突。
4.3 适用场景:磁盘目录、上传文件、多模块资源
手动注册资源映射最常见的需求是暴露上传文件。比如用户头像上传后存在某个磁盘路径/data/avatar/,你不想让请求经过 Controller 再读文件,而是希望直接通过 URL 访问,就可以这样做:
registry.addResourceHandler("/avatar/**") .addResourceLocations("file:/data/avatar/") .setCachePeriod(86400);这样/avatar/123.png会直接读取磁盘上的文件,性能比 Controller 里转流高不少。
另一个场景是多模块项目。某个模块里把静态资源放在了自己的src/main/resources/module1/static下面,主项目希望用/m1/**来访问它:
registry.addResourceHandler("/m1/**") .addResourceLocations("classpath:/module1/static/");这样不同模块之间的静态资源可以做到路径隔离,不会互相覆盖。
4.4 关于 WebMvcConfigurerAdapter 的提醒
如果搜到旧博客,可能会看到extends WebMvcConfigurerAdapter的写法。这个类在 Spring 5 已经废弃,在 Spring Boot 2.x 中虽然暂时还能用,但强烈不建议再写。新代码一律使用implements WebMvcConfigurer,省得以后升级框架时到处改。
5. Controller 把资源请求抢走了,怎么办
5.1 为什么 Controller 优先级比静态资源处理器高
Spring MVC 中,RequestMappingHandlerMapping的优先级高于静态资源处理器的 HandlerMapping。这意味着如果同时存在一个@GetMapping("/report")方法和一个static/report.html文件,请求/report一定会先被 Controller 方法接住。如果这个方法没有按预期返回页面,而是返回了一个对象或抛了异常,你最终看到的就是一个 404 或者 JSON。
这类问题很难一眼发现,因为从目录结构上看,静态文件确实存在,Spring Boot 的静态资源映射看起来也是正常的,但路径已经被某个隐蔽的 Controller 拦截了。排查的时候建议全局搜索一遍@GetMapping、@RequestMapping,看有没有和静态资源路径重合的接口。
5.2 没有动态逻辑时,改路径最简单
如果你的本意是暴露一个静态文件,根本不需要 Controller 参与,那就直接改掉其中一方。最简单的方式是让 Controller 走/api/report,静态资源继续走/report;或者反过来把静态资源放到/report-page.html。不要把静态资源和接口放在同一个路径上,否则优先级规则会让静态资源永远“隐形”。
5.3 需要 Controller 介入时,用 Resource 返回兜底
有些场景下确实需要 Controller 先做一些逻辑,比如权限校验、动态决定返回哪个文件。这时候可以不让 Spring 的静态资源处理器接管,而是直接在接口里返回一个ResponseEntity<Resource>。
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ResourceController { @GetMapping("/report") public ResponseEntity<Resource> report() { Resource resource = new ClassPathResource("static/report.html"); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE) .body(resource); } }这里new ClassPathResource("static/report.html")会从 classpath 下读取文件,ResponseEntity<Resource>由 Spring MVC 的ResourceHttpMessageConverter处理并输出到浏览器。优点是逻辑都在 Controller 里,适合做权限控制;缺点是你绕过了静态资源的缓存和压缩配置,性能不如直接让资源处理器处理。
如果你只是想内部转发到某个静态资源,也可以这样写:
@GetMapping("/report") public String report() { return "forward:/report.html"; }但要注意,forward之后 Spring MVC 会重新进入处理器查找流程,最终是否成功仍然取决于/report.html有没有对应的 Handler。如果静态映射本身已经被关了,forward依然会落到No mapping for GET /report.html。
5.4 模板引擎和静态资源不要混为一谈
如果把report.html放在templates目录里,并且项目里用了 Thymeleaf,那么 Controller 可以直接返回视图名:
@GetMapping("/report") public String report() { return "report"; }这是模板渲染流程,不是静态资源流程。它的结果是经过模板引擎解析后返回 HTML。这个方案在需要页面里嵌入动态数据时是好选择;但如果只是要一个纯静态文件,没必要引入模板引擎。
6. 从遇到报错到定位根因的排查清单
6.1 第一步:确认报错是不是从 DispatcherServlet 发出来的
看到 404 先不要慌,先看日志。如果日志里明确出现了No mapping for GET XXXX,说明请求已经进入了 Spring MVC,但没有任何 Handler 匹配。如果浏览器 404,但控制台完全没有相关日志,那问题可能根本不在 Spring MVC,比如应用没启动成功、端口不对、部署路径不对、反向代理配置有问题。
这一步是定位方向的,方向错了后面做再多都是白费。
6.2 第二步:按顺序检查资源映射的关键节点
建议按下面这个顺序去查,不要跳步:
- 检查请求 URL 的 context-path。如果配置了
server.servlet.context-path=/demo,那实际访问地址应该是http://localhost:8080/demo/xxx,而不是/xxx。 - 检查
spring.web.resources.add-mappings是否被设置成了false。如果是,默认静态资源映射被关闭。 - 检查项目里是否加了
@EnableWebMvc,或者是否有类继承了WebMvcConfigurationSupport。 - 检查文件是不是放在默认静态目录里,或者
static-locations有没有配置到对应目录。 - 检查编译产物。打开
target/classes/static,看文件在不在。如果不在,先 clean 再试。 - 检查路径中是否多写了
static或resources前缀。 - 全局搜索 Controller,看有没有路径冲突。
这七步走完,绝大部分No mapping for GET XXXX都能定位。
6.3 第三步:用日志确认 Spring MVC 里注册了哪些路径
如果项目很大,Controller 很多,想确认某个路径到底有没有被注册,可以在application.properties里临时打开调试日志:
logging.level.org.springframework.web.servlet.mvc.method.annotation=DEBUG logging.level.org.springframework.web=DEBUG启动日志里能看到 Spring MVC 注册的各种 RequestMapping,也能看到静态资源处理器的匹配过程。不过这个日志量很大,只建议排查时临时打开,定位完就关掉。
如果你不想看全量日志,也可以在启动类或配置类里临时注入RequestMappingHandlerMapping,打印一下所有注册路径。但是这种手段偶尔用用就好,别留在生产代码里。
6.4 临时绕过 vs 根治:要分清短期止血和长期修复
有时候线上急着发布,没时间调整整个资源目录结构。可以先用 Controller 转发或ResponseEntity<Resource>兜底,先把功能跑起来。但这是临时方案,它避开了静态资源处理器,也失去了默认的缓存、编码等配置。后续还是要回到资源目录、路径规划、自动配置条件这几个方向去确认根因,否则换个 URL 变体又会遇到同样的 404。
我个人在排查这类问题时有个习惯:先打开target/classes/static目录看一眼,再决定要不要改配置。很多所谓No mapping其实根本不是映射配置错了,而是 IDE 增量编译没把资源拷进去,mvn clean一下问题直接消失。如果你现在也卡在这个报错里,不妨先 clean 一次再继续查,可能比改代码更有效。