静态文件默认带不带缓存头?怎么让浏览器缓存一年又保证发布后能更新?内容哈希版本化到底改了什么?为什么设了长缓存还会拿到旧文件?add-mappings=false 关掉了什么?这篇在 Spring Boot 4.0.8 上逐个请求实测响应头。
GET /hello.txt → 200 '0123456789abcdefghij'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/plain', 'Content-Length': '21'}
GET /app.js → 200 'console.log("app v1");'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/javascript', 'Content-Length': '23'}
GET /css/site.css → 200 'body{color:red}'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/css', 'Content-Length': '16'}
GET / → 200 '<h1>welcome</h1>'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/html;charset=UTF-8', 'Content-Length': '17'}
GET /pub.txt → 200 'in public/'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/plain', 'Content-Length': '11'}
GET /nope.txt → 404 '{"timestamp":"2026-10-19T03:51:48.077Z",'
响应头:{'Content-Type': 'application/json'}
HEAD /hello.txt → 200 ''
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/plain', 'Content-Length': '21'}
GET /over.txt → 200 '来自控制器的 over.txt'
响应头:{'Content-Type': 'text/plain;charset=UTF-8', 'Content-Length': '27'}只有 Last-Modified 和 Accept-Ranges;控制器优先于同名静态文件。
GET /hello.txt → 200 '0123456789abcdefghij'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/plain', 'Content-Length': '21'}
条件请求:GET /hello.txt [If-Modified-Since: Mon, 19 Oct 2026 03:51:45 GMT] → 304 ''
响应头:{'Last-Modified': '<日期>'}
条件请求(旧日期):GET /hello.txt [If-Modified-Since: Mon, 01 Jan 2001 00:00:00 GMT] → 200 '0123456789abcdefghij'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/plain', 'Content-Length': '21'}
Range:GET /hello.txt [Range: bytes=0-4] → 206 '01234'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Range': 'bytes 0-4/21', 'Content-Type': 'text/plain', 'Content-Length': '5'}
Range:GET /hello.txt [Range: bytes=15-] → 206 'fghij'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Range': 'bytes 15-20/21', 'Content-Type': 'text/plain', 'Content-Length': '6'}
Range:GET /hello.txt [Range: bytes=999-1000] → 416 '{"timestamp":"2026-10-19T03:51:49.766Z",'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Range': 'bytes */21', 'Content-Type': 'application/json'}304、206、416 都是内置的。
-- spring.web.resources.cache.period=3600
GET /hello.txt → 200 '0123456789abcdefghij'
响应头:{'Cache-Control': 'max-age=3600', 'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/plain', 'Content-Length': '21'}
GET /app.js → 200 'console.log("app v1");'
响应头:{'Cache-Control': 'max-age=3600', 'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/javascript', 'Content-Length': '23'}
-- cachecontrol.max-age=1d、cache-public、must-revalidate
GET /hello.txt → 200 '0123456789abcdefghij'
响应头:{'Cache-Control': 'max-age=86400, must-revalidate, public', 'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/plain', 'Content-Length': '21'}
-- cachecontrol.no-cache=true
GET /hello.txt → 200 '0123456789abcdefghij'
响应头:{'Cache-Control': 'no-cache', 'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/plain', 'Content-Length': '21'}所有静态资源同一个值,长缓存不能配全局。
GET /url?path=/app.js → 200 '/app-0a5e4fedba3fc41d4a9932a0f872f7aa.js'
响应头:{'Content-Type': 'text/plain;charset=UTF-8', 'Content-Length': '40'}
GET /url?path=/css/site.css → 200 '/css/site-b3fc414da2e8cd21fb476f6870d7fa'
响应头:{'Content-Type': 'text/plain;charset=UTF-8', 'Content-Length': '46'}
GET /url?path=/hello.txt → 200 '/hello-3431dfdfe9c4653c16088037b05ad206.'
响应头:{'Content-Type': 'text/plain;charset=UTF-8', 'Content-Length': '43'}
未版本化的原路径:GET /app.js → 200 'console.log("app v1");'
响应头:{'Cache-Control': 'max-age=31536000, public', 'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/javascript', 'Content-Length': '23'}
带内容哈希的路径:GET /app-0a5e4fedba3fc41d4a9932a0f872f7aa.js → 200 'console.log("app v1");'
响应头:{'Cache-Control': 'max-age=31536000, public', 'Last-Modified': '<日期>', 'ETag': 'W/"0a5e4fedba3fc41d4a9932a0f872f7aa"', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/javascript', 'Content-Length': '23'}
错误的哈希:GET /app-00000000000000000000000000000000.js → 404 '{"timestamp":"2026-10-19T03:51:53.836Z",'
响应头:{'Content-Type': 'application/json'}
条件请求:GET /app-0a5e4fedba3fc41d4a9932a0f872f7aa.js [If-Modified-Since: Mon, 19 Oct 2026 03:51:45 GMT] → 304 ''
响应头:{'Cache-Control': 'max-age=31536000, public', 'Last-Modified': '<日期>'}URL 随内容变化,长期缓存才安全;原路径也被套上长缓存是陷阱。
GET /url?path=/app.js → 200 '/v7/app.js'
响应头:{'Content-Type': 'text/plain;charset=UTF-8', 'Content-Length': '10'}
版本化路径:GET /v7/app.js → 200 'console.log("app v1");'
响应头:{'Last-Modified': '<日期>', 'ETag': 'W/"v7"', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/javascript', 'Content-Length': '23'}
原路径:GET /app.js → 200 'console.log("app v1");'
响应头:{'Last-Modified': '<日期>', 'Accept-Ranges': 'bytes', 'Content-Type': 'text/javascript', 'Content-Length': '23'}手动维护的路径前缀,ETag 都一样。
GET /hello.txt → 404 '{"timestamp":"2026-10-19T03:51:56.085Z",'
响应头:{'Content-Type': 'application/json'}
GET /pub.txt → 404 '{"timestamp":"2026-10-19T03:51:56.101Z",'
响应头:{'Content-Type': 'application/json'}
GET /over.txt → 200 '来自控制器的 over.txt'
响应头:{'Content-Type': 'text/plain;charset=UTF-8', 'Content-Length': '27'}
GET /index.html → 404 '{"timestamp":"2026-10-19T03:51:56.105Z",'
响应头:{'Content-Type': 'application/json'}add-mappings=false 连欢迎页一起关。
GET /%2e%2e/hello.txt → 400 '<!doctype html><html lang="en"><head><ti'
响应头:{'Content-Type': 'text/html;charset=utf-8', 'Content-Length': '435'}
GET /css/../hello.txt → 404 '{"timestamp":"2026-10-19T03:51:57.054Z",'
响应头:{'Content-Type': 'application/json'}
GET /css/%2e%2e/hello.txt → 404 '{"timestamp":"2026-10-19T03:51:57.070Z",'
响应头:{'Content-Type': 'application/json'}
GET /css/..%2fhello.txt → 400 '<!doctype html><html lang="en"><head><ti'
响应头:{'Content-Type': 'text/html;charset=utf-8', 'Content-Length': '435'}
GET /..%2f..%2fetc/passwd → 400 '<!doctype html><html lang="en"><head><ti'
响应头:{'Content-Type': 'text/html;charset=utf-8', 'Content-Length': '435'}
GET /%2e%2e%2f%2e%2e%2fetc/passwd → 400 '<!doctype html><html lang="en"><head><ti'
响应头:{'Content-Type': 'text/html;charset=utf-8', 'Content-Length': '435'}
GET /css/%5c..%5chello.txt → 400 '<!doctype html><html lang="en"><head><ti'
响应头:{'Content-Type': 'text/html;charset=utf-8', 'Content-Length': '435'}试了的 7 种写法都没读到目录外的文件。