SpringMVC 之 ShallowEtagHeaderFilter 源码分析

HTTP 的 Last-Modified 和 Etag 头能够告诉浏览器如何使用缓存,这有助于提高网站的打开效率。

Etag 机制

Etag 是 HTTP1.1 新加的功能。它由服务端生成,由一串字符串标识资源的内容的唯一值,类似 MD5 值。

当第一次发送 HTTP 请求时,服务端生成 Etag 值,并通过 Etag 头返回。

当第二次再次请求时,客户端会将这个值作为 If-None-Match,提交到服务端。

服务端接收到浏览器提交的 If-None-Match 后,比对 Etag 是否一致,如果一致,则返回 304,浏览器接收到后则认为资源没有被修改,可以使用浏览器缓存的内容。如果不一致,服务端返回新的内容并同时返回新的 Etag。

Last-Modified 与 Etag 有类型的功能,都是用于浏览器缓存。只不过 Last-Modified 表示资源最后修改时间。有些场景下使用 Last-Modified 还是不够的,比如:

  • 文件会定期更新,但内容不会变化。
  • 有些文件不能确定变更频率。

ShallowEtagHeaderFilter 使用

SpringMVC 提供 ShallowEtagHeaderFilter 类用于服务端支持 Etag 特性。

ShallowEtagHeaderFilter 类其实也是一个 Filter,所以配置时也只需像普通的 Filter 一样配置:

1
2
3
4
5
6
7
8
9
<!-- web.xml 文件 -->
<filter>
<filter-name>etagFilter</filter-name>
<filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>etagFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

举一个简单的示例:

新建一个 JavaWEB 项目,该项目只有一个 test.jsp 文件:

项目启动后,第一次访问:http://127.0.0.1:8080/test.jsp,服务端返回 200。

以及 Etag 值:

当再次刷新页面后,服务端返回 304:

可以看到这次请求,浏览器在请求头中带上了刚才的 Etag 值:

ShallowEtagHeaderFilter 源码

ShallowEtagHeaderFilter 继承自 OncePerRequestFilter,当调用到 OncePerRequestFilter 的 doFilter 方法时,OncePerRequestFilter 会调用子类的 ShallowEtagHeaderFilter.doFilterInternal 方法。

内部继续调用到 updateResponse 方法,可以看出,通过调用 generateETagHeaderValue 方法生成 Etag 值。并比较 request 中传入的 Etag 值。当发现 Etag 值相符时,返回 304 状态码,并不返回响应体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void updateResponse(HttpServletRequest request, HttpServletResponse response) throws IOException {
ShallowEtagResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ShallowEtagResponseWrapper.class);
Assert.notNull(responseWrapper, "ShallowEtagResponseWrapper not found");

HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse();
int statusCode = responseWrapper.getStatusCode();
byte[] body = responseWrapper.toByteArray();

if (isEligibleForEtag(request, responseWrapper, statusCode, body)) {
String responseETag = generateETagHeaderValue(body);
rawResponse.setHeader(HEADER_ETAG, responseETag);
String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);
if (responseETag.equals(requestETag)) {
if (logger.isTraceEnabled()) {
logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304");
}
rawResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); // 返回 304,并不用返回 response 包体。
}
else {
if (logger.isTraceEnabled()) {
logger.trace("ETag [" + responseETag + "] not equal to If-None-Match [" + requestETag + "], sending normal response");
}
copyBodyToResponse(body, rawResponse);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Response with status code [" + statusCode + "] not eligible for ETag");
}
copyBodyToResponse(body, rawResponse);
}
}