《Spring Boot 入门》11.1 静态资源映射

图书服务要加一个管理页面,HTML、CSS、JS 与封面图该放哪、用什么 URL 访问?本节讲清 Spring Boot 四个默认静态资源目录及优先级、默认 /** 映射规则,区分 static-locations 与 static-path-pattern,并演示自定义映射、缓存控制与内容哈希指纹,最后说清静态资源与接口路径冲突时谁优先。

本节目标:搞清 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 布局)
1classpath:/META-INF/resources/src/main/resources/META-INF/resources/
2classpath:/resources/src/main/resources/resources/
3classpath:/static/src/main/resources/static/
4classpath:/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.htmlstatic/index.html
/css/app.cssstatic/css/app.css
/js/app.jsstatic/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-patternURL 长什么样给静态资源加统一前缀,避免与接口抢路径

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-agemax-age客户端可缓存多久(支持 30d、1h 写法)
cachecontrol.no-cacheno-cache允许缓存但每次必须回源校验
cachecontrol.no-storeno-store完全不允许缓存
cachecontrol.cache-publicpublic允许代理服务器缓存
cachecontrol.cache-privateprivate仅允许浏览器缓存
cachecontrol.must-revalidatemust-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 注册,专门处理资源请求。它的行为链条是:

  1. 拿到请求路径,逐个在配置的 locations 里查找资源。
  2. 找到:返回文件内容,并应用缓存头。
  3. 找不到:抛 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 跨域 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计