Spring Web HTTP缓存支持系列3: 对 Cache-Control头部的支持

105 篇文章 49 订阅
65 篇文章 6 订阅

为了支持HTTP缓存机制Cache-Control头部,Spring Web提供了如下支持 :

  • 使用CacheControl概念建模Cache-Control
  • 针对控制器方法的Cache-Control响应头部设置
  • 针对静态资源的Cache-Control响应头部设置

CacheControl

CacheControl其实是一个链式构建器(builder),它接收使用者的各种参数,用于最终生成一个Cache-Control响应头部的值字符串。
CacheControl主要功能是 :

  1. 每个对象维护了一组和Cache-Control头部指令对应的属性,用于接收使用者的设置;
  2. 类提供了一组总是返回自身(this)的静态方法用于链式构建最终的Cache-Control头部指令值;
  3. 在链式构建之后,最终在实例上调用#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定义的方法applyCacheSecondsprepareResponse,它们的实现如下:

// 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最终用来将静态资源写入到响应。ResourceHttpRequestHandlerRequestMappingHandlerAdapter一样,也继承自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 BootSpring 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支持验证器机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值