原文出处:[url]http://blog.chenlb.com/2009/07/use-httpservletresponsewrapper-implement-etag-filter.html[/url]
最近对 http caching 感兴趣,决定一步步学习之。现先来了解 Etag。
什么是“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 是否与请求头一致,再进一步处理。
代码实现:
web.xml 配置:
测试环境是 tomcat 6.0.18。
用 httpwatch 可以观察效果。
etag-filter
etag-filter,点击放大
第二次请求(刷新),返回 304 。说明有效了。
过虑器同时还加了 Last-Modified 是为了兼容不支持 Etag 头的客户端。
参考:使用ETags减少Web应用带宽和负载
infoq 下载来的代码没试用通过,原因是没有 flush PrintWriter。虽然有 304,但返回的内容为空。
当然算 etag 可用其它算法,我这里用 crc32。infoq 例子中用 md5。
最近对 http caching 感兴趣,决定一步步学习之。现先来了解 Etag。
什么是“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 可以观察效果。
etag-filter
etag-filter,点击放大
第二次请求(刷新),返回 304 。说明有效了。
过虑器同时还加了 Last-Modified 是为了兼容不支持 Etag 头的客户端。
参考:使用ETags减少Web应用带宽和负载
infoq 下载来的代码没试用通过,原因是没有 flush PrintWriter。虽然有 304,但返回的内容为空。
当然算 etag 可用其它算法,我这里用 crc32。infoq 例子中用 md5。