介绍
最近,大众对于 REST 风格应用架构表现出强烈兴趣,这表明 Web 的优雅设计开始受到人们的注意。现在,我们逐渐理解了“ 3W 架构(Architecture of the World Wide Web)”内在所蕴含的可伸缩性和弹性,并进一步探索运用其范式的方法。本文中,我们将探究一个可被 Web 开发者利用的、鲜为人知的工具,不引人注意的“ETag 响应头(ETag Response Header)”,以及如何将它集成进基于 Spring 和 Hibernate 的动态 Web 应用,以提升应用程序性能和可伸缩性。
我们将要使用的 Spring 框架应用是基于“宠物诊所(petclinic)”的。下载文件中包含了关于如何增加必要的配置及源码的说明,你可以自己尝试。
什么是“ETag”?
HTTP 协议规格说明定义 ETag 为“被请求变量的实体值” (参见 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html —— 章节 14.19)。 另一种说法是,ETag 是一个可以与Web 资源关联的记号(token)。典型的Web 资源可以一个Web 页,但也可能是JSON 或XML 文档。服务器单独负责判断记号是什么及其含义,并在HTTP 响应头中将其传送到客户端。
ETag 如何帮助提升性能?
聪明的服务器开发者会把 ETags 和 GET 请求的“If-None-Match”头一起使用,这样可利用客户端(例如浏览器)的缓存。因为服务器首先产生 ETag,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)缓存。
其过程如下:
- 客户端请求一个页面(A)。
- 服务器返回页面 A,并在给 A 加上一个 ETag。
- 客户端展现该页面,并将页面连同 ETag 一起缓存。
- 客户再次请求页面 A,并将上次请求时服务器返回的 ETag 一起传递给服务器。
- 服务器检查该 ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应 304(未修改——Not Modified)和一个空的响应体。
本文的其余部分将展示在基于 Spring 框架的 Web 应用中利用 ETag 的两种方法,该应用使用 Spring MVC。首先我们将使用 Servlet 2.3 Filter,利用展现视图(rendered view)的 MD5 校验和(checksum)以实现生成 ETag 的方法(一个“浅显的”ETag 实现)。 第二种方法使用更为复杂的方法追踪 view 中所使用的 model,以确定 ETag 有效性(一个“深入的”ETag 实现)。尽管我们使用的是 Spring MVC,但该技术可以应用于任何 MVC 风格的 Web 框架。
在我们继续之前,强调一下这里所展现的是提升动态产生页面性能的技术。已有的优化技术也应作为整体优化和应用性能特性调整分析的一部分来考虑。(见下)。
自顶向下的 Web 缓存
本文主要涉及对动态生成页面使用 HTTP 缓存技术。当考虑提升 Web 应用的性能的时候,应采取一个整体的、自顶向下的方法。为了这一目的,理解 HTTP 请求经过的各层是很重要的,应用哪些适当的技术取决于你所关注的热点。例如:
- 将 Apache 作为 Servlet 容器的前端,来处理如图片和 javascript 脚本这样的静态文件,而且还可以使用 FileETag 指令创建 ETag 响应头。
- 使用针对 javascript 文件的优化技术,如将多个文件合并到一个文件中以及压缩空格。
- 利用 GZip 和缓存控制头(Cache-Control headers)。
- 为确定你的 Spring 框架应用的痛处所在,可以考虑使用 JamonPerformanceMonitorInterceptor 。
- 确信你充分利用 ORM 工具的缓存机制,因此对象不需要从数据库中频繁的再生。花时间确定如何让查询缓存为你工作是值得的。
- 确保你最小化数据库中获取的数据量,尤其是大的列表。如果每个页面只请求大列表的一个小子集,那么大列表的数据应由其中某个页面一次获得。
- 使放入到 HTTP session 中的数据量最小。这样内存得到释放,而且当将应用集群的时候也会有所帮助。
- 使用数据库明细(database profiling)工具来查看在查询的时候使用了什么索引,在更新的时候整个表没有被上锁。
当然,应用性能优化的至理名言是:两次测量,一次剪裁(measure twice, cut once)。哦,等等,这是对木工而言的!没错,但是它在这里也很适用!
ETag Filter 内容体
我们要考虑的第一种方法是创建一个 Servlet Filter,它将基于页面(MVC 中的“View”)的内容产生其 ETag 记号。乍一看,使用这种方法所获得的任何性能提升看起来都是违反直觉的。我们仍然不得不产生页面,而且还增加了产生记号的计算时间。然而,这里的想法是减少带宽使用。在大的响应时间情形下,如你的主机和客户端分布在这个星球的两端,这很大程度上是有益的。我曾见过东京办公室使用纽约服务器上托管的应用,其响应时间达到了 350 ms。随着并发用户数的增长,这将变成巨大的瓶颈。
代码
我们用来产生记号的技术是基于从页面内容计算 MD5 哈希值。这通过在响应之上创建一个包装器来实现。该包装器使用字节数组来保存所产生的内容,在 filter 链处理完成之后我们利用数组的 MD5 哈希值计算记号。
doFilter 方法的实现如下所示。
<span color="#0000ff">public void</span> doFilter(ServletRequest req, ServletResponse res, FilterChain chain) <span color="#0000ff">throws</span> <span color="#006699">IOException</span>,<br></br> ServletException {<br></br> HttpServletRequest servletRequest = (HttpServletRequest) req;<br></br> HttpServletResponse servletResponse = (HttpServletResponse) res;<p><span color="#0000ff">ByteArrayOutputStream</span> baos = <span color="#0000ff">new</span> <span color="#0000ff">ByteArrayOutputStream</span>();</p><br></br> ETagResponseWrapper wrappedResponse = new ETagResponseWrapper(servletResponse, baos);<br></br> chain.doFilter(servletRequest, wrappedResponse);<p><span color="#0000ff">byte</span>[] bytes = baos.toByteArray();</p><p><span color="#0000ff">String</span> token = <span color="#800000">'"'</span> + ETagComputeUtils.getMd5Digest(bytes) + '"';</p><br></br> servletResponse.setHeader(<span color="#800000">"ETag"</span>, token); <span color="#006600">// always store the ETag in the header</span><p><span color="#0000ff">String</span> previousToken = servletRequest.getHeader(<span color="#800000">"If-None-Match"</span>);</p><br></br><span color="#0000ff">if</span> (previousToken != <span color="#0000ff">null</span> && previousToken.equals(token)) { <span color="#006600">// compare previous token with current one</span><br></br> logger.debug(<span color="#800000">"ETag match: returning 304 Not Modified"</span>);<br></br> servletResponse.sendError(HttpServletResponse.SC_NOT_MODIFIED);<p><span color="#006600">// use the same date we sent when we created the ETag the first time through</span> servletResponse.setHeader(<span color="#800000">"Last-Modified"</span>, servletRequest.getHeader(<span color="#800000">"If-Modified-Since"</span>));</p><br></br> } <span color="#0000ff">else</span> { <span color="#006600">// first time through - set last modified time to now </span><br></br><span color="#0000ff">Calendar</span> cal = <span color="#0000ff">Calendar</span>.getInstance();<br></br> cal.set(<span color="#0000ff">Calendar</span>.MILLISECOND, 0);<br></br><span color="#0000ff">Date</span> lastModified = cal.getTime();<br></br> servletResponse.setDateHeader(<span color="#800000">"Last-Modified"</span>, lastModified.getTime());<p> logger.debug(<span color="#800000">"Writing body content"</span>);</p><br></br> servletResponse.setContentLength(bytes.length);<br></br> ServletOutputStream sos = servletResponse.getOutputStream();<br></br> sos.write(bytes);<br></br> sos.flush();<br></br> sos.close();<br></br> }<br></br> }
清单 1:ETagContentFilter.doFilter 你需注意到,我们还设置了 Last-Modified 头。这被认为是为服务器产生内容的正确形式,因为其迎合了不认识 ETag 头的客户端。
下面的例子使用了一个工具类 EtagComputeUtils 来产生对象所对应的字节数组,并处理 MD5 摘要逻辑。我使用了 javax.security MessageDigest 来计算 MD5 哈希码。
<span color="#0000ff">public static byte</span>[] serialize(<span color="#006699">Object</span> obj) <span color="#0000ff">throws</span> <span color="#006699">IOException</span> {<br></br><span color="#0000ff">byte</span>[] byteArray = <span color="#0000ff">null</span>;<br></br><span color="#006699">ByteArrayOutputStream</span> baos = <span color="#0000ff">null</span>;<br></br><span color="#006699">ObjectOutputStream</span> out = <span color="#0000ff">null</span>;<br></br> try {<br></br><span color="#006600">// These objects are closed in the finally</span>.<br></br> baos = <span color="#0000ff">new</span> <span color="#006699">ByteArrayOutputStream</span>();<br></br> out = <span color="#0000ff">new</span> <span color="#006699">ObjectOutputStream</span>(baos);<br></br> out.writeObject(obj);<br></br> byteArray = baos.toByteArray();<br></br> } <span color="#0000ff">finally</span> {<br></br><span color="#0000ff">if</span> (out != <span color="#0000ff">null</span>) {<br></br> out.close();<br></br> }<br></br> }<br></br><span color="#0000ff">return</span> byteArray;<br></br> }<p><span color="#0000ff">public static</span> String getMd5Digest(<span color="#0000ff">byte</span>[] bytes) {</p><br></br> MessageDigest md;<br></br><span color="#0000ff">try</span> {<br></br> md = MessageDigest.getInstance("<span color="#800000">MD5</span>");<br></br> } <span color="#0000ff">catch</span> (NoSuchAlgorithmException e) {<br></br><span color="#0000ff">throw new</span> RuntimeException(<span color="#800000">"MD5 cryptographic algorithm is not available."</span>, e);<br></br> }<br></br><span color="#0000ff">byte</span>[] messageDigest = md.digest(bytes);<br></br> BigInteger number = <span color="#0000ff">new</span> BigInteger(1, messageDigest);<p><span color="#006600">// prepend a zero to get a "proper" MD5 hash value</span> StringBuffer sb = <span color="#0000ff">new</span> StringBuffer('<span color="#800000">0</span>');</p><br></br> sb.append(number.toString(16));<br></br><span color="#0000ff">return</span> sb.toString();<br></br> }
清单 2:ETagComputeUtils直接在 web.xml 中配置 filter。
<filter><br></br> <filter-name>ETag Content Filter</filter-name><br></br> <filter-class>org.springframework.samples.petclinic.web.ETagContentFilter</filter-class><br></br> </filter><p> <filter-mapping></p><br></br> <filter-name>ETag Content Filter</filter-name><br></br> <url-pattern>/*.htm</url-pattern><br></br> </filter-mapping>
清单 3:web.xml 中配置 filter。每个.htm 文件将被 EtagContentFilter 过滤,如果页面自上次客户端请求后没有改变,它将返回一个空内容体的 HTTP 响应。
我们在这里展示的方法对特定类型的页面是有用的。但是,该方法有两个缺点:
- 我们是在页面已经被展现在服务器之后计算 ETag 的,但是在返回客户端之前。如果有 Etag 匹配,实际上并不需要再为 model 装进数据,因为要展现的页面不需要发送回客户端。
- 对于类似于在页脚显示日期时间这样的页面,即使内容实际上并没有改变,每个页面也将是不同的。
下一节,我们将着眼于另一种方法,其通过理解更多关于构造页面的底层数据来克服这些问题的某些限制。
ETag 拦截器(Interceptor)
Spring MVC HTTP 请求处理途径中包括了在一个 controller 前插接拦截器(Interceptor)的能力,因而有机会处理请求。这儿是应用我们 ETag 比较逻辑的理想场所,因此如果我们发现构建一个页面的数据没有发生变化,我们可以避免进一步处理。
这儿的诀窍是你怎么知道构成页面的数据已经改变了?为了达到本文的目的,我创建了一个简单的 ModifiedObjectTracker,它通过 Hibernate 事件侦听器清楚地知道插入、更新和删除操作。该追踪器为应用程序的每个 view 维护一个唯一的号码,以及一个关于哪些 Hibernate 实体影响每个 view 的映射。每当一个 POJO 被改变了,使用了该实体的 view 的计数器就加 1。我们使用该计数值作为 ETag,这样当客户端将 ETag 送回时我们就知道页面背后的一个或多个对象是否被修改了。
代码
我们就从 ModifiedObjectTracker 开始吧:
<span color="#0000ff">public interface</span> ModifiedObjectTracker {<br></br><span color="#0000ff">void</span> notifyModified(> <span color="#006699">String</span> entity);<br></br> }
够简单吧?这个实现还有一点更有趣的。任何时候一个实体改变了,我们就更新每个受其影响的 view 的计数器:
<span color="#0000ff">public void</span> notifyModified(<span color="#006699">String</span> entity) {<p><span color="#006600">// entityViewMap is a map of entity -> list of view names</span><span color="#006699">List</span> views = getEntityViewMap().get(entity);</p><p><span color="#0000ff">if</span> (views == <span color="#0000ff">null</span>) {</p><br></br><span color="#0000ff">return</span>; // <span color="#006600">no views are configured for this entity</span><br></br> }<p><span color="#0000ff">synchronized</span> (counts) {</p><br></br><span color="#0000ff">for</span> (<span color="#006699">String</span> view : views) {<br></br><span color="#006699">Integer</span> count = counts.get(view);<br></br> counts.put(view, ++count);<br></br> }<br></br> }<br></br> }
一个“改变”就是插入、更新或者删除。这里给出的是侦听删除操作的处理器(配置为 Hibernate 3 LocalSessionFactoryBean 上的事件侦听器):
<span color="#0000ff">public class</span> DeleteHandler <span color="#0000ff">extends</span> DefaultDeleteEventListener {<br></br><span color="#0000ff">private</span> ModifiedObjectTracker tracker;<p><span color="#0000ff">public void</span> onDelete(DeleteEvent event) <span color="#0000ff">throws</span> HibernateException {</p><br></br> getModifiedObjectTracker().notifyModified(event.getEntityName());<br></br> }<p><span color="#0000ff">public</span> ModifiedObjectTracker getModifiedObjectTracker() {</p><br></br><span color="#0000ff">return</span> tracker;<br></br> }<br></br><span color="#0000ff">public void</span> setModifiedObjectTracker(ModifiedObjectTracker tracker) {<br></br><span color="#0000ff">this</span>.tracker = tracker;<br></br> }<br></br> }
ModifiedObjectTracker 通过 Spring 配置被注入到 DeleteHandler 中。还有一个 SaveOrUpdateHandler 来处理新建或更新 POJO。
如果客户端发送回当前有效的 ETag(意味着自上次请求之后我们的内容没有改变),我们将阻止更多的处理,以实现我们的性能提升。在 Spring MVC 里,我们可以使用 HandlerInterceptorAdaptor 并覆盖 preHandle 方法:
<span color="#0000ff">public final boolean</span> preHandle(HttpServletRequest request, HttpServletResponse response, <span color="#006699">Object</span> handler) <span color="#0000ff">throws</span><br></br> ServletException, <span color="#006699">IOException</span> {<br></br><span color="#006699">String</span> method = request.getMethod();<br></br> if (!"GET".equals(method))<br></br><span color="#0000ff">return true</span>;<p><span color="#006699">String</span> previousToken = request.getHeader("<span color="#800000">If-None-Match</span>");</p><br></br><span color="#006699">String</span> token = getTokenFactory().getToken(request);<p><span color="#006600">// compare previous token with current one</span><span color="#0000ff">if</span> ((token != <span color="#0000ff">null</span>) && (previousToken != null && previousToken.equals(<span color="#800000">'"'</span> + token + <span color="#800000">'"'</span>))) {</p><br></br> response.sendError(HttpServletResponse.SC_NOT_MODIFIED);<p><span color="#006600">// re-use original last modified timestamp</span> response.setHeader("<span color="#800000">Last-Modified</span>", request.getHeader("<span color="#800000">If-Modified-Since</span>"))</p><br></br><span color="#0000ff">return false</span>; <span color="#006600">// no further processing required</span><br></br> }<p><span color="#006600">// set header for the next time the client calls</span><span color="#0000ff">if</span> (token != null) { </p><br></br> response.setHeader(<span color="#800000">"ETag"</span>, <span color="#800000">'"'</span> + token + <span color="#800000">'"'</span>);<p><span color="#006600">// first time through - set last modified time to now</span><span color="#006699">Calendar</span> cal = <span color="#006699">Calendar</span>.getInstance();</p><br></br> cal.set(<span color="#006699">Calendar</span>.MILLISECOND, 0);<br></br><span color="#006699">Date</span> lastModified = cal.getTime();<br></br> response.setDateHeader("<span color="#800000">Last-Modified</span>", lastModified.getTime());<br></br> }<p><span color="#0000ff">return true</span>;</p><br></br> }
我们首先确信我们正在处理 GET 请求(与 PUT 一起的 ETag 可以用来检测不一致的更新,但其超出了本文的范围。)。如果该记号与上次我们发送的记号相匹配,我们返回一个“304 未修改”响应并“短路”请求处理链的其余部分。否则,我们设置 ETag 响应头以便为下一次客户端请求做好准备。
你需注意到我们将产生记号逻辑抽出到一个接口中,这样可以插接不同的实现。该接口有一个方法:
<span color="#0000ff">public interface</span> ETagTokenFactory {<br></br><span color="#0000ff">String</span> getToken(<span color="#006699">HttpServletRequest</span> request);<br></br> }
为了把代码清单减至最小,SampleTokenFactory 的简单实现还担当了 ETagTokenFactory 的角色。本例中,我们通过简单返回请求 URI 的更改计数值来产生记号:
<span color="#0000ff">public String</span> getToken(<span color="#006699">HttpServletRequest</span> request) {<br></br><span color="#006699">String</span> view = request.getRequestURI();<br></br><span color="#006699">Integer</span> count = counts.get(view);<br></br><span color="#0000ff">if</span> (count == null) {<br></br><span color="#0000ff">return</span> null;<br></br> }<p><span color="#0000ff">return</span> count.toString();</p><br></br> }
大功告成!
会话
这里,如果什么也没改变,我们的拦截器将阻止任何搜集数据或展现 view 的开销。现在,让我们看看 HTTP 头(借助于 LiveHTTPHeaders ),看看到底发生了什么。下载文件中包含了配置该拦截器的说明,因此 owner.htm“能够使用 ETag”:
我们发起的第一个请求说明该用户已经看过了这个页面:
---------------------------------------------------------- <p><strong>http://localhost:8080/petclinic/owner.htm?ownerId=10 </strong> GET /petclinic/owner.htm?ownerId=10 HTTP/1.1</p><br></br> Host: localhost:8080<br></br> User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4<br></br> Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5<br></br> Accept-Language: en-us,en;q=0.5<br></br> Accept-Encoding: gzip,deflate<br></br> Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7<br></br> Keep-Alive: 300<br></br> Connection: keep-alive<br></br> Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8<br></br> X-lori-time-1: 1182364348062<br></br> If-Modified-Since: Wed, 20 Jun 2007 18:29:03 GMT<br></br> If-None-Match: "-1"<p> HTTP/1.x 304 Not Modified</p><br></br> Server: Apache-Coyote/1.1<br></br> Date: Wed, 20 Jun 2007 18:32:30 GMT
我们现在应该做点修改,看看 ETag 是否改变了。我们给这个物主增加一个宠物:
----------------------------------------------------------<br></br><strong>http://localhost:8080/petclinic/addPet.htm?ownerId=10</strong> GET /petclinic/addPet.htm?ownerId=10 HTTP/1.1<br></br> Host: localhost:8080<br></br> User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4<br></br> Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5<br></br> Accept-Language: en-us,en;q=0.5<br></br> Accept-Encoding: gzip,deflate<br></br> Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7<br></br> Keep-Alive: 300<br></br> Connection: keep-alive<br></br> Referer: http://localhost:8080/petclinic/owner.htm?ownerId=10<br></br> Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8<br></br> X-lori-time-1: 1182364356265<p> HTTP/1.x 200 OK</p><br></br> Server: Apache-Coyote/1.1<br></br> Pragma: No-cache<br></br> Expires: Thu, 01 Jan 1970 00:00:00 GMT<br></br> Cache-Control: no-cache, no-store<br></br> Content-Type: text/html;charset=ISO-8859-1<br></br> Content-Language: en-US<br></br> Content-Length: 2174<br></br> Date: Wed, 20 Jun 2007 18:32:57 GMT<br></br> ----------------------------------------------------------<p><strong>http://localhost:8080/petclinic/addPet.htm?ownerId=10 </strong> POST /petclinic/addPet.htm?ownerId=10 HTTP/1.1</p><br></br> Host: localhost:8080<br></br> User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4<br></br> Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5<br></br> Accept-Language: en-us,en;q=0.5<br></br> Accept-Encoding: gzip,deflate<br></br> Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7<br></br> Keep-Alive: 300<br></br> Connection: keep-alive<br></br> Referer: http://localhost:8080/petclinic/addPet.htm?ownerId=10<br></br> Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8<br></br> X-lori-time-1: 1182364402968<br></br> Content-Type: application/x-www-form-urlencoded<br></br> Content-Length: 40<br></br> name=Noddy&birthDate=1000-11-11&typeId=5<br></br> HTTP/1.x 302 Moved Temporarily<br></br> Server: Apache-Coyote/1.1<br></br> Pragma: No-cache<br></br> Expires: Thu, 01 Jan 1970 00:00:00 GMT<br></br> Cache-Control: no-cache, no-store<br></br> Location: http://localhost:8080/petclinic/owner.htm?ownerId=10<br></br> Content-Language: en-US<br></br> Content-Length: 0<br></br> Date: Wed, 20 Jun 2007 18:33:23 GMT
因为对 addPet.htm 我们没有配置任何已知 ETag,也没有设置头信息。现在,我们再一次查看 id 为 10 的物主。注意 ETag 这时是 1:
----------------------------------------------------------<br></br><strong>http://localhost:8080/petclinic/owner.htm?ownerId=10</strong> GET /petclinic/owner.htm?ownerId=10 HTTP/1.1<br></br> Host: localhost:8080<br></br> User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4<br></br> Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5<br></br> Accept-Language: en-us,en;q=0.5<br></br> Accept-Encoding: gzip,deflate<br></br> Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7<br></br> Keep-Alive: 300<br></br> Connection: keep-alive<br></br> Referer: http://localhost:8080/petclinic/addPet.htm?ownerId=10<br></br> Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8<br></br> X-lori-time-1: 1182364403109<br></br> If-Modified-Since: Wed, 20 Jun 2007 18:29:03 GMT<br></br> If-None-Match: "-1"<p> HTTP/1.x 200 OK</p><br></br> Server: Apache-Coyote/1.1<br></br> Etag: "1"<br></br> Last-Modified: Wed, 20 Jun 2007 18:33:36 GMT<br></br> Content-Type: text/html;charset=ISO-8859-1<br></br> Content-Language: en-US<br></br> Content-Length: 4317<br></br> Date: Wed, 20 Jun 2007 18:33:45 GMT
最后,我们再次查看 id 为 10 的物主。这次我们的 ETag 命中了,我们得到一个“304 未修改”响应:
----------------------------------------------------------<br></br> http://localhost:8080/petclinic/owner.htm?ownerId=10<p> GET /petclinic/owner.htm?ownerId=10 HTTP/1.1</p><br></br> Host: localhost:8080<br></br> User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4<br></br> Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5<br></br> Accept-Language: en-us,en;q=0.5<br></br> Accept-Encoding: gzip,deflate<br></br> Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7<br></br> Keep-Alive: 300<br></br> Connection: keep-alive<br></br> Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8<br></br> X-lori-time-1: 1182364493500<br></br> If-Modified-Since: Wed, 20 Jun 2007 18:33:36 GMT<br></br> If-None-Match: "1"<p> HTTP/1.x 304 Not Modified</p><br></br> Server: Apache-Coyote/1.1<br></br> Date: Wed, 20 Jun 2007 18:34:55 GMT
我们已经利用 HTTP 缓存节约了带宽和计算时间!
细粒度印记(The Fine Print):实践中,我们可以通过以更细粒度的跟踪对象变化来获得更大的功效,例如使用对象 id。然而,这种使修改对象关联到 view 上的想法高度依赖应用程序的整体数据模型设计。这里的实现(ModifiedObjectTracker)是说明性的,有意为更多的探索提供想法。它并不是旨在生产环境中使用(比如它在簇中使用还不稳定)。一个可选的更深的考虑是使用数据库触发器来跟踪变化,让拦截器访问触发器所写入的表。
结论
我们已经看了两种使用 ETag 减少带宽和计算的方法。我希望本文已为你当下或将来基于 Web 的项目提供了精神食粮,并正确评价在底层利用 ETag 响应头的做法。
正如牛顿(Isaac Newton)的名言所说:“如果说我看得更远,那是因为我站在巨人的肩膀上。”REST 风格应用的核心是简单、好的软件设计、不要重新发明轮子。我相信随着使用量和知名度的增长,针对基于 Web 应用的 REST 风格架构有益于主流应用开发的迁移,我期盼着它在我将来的项目中发挥更大的作用。
关于作者
Gavin Terrill 是 BPS 公司的首席技术执行官。Gavin 已经有 20 多年的软件开发历史了,擅长企业 Java 应用程序,但仍拒绝扔掉他的 TRS-80。闲暇时间 Gavin 喜欢航海、钓鱼、玩吉他、品红酒(不分先后顺序)。
感谢
我要感谢我的同事 Patrick Bourke 和 Erick Dorvale 的帮助,他们对这篇文章提供的反馈意见。
代码和说明可以从这里下载。
查看英文原文: Using ETags to Reduce Bandwith & Workload with Spring & Hibernate
评论 1 条评论