返回列表

转载: 用 HttpServletResponseWrapper 实现 Etag 过滤器

默认分类 2011-03-03 05:04:33

原文出处:http://blog.chenlb.com/2009/07/use-httpservletresponsewrapper-implement-etag-filter.html

最近对 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 是否与请求头一致,再进一步处理。

代码实现:

  1. package com.chenlb.http; 
  2.  
  3. import java.io.ByteArrayOutputStream; 
  4. import java.io.IOException; 
  5. import java.io.PrintWriter; 
  6. import java.util.Calendar; 
  7. import java.util.Date; 
  8. import java.util.zip.CRC32; 
  9.  
  10. import javax.servlet.Filter; 
  11. import javax.servlet.FilterChain; 
  12. import javax.servlet.FilterConfig; 
  13. import javax.servlet.ServletException; 
  14. import javax.servlet.ServletOutputStream; 
  15. import javax.servlet.ServletRequest; 
  16. import javax.servlet.ServletResponse; 
  17. import javax.servlet.http.HttpServletRequest; 
  18. import javax.servlet.http.HttpServletResponse; 
  19. import javax.servlet.http.HttpServletResponseWrapper; 
  20.  
  21. publicclass EtagFilterimplements Filter { 
  22.  
  23.    publicvoid destroy() {} 
  24.  
  25.    publicvoid doFilter(ServletRequest request, ServletResponse response, 
  26.             FilterChain chain) throws IOException, ServletException { 
  27.  
  28.         HttpServletRequest servletRequest = (HttpServletRequest) request; 
  29.         HttpServletResponse servletResponse = (HttpServletResponse) response; 
  30.  
  31.         ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
  32.         HttpServletResponseWrapper hsrw = new MyHttpResponseWrapper(servletResponse, baos); 
  33.  
  34.         chain.doFilter(request, hsrw); 
  35.  
  36.         hsrw.flushBuffer(); 
  37.  
  38.        byte[] bytes = baos.toByteArray(); 
  39.  
  40.         CRC32 crc =new CRC32(); 
  41.         crc.update(bytes); 
  42.  
  43.         String token ="w/\"" + crc.getValue() +'"'
  44.         servletResponse.setHeader("ETag", token); 
  45.        // always store the ETag in the header 
  46.         String previousToken = servletRequest.getHeader("If-None-Match"); 
  47.        if (previousToken != null && previousToken.equals(token)) { 
  48.            // compare previous token with current one       
  49.  
  50.             System.out.println("ETag match: returning 304 Not Modified"); 
  51.             servletResponse.sendError(HttpServletResponse.SC_NOT_MODIFIED); 
  52.            // use the same date we sent when we created the ETag the first time through 
  53.             servletResponse.setHeader("Last-Modified", servletRequest.getHeader("If-Modified-Since")); 
  54.         }else  { 
  55.            // first time through - set last modified time to now 
  56.             Calendar cal = Calendar.getInstance(); 
  57.             cal.set(Calendar.MILLISECOND, 0); 
  58.             Date lastModified = cal.getTime(); 
  59.             servletResponse.setDateHeader("Last-Modified", lastModified.getTime()); 
  60.             System.out.println("Writing body content"); 
  61.             servletResponse.setContentLength(bytes.length); 
  62.             ServletOutputStream sos = servletResponse.getOutputStream(); 
  63.             sos.write(bytes); 
  64.             sos.flush(); 
  65.             sos.close(); 
  66.         } 
  67.  
  68.     } 
  69.  
  70.    publicvoid init(FilterConfig config) throws ServletException {} 
  71.  
  72.    privatestaticclass MyHttpResponseWrapperextends HttpServletResponseWrapper { 
  73.  
  74.         ByteServletOutputStream servletOutputStream; 
  75.         PrintWriter printWriter; 
  76.  
  77.        public MyHttpResponseWrapper(HttpServletResponse response, ByteArrayOutputStream buffer) { 
  78.            super(response); 
  79.             servletOutputStream =new ByteServletOutputStream(buffer); 
  80.         } 
  81.  
  82.        public ServletOutputStream getOutputStream() throws IOException { 
  83.            return servletOutputStream; 
  84.         } 
  85.  
  86.        public PrintWriter getWriter() throws IOException { 
  87.            if(printWriter == null) { 
  88.                 printWriter = new PrintWriter(servletOutputStream); 
  89.             } 
  90.            return printWriter; 
  91.         } 
  92.  
  93.        publicvoid flushBuffer()throws IOException { 
  94.             servletOutputStream.flush(); 
  95.            if(printWriter != null) { 
  96.                 printWriter.flush(); 
  97.             } 
  98.         } 
  99.     } 
  100.  
  101.    privatestaticclass ByteServletOutputStreamextends ServletOutputStream { 
  102.  
  103.         ByteArrayOutputStream baos; 
  104.  
  105.        public ByteServletOutputStream(ByteArrayOutputStream baos) { 
  106.            super(); 
  107.            this.baos = baos; 
  108.         } 
  109.  
  110.        publicvoid write(int b) throws IOException { 
  111.             baos.write(b); 
  112.         } 
  113.     } 

web.xml 配置:

  1. <filter> 
  2.    <filter-name>etag</filter-name> 
  3.    <filter-class>com.chenlb.http.EtagFilter</filter-class> 
  4. </filter>          
  5.  
  6. <filter-mapping> 
  7.    <filter-name>etag</filter-name> 
  8.    <url-pattern>*.jsp</url-pattern> 
  9. </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。