Spring Web HTTP缓存支持系列4: WebRequest#checkNotModified支持验证器机制

本文深入探讨SpringWeb框架如何支持HTTP缓存机制中的强/弱验证器,具体讲解了WebRequest#checkNotModified方法的使用及其实现原理,包括Etag/If-None-Match和Last-Modified/If-Modified-Since头部的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为了支持HTTP缓存机制强/弱验证器机制,Spring Web提供了WebRequest#checkNotModified方法供控制器方法使用。

典型用法举例如下 :

    @RequestMapping(value = "/document", method = RequestMethod.GET)
    public String getDocument(ServletWebRequest webRequest, Model model, 
                    @RequestParam(value = "documentId", required = true) long documentId) {
    
        // 1. 从文档服务中查找文档对象
        MyDocument document = documentService.findById(documentId);

        // HTTP 缓存处理
        // 2. 根据所找到的文档对象的最后一次更新时间, 以及请求对象 webRequest 中
        // 可能存在的HTTP缓存有关头部 If-Modified-Since 判断请求者
        // 缓存的文档是否过期
        final long lastUpdate = document.getLastUpdateTimestamp();
        final boolean notModified = webRequest.checkNotModified(lastUpdate);
        if (notModified) {// 服务器端文档未被更新过,请求者可以继续使用本地缓存的文档副本
            // 返回 null 声明请求已被处理完成,通常是响应被设置了HTTP状态字 304 
            return null;
        }

        // 3. 请求者首次请求该文档或者非首次请求+服务器端文档被更新过,现在需要结合
        // 文档对象和模板视图构造最后返回给用户的响应对象
        
        model.addAttribute("title", document.getTitle());
        model.addAttribute("content", document.getContent());
		// ... 
      
      
        return "document_view";
    }

该例子描述了这样一种场景 :

  1. 系统会动态维持一组文档对象,这组文档对象有可能会被修改;
  2. 系统用户访问某个文档对象,对应的文档会通过某个模板视图渲染给用户;
  3. 文档对象的更改并不会很频繁,如果用户已经获取了某个文档的页面,在其再次发生改变之前,服务器没有必要为该请求者再次渲染该文档;

在上面的例子中,webRequest.checkNotModified(lastUpdate)执行了检测文档是否发生改变这一关键逻辑,lastUpdate是服务器端文档对象的最近一次更改时间,而不是基于文档对象的ETag,所以这是Spring Web支持HTTP缓存机制弱验证器的一个例子。接下来我们分析一下WebRequest#checkNotModified的源代码。

源代码

源代码版本 : Spring Security Config 5.1.4.RELEASE

package org.springframework.web.context.request;

import java.security.Principal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.WebUtils;

/**
 * WebRequest adapter for an javax.servlet.http.HttpServletRequest.
 *
 * @author Juergen Hoeller
 * @author Brian Clozel
 * @author Markus Malkusch
 * @since 2.0
 */
public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest {

	private static final String ETAG = "ETag";

	private static final String IF_MODIFIED_SINCE = "If-Modified-Since";

	private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";

	private static final String IF_NONE_MATCH = "If-None-Match";

	private static final String LAST_MODIFIED = "Last-Modified";

	private static final List<String> SAFE_METHODS = Arrays.asList("GET", "HEAD");

	/**
	 * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
	 * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
	 */
	private static final Pattern ETAG_HEADER_VALUE_PATTERN = 
		Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");

	/**
	 * Date formats as specified in the HTTP RFC.
	 * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">
	 * Section 7.1.1.1 of RFC 7231</a>
	 */
	private static final String[] DATE_FORMATS = new String[] {
			"EEE, dd MMM yyyy HH:mm:ss zzz",
			"EEE, dd-MMM-yy HH:mm:ss zzz",
			"EEE MMM dd HH:mm:ss yyyy"
	};

	private static final TimeZone GMT = TimeZone.getTimeZone("GMT");

	private boolean notModified = false;


	/**
	 * Create a new ServletWebRequest instance for the given request.
	 * @param request current HTTP request
	 */
	public ServletWebRequest(HttpServletRequest request) {
		super(request);
	}

	/**
	 * Create a new ServletWebRequest instance for the given request/response pair.
	 * @param request current HTTP request
	 * @param response current HTTP response (for automatic last-modified handling)
	 */
	public ServletWebRequest(HttpServletRequest request, @Nullable HttpServletResponse response) {
		super(request, response);
	}


	@Override
	public Object getNativeRequest() {
		return getRequest();
	}

	@Override
	public Object getNativeResponse() {
		return getResponse();
	}

	@Override
	public <T> T getNativeRequest(@Nullable Class<T> requiredType) {
		return WebUtils.getNativeRequest(getRequest(), requiredType);
	}

	@Override
	public <T> T getNativeResponse(@Nullable Class<T> requiredType) {
		HttpServletResponse response = getResponse();
		return (response != null ? WebUtils.getNativeResponse(response, requiredType) : null);
	}

	/**
	 * Return the HTTP method of the request.
	 * @since 4.0.2
	 */
	@Nullable
	public HttpMethod getHttpMethod() {
		return HttpMethod.resolve(getRequest().getMethod());
	}

	@Override
	@Nullable
	public String getHeader(String headerName) {
		return getRequest().getHeader(headerName);
	}

	@Override
	@Nullable
	public String[] getHeaderValues(String headerName) {
		String[] headerValues = StringUtils.toStringArray(getRequest().getHeaders(headerName));
		return (!ObjectUtils.isEmpty(headerValues) ? headerValues : null);
	}

	@Override
	public Iterator<String> getHeaderNames() {
		return CollectionUtils.toIterator(getRequest().getHeaderNames());
	}

	@Override
	@Nullable
	public String getParameter(String paramName) {
		return getRequest().getParameter(paramName);
	}

	@Override
	@Nullable
	public String[] getParameterValues(String paramName) {
		return getRequest().getParameterValues(paramName);
	}

	@Override
	public Iterator<String> getParameterNames() {
		return CollectionUtils.toIterator(getRequest().getParameterNames());
	}

	@Override
	public Map<String, String[]> getParameterMap() {
		return getRequest().getParameterMap();
	}

	@Override
	public Locale getLocale() {
		return getRequest().getLocale();
	}

	@Override
	public String getContextPath() {
		return getRequest().getContextPath();
	}

	@Override
	@Nullable
	public String getRemoteUser() {
		return getRequest().getRemoteUser();
	}

	@Override
	@Nullable
	public Principal getUserPrincipal() {
		return getRequest().getUserPrincipal();
	}

	@Override
	public boolean isUserInRole(String role) {
		return getRequest().isUserInRole(role);
	}

	@Override
	public boolean isSecure() {
		return getRequest().isSecure();
	}


    // 仅使用 Last-Modified/If-Modified-Since 头部的HTTP缓存弱验证器机制
	@Override
	public boolean checkNotModified(long lastModifiedTimestamp) {
        // 设置 ETag 为 null,表明不使用强验证器机制
		return checkNotModified(null, lastModifiedTimestamp);
	}

    // 仅使用 ETag/If-None-Match 头部的HTTP缓存强验证器机制
	@Override
	public boolean checkNotModified(String etag) {
        // 设置 lastModifiedTimestamp 为 -1, 表示不使用弱验证器机制
		return checkNotModified(etag, -1);
	}

	@Override
	public boolean checkNotModified(@Nullable String etag, long lastModifiedTimestamp) {
		HttpServletResponse response = getResponse();
		if (this.notModified || (response != null && HttpStatus.OK.value() != response.getStatus())) {
			return this.notModified;
		}

		// Evaluate conditions in order of precedence.
		// See https://tools.ietf.org/html/rfc7232#section-6

		if (validateIfUnmodifiedSince(lastModifiedTimestamp)) {
        // 如果检测到请求头部 If-Unmodified-Since 并结合 lastModifiedTimestamp(>0)使用,
        // 并且此过程后如果 this.notModified 被设置为 true, 则此时需要设置响应状态字 412
			if (this.notModified && response != null) {
				response.setStatus(HttpStatus.PRECONDITION_FAILED.value());
			}
			return this.notModified;
		}

       // 首先尝试检测是否是 Etag/If-None-Match 强验证器机制并尝试应用,
        //否则再检测是否是 Last-Modified/If-Modified-Since 弱验证器机制并尝试应用
		boolean validated = validateIfNoneMatch(etag);
		if (!validated) {
            // 不是强验证器机制,再检测是否是 Last-Modified/If-Modified-Since 弱验证器机制并尝试应用
			validateIfModifiedSince(lastModifiedTimestamp);
		}

		// Update response , 更新响应对象
		if (response != null) {
			boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());
			if (this.notModified) {
				// 如果 this.notModified 为 true, 则根据请求HTTP方法决定响应 304 还是 412
				response.setStatus(isHttpGetOrHead ?
						HttpStatus.NOT_MODIFIED.value() : HttpStatus.PRECONDITION_FAILED.value());
			}            
			if (isHttpGetOrHead) {
				// this.notModified 为 false,并且 HTTP 请求方法为 GET 或者 HEAD,
				// 此时设置响应头部 Last-Modified, 或者 ETag
				if (lastModifiedTimestamp > 0 
					&& parseDateValue(response.getHeader(LAST_MODIFIED)) == -1) {
					response.setDateHeader(LAST_MODIFIED, lastModifiedTimestamp);
				}
				if (StringUtils.hasLength(etag) && response.getHeader(ETAG) == null) {
					response.setHeader(ETAG, padEtagIfNecessary(etag));
				}
			}
		}
		// 从上过程可以看出,this.notModified 为 true 的话,响应状态字会被设置,
		// this.notModified 为 false 的话,响应中可能会根据信息设置 头部 Last-Modified, 或者 ETag
		return this.notModified;
	}

	// 返回 false 表示没有检测到请求头部 If-Unmodified-Since 或者  lastModifiedTimestamp<0,   
	// 返回 true 表示检测到请求头部 If-Unmodified-Since 并结合 lastModifiedTimestamp(>0)使用,此时:
	// 如果文档被修改 ,则 this.notModified 被设置为 true
	// 如果文档未被修改,则 notModified 被设置为 false
	private boolean validateIfUnmodifiedSince(long lastModifiedTimestamp) {
		if (lastModifiedTimestamp < 0) {
            // 文档最近一次更新时间为 -1, 返回 false
			return false;
		}
		// 获取日期类型请求头部 If-Unmodified-Since : 
		// 告知服务器,指定的请求资源只有在字段值内指定的时间之后未发生更新的情况下才能处理请求。
		// 如果在指定日期时间后发生更新,则以状态码 412 Precondition Failed 作为响应返回
		long ifUnmodifiedSince = parseDateHeader(IF_UNMODIFIED_SINCE);
		if (ifUnmodifiedSince == -1) {
			// 请求头部不带 If-Unmodified-Since , 返回 false
			return false;
		}
		// We will perform this validation...
		// 如果 If-Unmodified-Since(一个时间点,此时间点之后文档未修改过) 之后文档被修改过,
		// 则将 this.notModified 设置为 true, 否则将 this.notModified 设置为 false
		this.notModified = (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000));
        
        // 返回 true 说明检测到头部 If-Unmodified-Since,要应用头部 If-Unmodified-Since         
		return true;
	}

    // 对 HTTP 缓存强验证器机制的支持,参数 etag 是服务端数据实体的唯一标识,
    // 如果服务端数据实体发生变化的话,其 etag 也会发生变化,该方法获取请求头部 If-None-Match,
    // 它记录了客户端首次访问该地址所获取的服务端数据实体的 etag,该方法通过比较这两个 etag
    // 确定客户端的缓存副本和服务器端数据实体当前版本是否一致
    // 该方法返回 true 表明 Etag/If-None-Match 强验证器机制被应用,
    // 不需要再执行 Last-Modified/If-Modified-Since 弱验证器机制
	private boolean validateIfNoneMatch(@Nullable String etag) {
		if (!StringUtils.hasLength(etag)) {
			return false;
		}

		Enumeration<String> ifNoneMatch;
		try {
			ifNoneMatch = getRequest().getHeaders(IF_NONE_MATCH);
		}
		catch (IllegalArgumentException ex) {
			return false;
		}
		if (!ifNoneMatch.hasMoreElements()) {
			return false;
		}

		// We will perform this validation...
		etag = padEtagIfNecessary(etag);
		if (etag.startsWith("W/")) {
			etag = etag.substring(2);
		}
		while (ifNoneMatch.hasMoreElements()) {
			String clientETags = ifNoneMatch.nextElement();
			Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags);
			// Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
			while (etagMatcher.find()) {
				if (StringUtils.hasLength(etagMatcher.group()) && etag.equals(etagMatcher.group(3))) {
					this.notModified = true;
					break;
				}
			}
		}

		return true;
	}

	private String padEtagIfNecessary(String etag) {
		if (!StringUtils.hasLength(etag)) {
			return etag;
		}
		if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) {
			return etag;
		}
		return "\"" + etag + "\"";
	}

	private boolean validateIfModifiedSince(long lastModifiedTimestamp) {
		if (lastModifiedTimestamp < 0) {
			// 如果 lastModifiedTimestamp 为 -1, 则直接返回 false,表示不验证头部 If-Modified-Since
			return false;
		}
        // 获得日期时间类型头部 If-Modified-Since 值
		long ifModifiedSince = parseDateHeader(IF_MODIFIED_SINCE);
		if (ifModifiedSince == -1) {
			// 如果没有检测到头部 If-Modified-Since,则直接返回 false,表示不验证头部 If-Modified-Since
			return false;
		}
		// We will perform this validation...
		// 如果 If-Modified-Since(一个时间点,用于检查此时间点之后文档是否被修改过) 之后文档被修改过,
		// 则将 this.notModified 设置为 false, 否则将 this.notModified 设置为 true        
		this.notModified = ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000);
        
		// 返回 true 说明检测到头部 If-Modified-Since,要应用头部 If-Modified-Since 
		return true;
	}

	public boolean isNotModified() {
		return this.notModified;
	}

    // 工具方法,从请求中获取指定名称的日期时间类型的头部值
	private long parseDateHeader(String headerName) {
		long dateValue = -1;
		try {
			dateValue = getRequest().getDateHeader(headerName);
		}
		catch (IllegalArgumentException ex) {
			String headerValue = getHeader(headerName);
			// Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774"
			if (headerValue != null) {
				int separatorIndex = headerValue.indexOf(';');
				if (separatorIndex != -1) {
					String datePart = headerValue.substring(0, separatorIndex);
					dateValue = parseDateValue(datePart);
				}
			}
		}
		return dateValue;
	}

	private long parseDateValue(@Nullable String headerValue) {
		if (headerValue == null) {
			// No header value sent at all
			return -1;
		}
		if (headerValue.length() >= 3) {
			// Short "0" or "-1" like values are never valid HTTP date headers...
			// Let's only bother with SimpleDateFormat parsing for long enough values.
			for (String dateFormat : DATE_FORMATS) {
				SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US);
				simpleDateFormat.setTimeZone(GMT);
				try {
					return simpleDateFormat.parse(headerValue).getTime();
				}
				catch (ParseException ex) {
					// ignore
				}
			}
		}
		return -1;
	}

	@Override
	public String getDescription(boolean includeClientInfo) {
		HttpServletRequest request = getRequest();
		StringBuilder sb = new StringBuilder();
		sb.append("uri=").append(request.getRequestURI());
		if (includeClientInfo) {
			String client = request.getRemoteAddr();
			if (StringUtils.hasLength(client)) {
				sb.append(";client=").append(client);
			}
			HttpSession session = request.getSession(false);
			if (session != null) {
				sb.append(";session=").append(session.getId());
			}
			String user = request.getRemoteUser();
			if (StringUtils.hasLength(user)) {
				sb.append(";user=").append(user);
			}
		}
		return sb.toString();
	}


	@Override
	public String toString() {
		return "ServletWebRequest: " + getDescription(true);
	}

}

相关文章

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、付费专栏及课程。

余额充值