为了支持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";
}
该例子描述了这样一种场景 :
- 系统会动态维持一组文档对象,这组文档对象有可能会被修改;
- 系统用户访问某个文档对象,对应的文档会通过某个模板视图渲染给用户;
- 文档对象的更改并不会很频繁,如果用户已经获取了某个文档的页面,在其再次发生改变之前,服务器没有必要为该请求者再次渲染该文档;
在上面的例子中,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支持验证器机制