本节目标:搞清 Spring Boot 默认从哪些目录查找静态资源、为什么可以直接用 URL 访问,掌握自定义资源映射、缓存控制与内容哈希指纹,并能判断静态资源与接口路径冲突时的优先级。
适用版本:Spring Boot 4.1.x(Java 21)
11.1 静态资源映射
前面十章,图书服务一直在返回 JSON。但一个能交付的项目通常还需要一个最简单的管理页面:一张展示图书列表的 HTML,配套的 CSS、JavaScript,以及每本书的封面图片。这些不需要经过 Controller 处理、也不该被 Java 代码一个个映射,它们就是静态资源。
Spring Boot 对静态资源的支持是「约定优于配置」的典型:把文件丢进固定目录,启动后就能通过 URL 直接访问,一行配置都不用写。本节先讲清这套约定,再讲怎么在需要时覆盖它。
11.1.1 一个最小的静态页面
在 src/main/resources/ 下新建 static/index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图书管理</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<h1>图书管理</h1>
<script src="/js/app.js"></script>
</body>
</html>
再放两个文件:static/css/app.css 和 static/js/app.js。启动应用(./mvnw spring-boot:run),浏览器访问 http://localhost:8080/index.html,页面就出来了。访问 http://localhost:8080/ 同样返回这个页面——因为 index.html 被当作欢迎页。
注意:这里没有写任何 @Controller 或 @RequestMapping。这些文件是被 Spring MVC 内置的 ResourceHttpRequestHandler 处理的,它专门负责「把请求路径映射到 classpath(或文件系统)里的资源」。
11.1.2 四个默认位置与优先级
Spring Boot 默认从四个 classpath 位置查找静态资源,顺序如下:
| 顺序 | classpath 位置 | 物理路径(Maven 布局) |
|---|---|---|
| 1 | classpath:/META-INF/resources/ | src/main/resources/META-INF/resources/ |
| 2 | classpath:/resources/ | src/main/resources/resources/ |
| 3 | classpath:/static/ | src/main/resources/static/ |
| 4 | classpath:/public/ | src/main/resources/public/ |
这个顺序由 WebMvcAutoConfiguration 在注册资源处理器时定义,默认值就是这四项。
优先级规则:如果在多个位置放了同名文件(例如 static/app.css 与 public/app.css),排在前面的位置获胜。所以 META-INF/resources 优先级最高,public 最低。日常最常用的是 static/,把文件都放这里即可。
一个高频疑问:「
src/main/resources/resources/这层目录名是笔误吗?」不是。它是 classpath 根下的resources/目录,物理上就位于src/main/resources/里面,因此看起来重复。用到的概率很低,知道存在即可。
11.1.3 默认 URL 映射是 /**
静态资源默认映射到 /**,也就是说,请求路径原样对应资源路径:
| 请求 URL | 命中的资源 |
|---|---|
/index.html | static/index.html |
/css/app.css | static/css/app.css |
/js/app.js | static/js/app.js |
/ | 欢迎页,默认取 index.html |
因为映射是 /**,所以只要路径能对上文件,任何 URL 都能命中静态资源。这也是为什么「明明没写接口,访问 /abc.txt 却返回 404 而不是 404 JSON」——它先被静态资源处理器接管,找不到文件才 404。
11.1.4 static-locations 与 static-path-pattern 的区别
这两个属性名字很像,作用完全不同,是新手最容易混淆的地方。
spring.web.resources.static-locations:改变物理查找位置(从哪几个目录找文件)。默认值是上表那四项。spring.mvc.static-path-pattern:改变URL 匹配模式(用什么 URL 前缀访问)。默认值是/**。
spring:
web:
resources:
# 只从自定义目录找,注意会覆盖默认的四个位置
static-locations: classpath:/assets/,file:/opt/library/uploads/
mvc:
# 所有静态资源改到 /static/** 下访问
static-path-pattern: /static/**
配置后,static/index.html 要通过 http://localhost:8080/static/index.html 访问,直接访问 /index.html 会 404。同时因为 static-locations 被显式覆盖,默认四目录不再生效(除非在值里重新写回 classpath:/static/)。
| 属性 | 改的是 | 影响 |
|---|---|---|
spring.web.resources.static-locations | 文件在哪 | 换目录、加外部目录(如上传目录) |
spring.mvc.static-path-pattern | URL 长什么样 | 给静态资源加统一前缀,避免与接口抢路径 |
11.1.5 自定义映射:addResourceHandlers
属性只能做全局调整。如果要「某个 URL 前缀映射到某个特定目录」,就用 WebMvcConfigurer:
package com.example.library.config;
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 WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 封面图片存到服务器本地目录,通过 /covers/** 暴露
registry.addResourceHandler("/covers/**")
.addResourceLocations("file:/opt/library/covers/");
// 前端打包产物放在另一个路径前缀下
registry.addResourceHandler("/admin/**")
.addResourceLocations("classpath:/admin-static/")
.setCachePeriod(3600);
}
}
要点:
addResourceHandler是 URL 模式,addResourceLocations是 物理位置,顺序不要反。classpath:前缀指向打包进 jar 的资源;file:前缀指向磁盘目录,末尾必须带斜杠,否则拼接路径会出错。- 多个
addResourceLocations可以链式叠加,按声明顺序查找。 - 自定义映射不会覆盖默认的
/**,它是额外增加的一条映射。
安全提醒:把上传目录通过
file:直接暴露时,务必确认目录内没有可执行脚本或敏感文件。生产环境更推荐由 Nginx 直接托管静态文件,应用只负责生成。
11.1.6 缓存控制
静态资源(尤其图片、字体、打包后的 JS)体积大、变更少,是缓存的重点对象。Spring Boot 提供一组属性统一控制响应头 Cache-Control:
spring:
web:
resources:
cache:
cachecontrol:
max-age: 30d
cache-public: true
must-revalidate: false
对应的响应头:
Cache-Control: max-age=2592000, public
常用子项:
| 属性 | 对应指令 | 含义 |
|---|---|---|
cachecontrol.max-age | max-age | 客户端可缓存多久(支持 30d、1h 写法) |
cachecontrol.no-cache | no-cache | 允许缓存但每次必须回源校验 |
cachecontrol.no-store | no-store | 完全不允许缓存 |
cachecontrol.cache-public | public | 允许代理服务器缓存 |
cachecontrol.cache-private | private | 仅允许浏览器缓存 |
cachecontrol.must-revalidate | must-revalidate | 过期后必须重新校验 |
坑:max-age 设得越长,用户越可能看到旧文件。对文件名固定的 app.js 设 max-age=30d,一旦发版,用户可能一直用旧版本。解决办法就是下一节的指纹策略。
11.1.7 内容哈希指纹:让长缓存安全
思路很简单:把文件内容算成哈希,拼进文件名。文件一变,哈希变,URL 就变,浏览器自然重新下载;文件不变,URL 不变,缓存永远有效。
开启方式:
spring:
web:
resources:
chain:
strategy:
content:
enabled: true
paths: /**
开启后,Spring 会为每个静态资源计算内容哈希,并提供 ResourceUrlProvider 来生成带指纹的 URL。在模板(如 Thymeleaf)里用 @{/css/app.css} 会输出类似:
/css/app-7f3a9c2e8b1d4e5f.css
而在纯 HTML 或 Java 代码里,需要注入 ResourceUrlProvider:
@Service
public class AssetUrlService {
private final ResourceUrlProvider resourceUrlProvider;
public AssetUrlService(ResourceUrlProvider resourceUrlProvider) {
this.resourceUrlProvider = resourceUrlProvider;
}
public String fingerprinted(String path) {
return resourceUrlProvider.getForLookupPath(path);
}
}
getForLookupPath("/css/app.css") 返回带哈希的真实 URL;如果该路径不是静态资源,则返回 null。
关键前提:指纹 URL 只在「通过
ResourceUrlProvider生成」时才有意义。直接手写/css/app.css仍然访问原文件,只是享受不到指纹带来的长缓存收益。因此在开启指纹后,HTML 里的静态资源引用都要改成动态生成。
11.1.8 ResourceHttpRequestHandler 与 404
前面反复提到的 ResourceHttpRequestHandler 是一个特殊的 HttpRequestHandler,由 SimpleUrlHandlerMapping 注册,专门处理资源请求。它的行为链条是:
- 拿到请求路径,逐个在配置的 locations 里查找资源。
- 找到:返回文件内容,并应用缓存头。
- 找不到:抛
NoResourceFoundException(Spring Framework 6.1 引入,4.x 沿用),最终被解析为 404。
这解释了三个常见现象:
- 访问一个不存在的静态路径,返回的是 Spring 默认的 404 页面或 JSON,而不是异常堆栈。
- 如果你在第 10 章写的
@RestControllerAdvice里捕获了Exception,可能会意外吞掉这个 404,把它变成 500。不要用@ExceptionHandler(Exception.class)兜底一切,它会盖住框架的 404/405 处理。 - 自定义错误页应放在
static/error/404.html(配合ErrorController约定),而不是去接管NoResourceFoundException。
11.1.9 静态资源与 @RestController 路径冲突
如果静态资源目录里有个 books 文件,同时又有 @GetMapping("/books"),谁赢?
@RestController 赢。 原因是两条映射注册在不同的 HandlerMapping 上,优先级不同:
| HandlerMapping | 处理 | order |
|---|---|---|
RequestMappingHandlerMapping | @RequestMapping 注解的方法 | 0(最高优先级) |
SimpleUrlHandlerMapping | 静态资源 /** | Ordered.LOWEST_PRECEDENCE - 1(最低) |
DispatcherServlet 按 order 从小到大依次询问各 HandlerMapping,谁先返回非 null 的 handler 就用谁。RequestMappingHandlerMapping 的 order 是 0,永远排在资源映射前面,所以只要接口能匹配上,静态资源就没有机会。
实测验证:定义一个 @GetMapping("/books") 返回 JSON,同时在 static/ 放一个名为 books 的文件(无扩展名),访问 /books 得到的是 JSON,不是文件内容。
curl -i http://localhost:8080/books
HTTP/1.1 200 OK
Content-Type: application/json
[{"id":1,"title":"Effective Java"}]
反之,如果接口路径是 /books/{id} 而静态目录里有 books/1 这个文件,访问 /books/1 时仍然走接口,因为 /books/{id} 能匹配。规律:接口路径优先,静态资源只在接口匹配不到时兜底。
11.1.10 常见坑速查
| 坑 | 现象 | 解决 |
|---|---|---|
| 文件放错目录 | 访问 404 | 放 src/main/resources/static/ |
改 static-path-pattern 后旧 URL 失效 | 全部 404 | 前端引用同步改前缀 |
static-locations 覆盖默认值 | 原来能访问的资源全 404 | 在值里补回默认四项 |
file: 路径末尾缺斜杠 | 404 或路径拼接异常 | 写成 file:/opt/xxx/ |
@ExceptionHandler(Exception.class) 兜底 | 404 变成 500 | 别兜底所有异常 |
长 max-age + 固定文件名 | 发版后用户看到旧页面 | 开启内容哈希指纹 |
| 开启指纹后仍手写 URL | 指纹不生效 | 改用 ResourceUrlProvider 生成 |
小结
- 静态资源默认从
classpath:/META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/四处查找,顺序即优先级,同名文件靠前者胜出。 - 默认 URL 模式是
/**,请求路径原样对应资源路径;/会自动映射到欢迎页index.html。 spring.web.resources.static-locations改物理位置,spring.mvc.static-path-pattern改URL 模式,二者不可混淆。- 需要「特定前缀 → 特定目录」时用
WebMvcConfigurer#addResourceHandlers;classpath:与file:前缀、末尾斜杠都要注意。 - 缓存通过
spring.web.resources.cache.cachecontrol.*统一控制;要安全地长缓存,必须配合chain.strategy.content内容哈希指纹,并用ResourceUrlProvider生成 URL。 - 静态资源由
ResourceHttpRequestHandler处理,找不到文件抛NoResourceFoundException并转为 404,别用全局异常兜底把它变成 500。 - 静态资源与接口路径冲突时接口优先,因为
RequestMappingHandlerMapping的 order(0)远高于资源映射。
页面有了,缓存也配好了,但前端一旦从别的域名或端口调用图书接口,就会撞上浏览器的同源策略。下一节我们把 CORS 讲透。
阅读导航:上一节:10.3 统一响应封装 · 下一节:11.2 CORS 跨域 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。