什么是“ETag”?
HTTP协议规格说明定义ETag为“被请求变量的实体值” (参见 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html —— 章节 14.19)。 另一种说法是,ETag是一个可以与Web资源关联的记号(token)。典型的Web资源可以一个Web页,但也可能是JSON或XML文档。服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端。
如果http 请求头 If-None-Match 的内容,与服务器对资源算出来的 etag 相同,就返回 304 响应。
下面来动动手,实现一个 etag 过虑器。原理:用 HttpServletResponseWrapper 把正常的页面输出到一个 byte 数组里,然后计算 etag,etag 是否与请求头一致,再进一步处理。
代码实现:
- package com.chenlb.http;
- import java.io.ByteArrayOutputStream;
- import java.io.IOException;
- import java.io.PrintWriter;
- import java.util.Calendar;
- import java.util.Date;
- import java.util.zip.CRC32;
- import javax.servlet.Filter;
- import javax.servlet.FilterChain;
- import javax.servlet.FilterConfig;
- import javax.servlet.ServletException;
- import javax.servlet.ServletOutputStream;
- import javax.servlet.ServletRequest;
- import javax.servlet.ServletResponse;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import javax.servlet.http.HttpServletResponseWrapper;
- public class EtagFilter implements Filter {
- public void destroy() {}
- public void doFilter(ServletRequest request, ServletResponse response,
- FilterChain chain) throws IOException, ServletException {
- HttpServletRequest servletRequest = (HttpServletRequest) request;
- HttpServletResponse servletResponse = (HttpServletResponse) response;
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- HttpServletResponseWrapper hsrw = new MyHttpResponseWrapper(servletResponse, baos);
- chain.doFilter(request, hsrw);
- hsrw.flushBuffer();
- byte[] bytes = baos.toByteArray();
- CRC32 crc = new CRC32();
- crc.update(bytes);
- String token = "w/\"" + crc.getValue() + '"';
- servletResponse.setHeader("ETag", token);
- // always store the ETag in the header
- String previousToken = servletRequest.getHeader("If-None-Match");
- if (previousToken != null && previousToken.equals(token)) {
- // compare previous token with current one
- System.out.println("ETag match: returning 304 Not Modified");
- servletResponse.sendError(HttpServletResponse.SC_NOT_MODIFIED);
- // use the same date we sent when we created the ETag the first time through
- servletResponse.setHeader("Last-Modified", servletRequest.getHeader("If-Modified-Since"));
- } else {
- // first time through - set last modified time to now
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.MILLISECOND, 0);
- Date lastModified = cal.getTime();
- servletResponse.setDateHeader("Last-Modified", lastModified.getTime());
- System.out.println("Writing body content");
- servletResponse.setContentLength(bytes.length);
- ServletOutputStream sos = servletResponse.getOutputStream();
- sos.write(bytes);
- sos.flush();
- sos.close();
- }
- }
- public void init(FilterConfig config) throws ServletException {}
- private static class MyHttpResponseWrapper extends HttpServletResponseWrapper {
- ByteServletOutputStream servletOutputStream;
- PrintWriter printWriter;
- public MyHttpResponseWrapper(HttpServletResponse response, ByteArrayOutputStream buffer) {
- super(response);
- servletOutputStream = new ByteServletOutputStream(buffer);
- }
- public ServletOutputStream getOutputStream() throws IOException {
- return servletOutputStream;
- }
- public PrintWriter getWriter() throws IOException {
- if(printWriter == null) {
- printWriter = new PrintWriter(servletOutputStream);
- }
- return printWriter;
- }
- public void flushBuffer() throws IOException {
- servletOutputStream.flush();
- if(printWriter != null) {
- printWriter.flush();
- }
- }
- }
- private static class ByteServletOutputStream extends ServletOutputStream {
- ByteArrayOutputStream baos;
- public ByteServletOutputStream(ByteArrayOutputStream baos) {
- super();
- this.baos = baos;
- }
- public void write(int b) throws IOException {
- baos.write(b);
- }
- }
- }
web.xml 配置:
- <filter>
- <filter-name>etag</filter-name>
- <filter-class>com.chenlb.http.EtagFilter</filter-class>
- </filter>
- <filter-mapping>
- <filter-name>etag</filter-name>
- <url-pattern>*.jsp</url-pattern>
- </filter-mapping>
测试环境是 tomcat 6.0.18。
用 httpwatch 可以观察效果。
第二次请求(刷新),返回 304 。说明有效了。
过虑器同时还加了 Last-Modified 是为了兼容不支持 Etag 头的客户端。
infoq 下载来的代码没试用通过,原因是没有 flush PrintWriter。虽然有 304,但返回的内容为空。
当然算 etag 可用其它算法,我这里用 crc32。infoq 例子中用 md5。