導(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)如下:
1
public 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值。
1
public 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
}
33
Listing 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ì)本文的反饋。