为了支持HTTP
缓存机制Cache-Control
头部,Spring Web
提供了如下支持 :
- 使用
CacheControl
概念建模Cache-Control
- 针对控制器方法的
Cache-Control
响应头部设置 - 针对静态资源的
Cache-Control
响应头部设置
CacheControl
类CacheControl
其实是一个链式构建器(builder
),它接收使用者的各种参数,用于最终生成一个Cache-Control
响应头部的值字符串。
CacheControl
主要功能是 :
- 每个对象维护了一组和
Cache-Control
头部指令对应的属性,用于接收使用者的设置; - 类提供了一组总是返回自身(
this
)的静态方法用于链式构建最终的Cache-Control
头部指令值; - 在链式构建之后,最终在实例上调用
#getHeaderValue
生成最终的Cache-Control
头部指令值;
其典型用法如下例子所示 :
CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).noTransform().cachePublic()
String headerValue = cc.getHeaderValue();
// 这里 headerValue ,也就是该构建器最终生成的头部的值会是 "max-age=3600, no-transform, public"
针对控制器方法的Cache-Control
响应头部设置
Spring Web
处理一个请求时,最终通过RequestMappingHandlerAdapter
调用控制器方法,其中涉及到了对Cache-Control
响应头部的设置,有关代码如下所示 :
// 类 RequestMappingHandlerAdapter 代码片段
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
// 对目标控制器方法的调用
// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}
// 如果响应中头部 Cache-Control 头部尚未设置,则
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
// 使用了 @SessionAttributes 的情况
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
// 准备相应对象,主要是根据设置向响应对象设置跟缓存有关的头部
prepareResponse(response);
}
}
return mav;
}
上面方法使用到了基类WebContentGenerator
定义的方法applyCacheSeconds
和prepareResponse
,它们的实现如下:
// WebContentGenerator 类代码片段
/**
* Prepare the given response according to the settings of this generator.
* Applies the number of cache seconds specified for this generator.
* @param response current HTTP response
* @since 4.2
*/
protected final void prepareResponse(HttpServletResponse response) {
// 如果属性 cacheControl 被设置,优先使用 applyCacheControl,
// 否则使用 applyCacheSeconds
if (this.cacheControl != null) {
applyCacheControl(response, this.cacheControl);
}
else {
applyCacheSeconds(response, this.cacheSeconds);
}
// 如果属性 varyByRequestHeaders 被设置, 向响应对象中设置 Vary 头部
if (this.varyByRequestHeaders != null) {
for (String value : getVaryRequestHeadersToAdd(response, this.varyByRequestHeaders)) {
response.addHeader("Vary", value);
}
}
}
/**
* Apply the given cache seconds and generate corresponding HTTP headers,
* i.e. allow caching for the given number of seconds in case of a positive
* value, prevent caching if given a 0 value, do nothing else.
* Does not tell the browser to revalidate the resource.
* @param response current HTTP response
* @param cacheSeconds positive number of seconds into the future that the
* response should be cacheable for, 0 to prevent caching
*/
@SuppressWarnings("deprecation")
protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds) {
if (this.useExpiresHeader || !this.useCacheControlHeader) {
// Deprecated HTTP 1.0 cache behavior, as in previous Spring versions
// 被要求使用 Expires 头部或者没有被要求使用 Cache-Control 头部的情况
if (cacheSeconds > 0) {
cacheForSeconds(response, cacheSeconds);
}
else if (cacheSeconds == 0) {
preventCaching(response);
}
}
else {
CacheControl cControl;
if (cacheSeconds > 0) {
cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS);
if (this.alwaysMustRevalidate) {
cControl = cControl.mustRevalidate();
}
}
else if (cacheSeconds == 0) {
cControl = (this.useCacheControlNoStore ? CacheControl.noStore() : CacheControl.noCache());
}
else {
cControl = CacheControl.empty();
}
applyCacheControl(response, cControl);
}
}
/**
* Set the HTTP Cache-Control header according to the given settings.
* @param response current HTTP response
* @param cacheControl the pre-configured cache control settings
* @since 4.2
*/
protected final void applyCacheControl(HttpServletResponse response, CacheControl cacheControl) {
String ccValue = cacheControl.getHeaderValue();
// 如果 cacheControl 生成的 Cache-Control 头部值 ccValue 不为 null,则将其设置到响应对象,
// 同时如果响应头部被设置了 Pragma 或者 Expires , 将它们都设置为空,该逻辑表明如果这三种头部
// 中优先使用的是 Cache-Control
if (ccValue != null) {
// Set computed HTTP 1.1 Cache-Control header
response.setHeader(HEADER_CACHE_CONTROL, ccValue);
if (response.containsHeader(HEADER_PRAGMA)) {
// Reset HTTP 1.0 Pragma header if present
response.setHeader(HEADER_PRAGMA, "");
}
if (response.containsHeader(HEADER_EXPIRES)) {
// Reset HTTP 1.0 Expires header if present
response.setHeader(HEADER_EXPIRES, "");
}
}
}
/**
* Set HTTP headers to allow caching for the given number of seconds.
* Does not tell the browser to revalidate the resource.
* @param response current HTTP response
* @param seconds number of seconds into the future that the response
* should be cacheable for
* @deprecated as of 4.2, in favor of #applyCacheControl
*/
@Deprecated
protected final void cacheForSeconds(HttpServletResponse response, int seconds) {
cacheForSeconds(response, seconds, false);
}
/**
* Set HTTP headers to allow caching for the given number of seconds.
* Tells the browser to revalidate the resource if mustRevalidate is
* true.
* @param response the current HTTP response
* @param seconds number of seconds into the future that the response
* should be cacheable for
* @param mustRevalidate whether the client should revalidate the resource
* (typically only necessary for controllers with last-modified support)
* @deprecated as of 4.2, in favor of #applyCacheControl
* Spring 4.2 之后该方法已经被放弃,建议使用 #applyCacheControl,
* #applyCacheControl 使用了 CacheControl 构建器构造 Cache-Control 头部值,
* 而该方法 #cacheForSeconds 则是在自己的逻辑中根据配置构造各种参数的值
*/
@Deprecated
protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
if (this.useExpiresHeader) {
// HTTP 1.0 header Expires 被强制要求使用的情况
response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L);
}
else if (response.containsHeader(HEADER_EXPIRES)) {
// HTTP 1.0 header Expires 没有被要求使用的情况
// Reset HTTP 1.0 Expires header if present
// 重置 HTTP 1.0 Expires 头部(如果它存在于响应中的话)
response.setHeader(HEADER_EXPIRES, "");
}
if (this.useCacheControlHeader) {
// HTTP 1.1 header Cache-Control 被要求使用的情况
String headerValue = "max-age=" + seconds;
if (mustRevalidate || this.alwaysMustRevalidate) {
headerValue += ", must-revalidate";
}
response.setHeader(HEADER_CACHE_CONTROL, headerValue);
}
if (response.containsHeader(HEADER_PRAGMA)) {
// Reset HTTP 1.0 Pragma header if present
// 重置 HTTP 1.0 Pragma 头部(如果它存在于响应中的话)
response.setHeader(HEADER_PRAGMA, "");
}
}
/**
* Prevent the response from being cached. 阻止响应中的资源被缓存
* Only called in HTTP 1.0 compatibility mode.
* See http://www.mnot.net/cache_docs.
* @deprecated as of 4.2, in favor of #applyCacheControl
*/
@Deprecated
protected final void preventCaching(HttpServletResponse response) {
response.setHeader(HEADER_PRAGMA, "no-cache");
if (this.useExpiresHeader) {
// HTTP 1.0 Expires header
response.setDateHeader(HEADER_EXPIRES, 1L);
}
if (this.useCacheControlHeader) {
// HTTP 1.1 Cache-Control header: "no-cache" is the standard value,
// "no-store" is necessary to prevent caching on Firefox.
response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
if (this.useCacheControlNoStore) {
response.addHeader(HEADER_CACHE_CONTROL, "no-store");
}
}
}
private Collection<String> getVaryRequestHeadersToAdd(HttpServletResponse response,
String[] varyByRequestHeaders) {
if (!response.containsHeader(HttpHeaders.VARY)) {
return Arrays.asList(varyByRequestHeaders);
}
Collection<String> result = new ArrayList<>(varyByRequestHeaders.length);
Collections.addAll(result, varyByRequestHeaders);
for (String header : response.getHeaders(HttpHeaders.VARY)) {
for (String existing : StringUtils.tokenizeToStringArray(header, ",")) {
if ("*".equals(existing)) {
return Collections.emptyList();
}
for (String value : varyByRequestHeaders) {
if (value.equalsIgnoreCase(existing)) {
result.remove(value);
}
}
}
}
return result;
}
上面的各个方法最终会被RequestMappingHandlerAdapter#handleInternal
用于设置响应的Cache-Control
头部,以及相关的一些头部,比如Expires,Progma,Varying
。而影响该头部设置的主要属性初始化如下 :
// RequestMappingHandlerAdapter 代码片段
private int cacheSecondsForSessionAttributeHandlers = 0;
// RequestMappingHandlerAdapter 基类 WebContentGenerator 代码片段
@Nullable
private CacheControl cacheControl;
private int cacheSeconds = -1;
@Nullable
private String[] varyByRequestHeaders;
// deprecated fields
/** Use HTTP 1.0 expires header? */
private boolean useExpiresHeader = false;
/** Use HTTP 1.1 cache-control header? */
private boolean useCacheControlHeader = true;
/** Use HTTP 1.1 cache-control header value "no-store"? */
private boolean useCacheControlNoStore = true;
private boolean alwaysMustRevalidate = false;
除非开发人员提供设置响应Cache-Control
头部的逻辑,否则结合以上初始化参数,并没有Cache-Control
头部会被写入响应对象。
针对静态资源的Cache-Control
响应头部设置
Spring Web
中静态资源是通过ResourceHandlerRegistration
注册进来的。而ResourceHandlerRegistration
有两个参数用来控制Cache-Control
响应头部设置 :
@Nullable
private Integer cachePeriod;
@Nullable
private CacheControl cacheControl;
最终这两个属性会设置到相应的静态资源处理器ResourceHttpRequestHandler
实例上,而ResourceHttpRequestHandler
最终用来将静态资源写入到响应。ResourceHttpRequestHandler
跟RequestMappingHandlerAdapter
一样,也继承自WebContentGenerator
。在ResourceHttpRequestHandler
将资源写入响应时,使用了同样的缓存控制头部处理机制 :
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// For very general mappings (e.g. "/") we need to check 404 first
Resource resource = getResource(request);
if (resource == null) {
logger.debug("Resource not found");
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
response.setHeader("Allow", getAllowHeader());
return;
}
// Supported methods and required session
checkRequest(request);
// Header phase
if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified");
return;
}
// Apply cache settings, if any
// 注意这里 : 准备响应,其实就是应用 cache 有关头部设置,该方法实现在基类 WebContentGenerator
// 中,ResourceHttpRequestHandler 对 cache 有关头部的设置使用的也是该方法
prepareResponse(response);
// Check the media type for the resource
MediaType mediaType = getMediaType(request, resource);
// Content phase
if (METHOD_HEAD.equals(request.getMethod())) {
setHeaders(response, resource, mediaType);
return;
}
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if (request.getHeader(HttpHeaders.RANGE) == null) {
Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
setHeaders(response, resource, mediaType);
// 将静态资源写入响应
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
}
else {
Assert.state(this.resourceRegionHttpMessageConverter != null, "Not initialized");
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request);
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
// 将静态资源写入响应
this.resourceRegionHttpMessageConverter.write(
HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage);
}
catch (IllegalArgumentException ex) {
response.setHeader("Content-Range", "bytes */" + resource.contentLength());
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
}
}
}
如果不做特殊设置,ResourceHttpRequestHandler
也会使用基类WebContentGenerator
中有关缓存头部的属性初始值,所以同样道理,缺省情况下,对于静态资源,响应头部的Cache-Control
也不会应用。不过跟控制器方法不同的时,通常静态资源映射是通过ResourceHandlerRegistration
注册进来的,而通过ResourceHandlerRegistration
,开发人员可以很方便地配置静态资源的Cache-Control
。比如对于基于Spring Boot
的Spring MVC
应用,就是在自动配置中加载配置文件中关于静态文件的配置,然后设置到ResourceHandlerRegistration
上,从而影响相应静态资源响应的缓存头部设置。代码如下所示 :
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
// 从配置文件中加载缓存有关配置参数
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache()
.getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
// 对 webjars 静态资源的注册,注意缓存有关的设置
customizeResourceHandlerRegistration(registry
.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
// 对 spring boot mvc 缺省静态资源的注册,注意缓存有关的设置
// "classpath:/META-INF/resources/", "classpath:/resources/",
// "classpath:/static/", "classpath:/public/"
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(
this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
}
相关文章
Spring Web HTTP缓存支持系列1: HTTP缓存机制简介
Spring Web HTTP缓存支持系列2: 支持概述
Spring Web HTTP缓存支持系列3: 对 Cache-Control头部的支持
Spring Web HTTP缓存支持系列4: WebRequest#checkNotModified支持验证器机制