導(dǎo)言

REST方式的應(yīng)用程序構(gòu)架在近日所產(chǎn)生的巨大影響突出了Web應(yīng)用程序的優(yōu)雅設(shè)計(jì)的重要性。現(xiàn)在人們開(kāi)始理解“WWW架構(gòu)”內(nèi)在的可測(cè)量性及彈性,并且已經(jīng)開(kāi)始探索使用其范例的更好的方式。在本文中,我們將討論一個(gè)Web應(yīng)用開(kāi)發(fā)工具——“簡(jiǎn)陋的、卑下的”ETags,以及如何在基于SpringFramework的動(dòng)態(tài)Web應(yīng)用程序中集成這個(gè)工具,來(lái)提高應(yīng)用的性能及可測(cè)性。

我們將要使用的基于Spring的應(yīng)用程序是基于“petclinic”(寵物門(mén)診?)的一個(gè)應(yīng)用。在您下載的程序包中,包含了如何加入必要的配置和源代碼讓你親自體驗(yàn)該程序的介紹。

什么是ETag
 
在HTTP協(xié)議規(guī)范中,ETag被定義為“被請(qǐng)求的變量的實(shí)體值”。(
參見(jiàn) http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - Section 14.19。)換句話說(shuō),ETag是一個(gè)與Web資源相關(guān)聯(lián)的標(biāo)記。典型的Web資源是一個(gè)Web頁(yè)面,但也可以是一個(gè)JSON格式或者XML格式的文檔。服務(wù)器可以指出一個(gè)標(biāo)記是什么及其意義,并將這個(gè)標(biāo)記放在HTTP頭重傳送給客戶端。

ETag如何提高應(yīng)用程序性能

ETag和一個(gè)GET請(qǐng)求的“If-None-Match”頭信息一起使用,服務(wù)器開(kāi)發(fā)者以此來(lái)使用客戶端緩存的優(yōu)勢(shì)。服務(wù)器在客戶端的一次請(qǐng)求時(shí)產(chǎn)生ETag,并在以后的請(qǐng)求中判斷被請(qǐng)求資源是否發(fā)生了變化。確切的說(shuō),客戶端將這個(gè)標(biāo)記傳回給服務(wù)器,來(lái)驗(yàn)證它自己的緩存是否有效。

整個(gè)處理過(guò)程如下:

客戶端請(qǐng)求頁(yè)面A
服務(wù)器響應(yīng),返回頁(yè)面A,附加ETag
客戶端顯示A,并將頁(yè)面和ETag一并緩存
客戶端再次請(qǐng)求頁(yè)面A,請(qǐng)求中包含了上次請(qǐng)求頁(yè)面A時(shí)返回的ETag
服務(wù)器檢查客戶端發(fā)送過(guò)來(lái)的ETag,并確定頁(yè)面A在該客戶端上次請(qǐng)求后到現(xiàn)在沒(méi)有發(fā)生過(guò)變化,因此,發(fā)送一個(gè)304(未改變)響應(yīng)頭給客戶端,附帶一個(gè)空的響應(yīng)體。

文章的剩余部分將討論在基于SpringFramework的使用SpringMVC的Web應(yīng)用程序中使用ETag兩種方式。首先,我們將通過(guò)一個(gè)Servlet2.3 過(guò)濾器,使用由計(jì)算請(qǐng)求返回結(jié)果的MD5值而產(chǎn)生的ETag(一個(gè)簡(jiǎn)單的ETag實(shí)現(xiàn))。第二種方式使用一種更加“專業(yè)”的方式通過(guò)跟蹤頁(yè)面呈現(xiàn)所用到的模型的變化來(lái)確定ETag的有效性(一個(gè)“專業(yè)”的ETag實(shí)現(xiàn))。雖然我們?cè)谶@里使用了Spring MVC,但這個(gè)技術(shù)適用于其他任何的MVC框架。

在繼續(xù)之前,我們有必要明確,ETag技術(shù)是為了希望改進(jìn)動(dòng)態(tài)產(chǎn)生的頁(yè)面的訪問(wèn)速度而提出的。作為一個(gè)完整的性能優(yōu)化方案和性能分析,其他的性能優(yōu)化技術(shù)依然應(yīng)當(dāng)被考慮。

自頂向下的Web緩存

本文首先討論將HTTP緩存技術(shù)應(yīng)用于動(dòng)態(tài)頁(yè)面。尋求Web應(yīng)用程序優(yōu)化方案時(shí),我們應(yīng)當(dāng)采用一個(gè)完整的,自頂向下的步驟。從根本上說(shuō),理解HTTP請(qǐng)求的過(guò)程是很重要的,采用哪種具體的技術(shù)取決于你在什么場(chǎng)合。例如:

Apache可以放在你的Servlet容易之前,來(lái)接受如圖片,js請(qǐng)求,同時(shí)也可以使用FileETag指令產(chǎn)生ETag響應(yīng)頭。

使用Javascript優(yōu)化技術(shù),例如將多個(gè)js文件合并,并去除空格等無(wú)用信息。

利用GZip和Cache-Control響應(yīng)頭。

使用JamonPerformanceMonitorInterceptor確定你的Spring應(yīng)用系統(tǒng)中的性能瓶頸。

確定你充分地使用了ORM工具的緩存機(jī)制,從而使得實(shí)體信息不是頻繁的從數(shù)據(jù)庫(kù)中重新加載。搞清楚如何讓查詢緩存很好的工作需要一定的時(shí)間。

確保盡量少聰數(shù)據(jù)庫(kù)中重新加載數(shù)據(jù),特別是一些大的列表。大列表應(yīng)當(dāng)被按頁(yè)分割,對(duì)每一頁(yè)的請(qǐng)求返回大列表的一個(gè)小的子集。

Session中保存盡量少的信息。這降低了內(nèi)存要求,在建立應(yīng)用層集群時(shí)將會(huì)顯得非常有用。

使用一個(gè)數(shù)據(jù)庫(kù)調(diào)試工具,確定查詢時(shí)使用了哪些索引,查詢時(shí)數(shù)據(jù)表將不會(huì)被鎖定。

當(dāng)然了,性能優(yōu)化的最佳格言是適用的:測(cè)量?jī)纱危懈钜淮巍#ǘ啻螠y(cè)試后再修改)
等等,上面的話是對(duì)木匠說(shuō)的,但雖然如此,它一樣適用于我們!


 一個(gè)內(nèi)容主體ETag過(guò)濾器

我們將看到的第一種方式是建立一個(gè)Servlet過(guò)濾器基于頁(yè)面內(nèi)容(MVC中的View)來(lái)產(chǎn)生ETag標(biāo)記。乍一看,使用這種方式對(duì)性能的提升似乎沒(méi)什么大的作用。服務(wù)器依然需要聲稱頁(yè)面,并且增加了計(jì)算標(biāo)記值的時(shí)間。但是,在這里我們的目的是減少帶寬占用。這對(duì)于很多的反應(yīng)時(shí)間很長(zhǎng)的情形是一個(gè)很大的益處,例如如果你的應(yīng)用的服務(wù)器和客戶端分別在地球的不同半球上。我曾看到一個(gè)從東京發(fā)出的對(duì)紐約的某臺(tái)服務(wù)器的請(qǐng)求,響應(yīng)長(zhǎng)達(dá)350毫秒。考慮并發(fā)用戶因素后,這將成為一個(gè)重大的瓶頸。


代碼

我們用于產(chǎn)生標(biāo)記的技術(shù)是計(jì)算頁(yè)面返回內(nèi)容的MD5值。創(chuàng)建一個(gè)響應(yīng)包裝器將完成這個(gè)工作。包裝器使用一個(gè)字節(jié)數(shù)組來(lái)保存返回內(nèi)容,在過(guò)濾器鏈處理完成之后,我們計(jì)算這個(gè)字節(jié)數(shù)組的MD5哈希值。

doFilter方法的實(shí)現(xiàn)如下:


 1public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
 2 ServletException {
 3  HttpServletRequest servletRequest = (HttpServletRequest) req;
 4  HttpServletResponse servletResponse = (HttpServletResponse) res;
 5
 6   ByteArrayOutputStream baos = new ByteArrayOutputStream();
 7  ETagResponseWrapper wrappedResponse = new ETagResponseWrapper(servletResponse, baos);
 8  chain.doFilter(servletRequest, wrappedResponse);
 9
10   byte[] bytes = baos.toByteArray();
11
12   String token = '"' + ETagComputeUtils.getMd5Digest(bytes) + '"';
13  servletResponse.setHeader("ETag", token); // always store the ETag in the header
14
15   String previousToken = servletRequest.getHeader("If-None-Match");
16  if (previousToken != null && previousToken.equals(token)) // compare previous token with current one
17   logger.debug("ETag match: returning 304 Not Modified");
18   servletResponse.sendError(HttpServletResponse.SC_NOT_MODIFIED);
19   // use the same date we sent when we created the ETag the first time through
20   servletResponse.setHeader("Last-Modified", servletRequest.getHeader("If-Modified-Since"));
21  }
 else  {   // first time through - set last modified time to now 
22   Calendar cal = Calendar.getInstance();
23   cal.set(Calendar.MILLISECOND, 0);
24   Date lastModified = cal.getTime();
25   servletResponse.setDateHeader("Last-Modified", lastModified.getTime());
26
27    logger.debug("Writing body content");
28   servletResponse.setContentLength(bytes.length);
29   ServletOutputStream sos = servletResponse.getOutputStream();
30   sos.write(bytes);
31   sos.flush();
32   sos.close();
33  }

34 }
Listing 1: ETagContentFilter.doFilter

應(yīng)該注意到,我們?cè)O(shè)置了“Last-Modified”響應(yīng)頭。這是因?yàn)槲覀冃枰M織良好的內(nèi)容格式,以對(duì)應(yīng)哪些無(wú)法理解ETag響應(yīng)頭的客戶端。
上面的示例代碼用到了一個(gè)EtagComputeUtils工具類來(lái)產(chǎn)生一個(gè)對(duì)象的字節(jié)數(shù)組表示并處理MD5雜湊邏輯。在這里我使用javax.security.MessageDigest來(lái)計(jì)算MD5值。

 1public static byte[] serialize(Object obj) throws IOException {
 2  byte[] byteArray = null;
 3  ByteArrayOutputStream baos = null;
 4  ObjectOutputStream out = null;
 5  try {
 6   // These objects are closed in the finally.
 7   baos = new ByteArrayOutputStream();
 8   out = new ObjectOutputStream(baos);
 9   out.writeObject(obj);
10   byteArray = baos.toByteArray();
11  }
 finally {
12   if (out != null{
13    out.close();
14   }

15  }

16  return byteArray;
17 }

18
19  public static String getMd5Digest(byte[] bytes) {
20  MessageDigest md;
21  try {
22   md = MessageDigest.getInstance("MD5");
23  }
 catch (NoSuchAlgorithmException e) {
24   throw new RuntimeException("MD5 cryptographic algorithm is not available.", e);
25  }

26  byte[] messageDigest = md.digest(bytes);
27  BigInteger number = new BigInteger(1, messageDigest);
28  // prepend a zero to get a "proper" MD5 hash value
29  StringBuffer sb = new StringBuffer('0');
30  sb.append(number.toString(16));
31  return sb.toString();
32 }
 
33Listing 2: ETagComputeUtils 

在Web.xml中調(diào)用這個(gè)過(guò)濾器是很簡(jiǎn)單的:
1<filter>
2    <filter-name>ETag Content Filter</filter-name>
3    <filter-class>org.springframework.samples.petclinic.web.ETagContentFilter</filter-class>
4</filter>
5
6<filter-mapping>
7    <filter-name>ETag Content Filter</filter-name>
8    <url-pattern>/*.htm</url-pattern>
9</filter-mapping>
Listing 3: Configuration of the filter in web.xml.


每一個(gè)htm文件將被EtagContentFilter過(guò)濾,如果該文件在上次請(qǐng)求后沒(méi)有發(fā)生變化,則返回一個(gè)空的HTTP響應(yīng)體。

上面討論的方式對(duì)于確定類型的頁(yè)面很有用,但也有一些缺點(diǎn)。
頁(yè)面在服務(wù)器段生成之后,在返回給客戶端之前,我們計(jì)算了ETag值,如果ETag匹配,那么我們實(shí)在是沒(méi)有必要去取出模型數(shù)據(jù),因?yàn)殇秩境鰜?lái)的頁(yè)面將不會(huì)返回給客戶端。
對(duì)于在頁(yè)腳呈現(xiàn)日期和時(shí)間的頁(yè)面,每次請(qǐng)求都是不同的,即使頁(yè)面的主題內(nèi)容并沒(méi)有發(fā)生改變。
下面,我們將看到另一種可選的方法——通過(guò)理解構(gòu)建頁(yè)面的底層數(shù)據(jù)來(lái)解決上面的限制帶來(lái)的問(wèn)題。

ETag攔截器
Spring MVC中的HTTP請(qǐng)求傳遞途徑包含了一種可以在控制器處理請(qǐng)求之前插入一個(gè)攔截器的能力。這對(duì)于插入ETag對(duì)比邏輯來(lái)說(shuō)是一個(gè)極其合適的切入點(diǎn),在這里,如果發(fā)現(xiàn)構(gòu)建頁(yè)面的數(shù)據(jù)沒(méi)有發(fā)生變化,我們就可以停止更進(jìn)一步的處理。
這里的訣竅是如何知道構(gòu)建所請(qǐng)求的頁(yè)面的數(shù)據(jù)沒(méi)有發(fā)生變化。為了本文的目的,我創(chuàng)建了一個(gè)簡(jiǎn)單的ModifiedObjectTracker,通過(guò)Hiberante事件監(jiān)聽(tīng)器來(lái)跟蹤新增、更新、刪除操作。跟蹤器將為每一個(gè)頁(yè)面保持一個(gè)為一個(gè)數(shù)字,以及一個(gè)影響到該頁(yè)面的持久化實(shí)體的Map。如果一個(gè)POJO發(fā)生了變化,那么一個(gè)技術(shù)其將增加所有用到了這個(gè)POJO的頁(yè)面對(duì)應(yīng)的數(shù)字。將這個(gè)數(shù)字作為ETag,當(dāng)客戶端將ETag返回時(shí),我們將會(huì)知道一個(gè)頁(yè)面所用到的模型是否發(fā)生了變化。

代碼


從ModifiedObjectTracker開(kāi)始:
1 public interface ModifiedObjectTracker {
2     void notifyModified(> String entity);
3 

很簡(jiǎn)單吧?它的實(shí)現(xiàn)會(huì)比較有意思。每當(dāng)一個(gè)實(shí)體發(fā)生了變化,我們?yōu)槊恳粋€(gè)用到了該實(shí)體的頁(yè)面更新對(duì)應(yīng)的計(jì)數(shù)器。
 1 public void notifyModified(String entity) {
 2   // entityViewMap is a map of entity -> list of view names
 3   List views = getEntityViewMap().get(entity);
 4 
 5    if (views == null) {
 6    return// no views are configured for this entity
 7   }
 8 
 9    synchronized (counts) {
10    for (String view : views) {
11     Integer count = counts.get(view);
12     counts.put(view, ++count);
13    }
14   }
15  } 

一次“變化”就是一次新增、修改或者刪除操作。下面是針對(duì)刪除操作的處理器列表(作為事件監(jiān)聽(tīng)器配置在Hibernate 3 LocalSessionFactoryBean中)。
public class DeleteHandler extends DefaultDeleteEventListener {
  
private ModifiedObjectTracker tracker;

   
public void onDelete(DeleteEvent event) throws HibernateException {
   getModifiedObjectTracker().notifyModified(event.getEntityName());
  }

  
public ModifiedObjectTracker getModifiedObjectTracker() {
   
return tracker;
  }
   
public void setModifiedObjectTracker(ModifiedObjectTracker tracker) {
   
this.tracker = tracker;
  }
 } 

ModifiedObjectTracker將通過(guò)Spring配置注射到DeleteHandler中。同時(shí),將會(huì)有一個(gè)SaveOrUpdateHandler處理實(shí)體的新增和修改。
如果客戶端發(fā)回了一個(gè)當(dāng)前有效的ETag(意思是內(nèi)容在上次請(qǐng)求后未曾發(fā)生改變),我們將阻止更多的處理邏輯,以實(shí)現(xiàn)我們的性能提升。在Spring MVC中,可以使用一個(gè)HandlerInterceptorAdaptor ,并重寫(xiě)preHandle方法:

public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
 ServletException, IOException {
  String method 
= request.getMethod();
  
if (!"GET".equals(method))
   
return true;

   String previousToken 
= request.getHeader("If-None-Match");
  String token 
= getTokenFactory().getToken(request);

   
// compare previous token with current one
  if ((token != null&& (previousToken != null && previousToken.equals('"' + token + '"'))) {
   response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
   
// re-use original last modified timestamp
   response.setHeader("Last-Modified", request.getHeader("If-Modified-Since"))
   
return false// no further processing required
  }

   
// set header for the next time the client calls
  if (token != null) { 
   response.setHeader(
"ETag"'"' + token + '"');

    
// first time through - set last modified time to now
   Calendar cal = Calendar.getInstance();
   cal.set(Calendar.MILLISECOND, 
0);
   Date lastModified 
= cal.getTime();
   response.setDateHeader(
"Last-Modified", lastModified.getTime());
  }

   
return true;
 } 


首先我們需要確定我們處理的是一個(gè)GET請(qǐng)求(ETag可以在客戶端發(fā)出PUT請(qǐng)求時(shí)驗(yàn)證更新是否沖突,但那已經(jīng)超出了本文的范圍)。如果標(biāo)記和服務(wù)器上次返回的標(biāo)記相匹配,則返回一個(gè)304位發(fā)生改變響應(yīng),并繞過(guò)后面的處理鏈。否則,我們?cè)O(shè)置一個(gè)ETag響應(yīng)頭,以備客戶端下次請(qǐng)求同樣的頁(yè)面。

可以看到,我將產(chǎn)生標(biāo)記的邏輯抽象出來(lái)形成了一個(gè)接口,如此我們則可以使用不同的標(biāo)記生成策略。該接口只有一個(gè)方法:

public interface ETagTokenFactory {
  String getToken(HttpServletRequest request);

為了少列出一些代碼,我的SampleTokenFactory實(shí)現(xiàn)同時(shí)承擔(dān)了ETagTokenFactory的任務(wù)。如此,我們簡(jiǎn)單的將被請(qǐng)求的URL的修改次數(shù)作為標(biāo)記返回。
public String getToken(HttpServletRequest request) {
  String view 
= request.getRequestURI();
  Integer count 
= counts.get(view);
  
if (count == null) {
   
return null;
  }

   
return count.toString();
 } 


就這樣!

討論

在這里,我們的攔截器將在沒(méi)有相關(guān)數(shù)據(jù)發(fā)生變化時(shí)阻止一切收集數(shù)據(jù)和渲染頁(yè)面的處理過(guò)程。現(xiàn)在,讓我們來(lái)看一下HTTP頭,以及在表象之下到底發(fā)生了些什么。示例程序中包含了使得owner.htm使用ETag的配置介紹。
第一次請(qǐng)求說(shuō)明用戶已經(jīng)看到了該頁(yè)面:

http://localhost:8080/petclinic/owner.htm?ownerId=10 

 GET /petclinic/owner.htm?ownerId=10 HTTP/1.1
 Host: localhost:8080
 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
 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
 Accept-Language: en-us,en;q=0.5
 Accept-Encoding: gzip,deflate
 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
 Keep-Alive: 300
 Connection: keep-alive
 Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
 X-lori-time-1: 1182364348062
 If-Modified-Since: Wed, 20 Jun 2007 18:29:03 GMT
 If-None-Match: "-1"

 HTTP/1.x 304 Not Modified
 Server: Apache-Coyote/1.1
 Date: Wed, 20 Jun 2007 18:32:30 GMT

下面我們觸發(fā)一些變化,并觀察ETag是否改變。為這個(gè)Owner增加了一個(gè)Pet:


----------------------------------------------------------
 http://localhost:8080/petclinic/addPet.htm?ownerId=10

 GET /petclinic/addPet.htm?ownerId=10 HTTP/1.1
 Host: localhost:8080
 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
 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
 Accept-Language: en-us,en;q=0.5
 Accept-Encoding: gzip,deflate
 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
 Keep-Alive: 300
 Connection: keep-alive
 Referer: http://localhost:8080/petclinic/owner.htm?ownerId=10
 Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
 X-lori-time-1: 1182364356265

 HTTP/1.x 200 OK
 Server: Apache-Coyote/1.1
 Pragma: No-cache
 Expires: Thu, 01 Jan 1970 00:00:00 GMT
 Cache-Control: no-cache, no-store
 Content-Type: text/html;charset=ISO-8859-1
 Content-Language: en-US
 Content-Length: 2174
 Date: Wed, 20 Jun 2007 18:32:57 GMT
 ----------------------------------------------------------
 http://localhost:8080/petclinic/addPet.htm?ownerId=10 
 
 POST /petclinic/addPet.htm?ownerId=10 HTTP/1.1
 Host: localhost:8080
 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
 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
 Accept-Language: en-us,en;q=0.5
 Accept-Encoding: gzip,deflate
 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
 Keep-Alive: 300
 Connection: keep-alive
 Referer: http://localhost:8080/petclinic/addPet.htm?ownerId=10
 Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
 X-lori-time-1: 1182364402968
 Content-Type: application/x-www-form-urlencoded
 Content-Length: 40
 name=Noddy&birthDate=1000-11-11&typeId=5
 HTTP/1.x 302 Moved Temporarily
 Server: Apache-Coyote/1.1
 Pragma: No-cache
 Expires: Thu, 01 Jan 1970 00:00:00 GMT
 Cache-Control: no-cache, no-store
 Location: http://localhost:8080/petclinic/owner.htm?ownerId=10
 Content-Language: en-US
 Content-Length: 0
 Date: Wed, 20 Jun 2007 18:33:23 GMT

因?yàn)槲覀儧](méi)有為addPet.htm配置ETag,所以不設(shè)置相關(guān)的響應(yīng)頭。現(xiàn)在,我們?cè)俅卧L問(wèn)Owener 10,注意相應(yīng)中的ETag成為了1:


----------------------------------------------------------
 http://localhost:8080/petclinic/owner.htm?ownerId=10

 GET /petclinic/owner.htm?ownerId=10 HTTP/1.1
 Host: localhost:8080
 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
 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
 Accept-Language: en-us,en;q=0.5
 Accept-Encoding: gzip,deflate
 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
 Keep-Alive: 300
 Connection: keep-alive
 Referer: http://localhost:8080/petclinic/addPet.htm?ownerId=10
 Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
 X-lori-time-1: 1182364403109
 If-Modified-Since: Wed, 20 Jun 2007 18:29:03 GMT
 If-None-Match: "-1"

 HTTP/1.x 200 OK
 Server: Apache-Coyote/1.1
 Etag: "1"
 Last-Modified: Wed, 20 Jun 2007 18:33:36 GMT
 Content-Type: text/html;charset=ISO-8859-1
 Content-Language: en-US
 Content-Length: 4317
 Date: Wed, 20 Jun 2007 18:33:45 GMT

最后,我們?cè)俅握?qǐng)求Owener 10,這次ETag起了作用,我們接受到了一個(gè)304未改變信息。

----------------------------------------------------------
 http://localhost:8080/petclinic/owner.htm?ownerId=10

 GET /petclinic/owner.htm?ownerId=10 HTTP/1.1
 Host: localhost:8080
 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
 Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
 Accept-Language: en-us,en;q=0.5
 Accept-Encoding: gzip,deflate
 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
 Keep-Alive: 300
 Connection: keep-alive
 Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
 X-lori-time-1: 1182364493500
 If-Modified-Since: Wed, 20 Jun 2007 18:33:36 GMT
 If-None-Match: "1"

 HTTP/1.x 304 Not Modified
 Server: Apache-Coyote/1.1
 Date: Wed, 20 Jun 2007 18:34:55 GMT

如此,我們使用HTTP緩存降低了帶寬占用,縮短了處理周期。
The Fine Print: 事實(shí)上,采用更細(xì)粒度的對(duì)象變化跟蹤,例如使用對(duì)象標(biāo)識(shí)。可以更大程度的提高效率。但是,頁(yè)面和實(shí)體之間的關(guān)聯(lián)很大程度上是由系統(tǒng)中的數(shù)據(jù)模型設(shè)計(jì)決定的。上面的實(shí)現(xiàn)(ModifiedObjectTracker)是一個(gè)說(shuō)明性的例子,謎底是為更深入的嘗試提供思路。上面的實(shí)現(xiàn)的目的不是應(yīng)用于實(shí)際的生產(chǎn)環(huán)境中(例如不適用于集群環(huán)境),一種更遠(yuǎn)的考慮是使用數(shù)據(jù)庫(kù)的觸發(fā)器跟蹤數(shù)據(jù)變化,讓攔截器監(jiān)測(cè)觸發(fā)器輸出結(jié)果所在的數(shù)據(jù)表。

結(jié)論

我們已經(jīng)看到了使用ETag降低貸款占用和縮短處理周期的兩種方法。我所希望的是這篇文章為你現(xiàn)在和將來(lái)的Web應(yīng)用項(xiàng)目提供了一種思路,以及對(duì)底層的ETag響應(yīng)頭的正確理解和使用。
正如牛頓所說(shuō),“如果我看得更遠(yuǎn),那是因?yàn)槲艺驹诰奕说募绨蛏?#8221;。作為REST的核心,這種風(fēng)格的應(yīng)用程序講的是簡(jiǎn)單、優(yōu)雅的軟件設(shè)計(jì),不重復(fù)發(fā)明輪子。我相信了解和使用REST風(fēng)格的架構(gòu)的核心是主流應(yīng)用程序開(kāi)發(fā)的一個(gè)好的發(fā)展,并且我盼望著在以后的開(kāi)發(fā)中能夠抬起它的未來(lái)。

關(guān)于作者
Gavin Terrill是BPS的CTO。從事Java企業(yè)級(jí)應(yīng)用開(kāi)發(fā)20年以上。現(xiàn)在依然拒絕發(fā)布他的TRS-80。在空閑時(shí)間,Gavin喜歡航行、釣魚(yú)、吉他和一飲而盡高質(zhì)量的紅葡萄酒(不一定要按這個(gè)順序來(lái))。

感謝
感謝我的同事 Patrick Bourke 和Erick Dorvale對(duì)本文的反饋。