導(dǎo)言
REST方式的應(yīng)用程序構(gòu)架在近日所產(chǎn)生的巨大影響突出了Web應(yīng)用程序的優(yōu)雅設(shè)計(jì)的重要性。現(xiàn)在人們開始理解“WWW架構(gòu)”內(nèi)在的可測量性及彈 性,并且已經(jīng)開始探索使用其范例的更好的方式。在本文中,我們將討論一個(gè)Web應(yīng)用開發(fā)工具——“簡陋的、卑下的”ETags,以及如何在基于 SpringFramework的動(dòng)態(tài)Web應(yīng)用程序中集成這個(gè)工具,來提高應(yīng)用的性能及可測性。
我們將要使用的基于Spring的應(yīng)用程序是基于“petclinic”(寵物門診?)的一個(gè)應(yīng)用。在您下載的程序包中,包含了如何加入必要的配置和源代碼讓你親自體驗(yàn)該程序的介紹。
什么是ETag
?
在HTTP協(xié)議規(guī)范中,ETag被定義為“被請求的變量的實(shí)體值”。(
參見 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - Section 14.19。)換句話說,ETag是一個(gè)與Web資源相關(guān)聯(lián)的標(biāo)記。典型的Web資源是一個(gè)Web頁面,但也可以是一個(gè)JSON格式或者XML格式的文檔。服務(wù)器可以指出一個(gè)標(biāo)記是什么及其意義,并將這個(gè)標(biāo)記放在HTTP頭重傳送給客戶端。
ETag如何提高應(yīng)用程序性能
ETag和一個(gè)GET請求的“If-None-Match”頭信息一起使用,服務(wù)器開發(fā)者以此來使用客戶端緩存的優(yōu)勢。服務(wù)器在客戶端的一次請求時(shí)
產(chǎn)生ETag,并在以后的請求中判斷被請求資源是否發(fā)生了變化。確切的說,客戶端將這個(gè)標(biāo)記傳回給服務(wù)器,來驗(yàn)證它自己的緩存是否有效。
整個(gè)處理過程如下:
客戶端請求頁面A
服務(wù)器響應(yīng),返回頁面A,附加ETag
客戶端顯示A,并將頁面和ETag一并緩存
客戶端再次請求頁面A,請求中包含了上次請求頁面A時(shí)返回的ETag
服務(wù)器檢查客戶端發(fā)送過來的ETag,并確定頁面A在該客戶端上次請求后到現(xiàn)在沒有發(fā)生過變化,因此,發(fā)送一個(gè)304(未改變)響應(yīng)頭給客戶端,附帶一個(gè)空的響應(yīng)體。
文章的剩余部分將討論在基于SpringFramework的使用SpringMVC的Web應(yīng)用程序中使用ETag兩種方式。首先,我們將通過一 個(gè)Servlet2.3 過濾器,使用由計(jì)算請求返回結(jié)果的MD5值而產(chǎn)生的ETag(一個(gè)簡單的ETag實(shí)現(xiàn))。第二種方式使用一種更加“專業(yè)”的方式通過跟蹤頁面呈現(xiàn)所用到的 模型的變化來確定ETag的有效性(一個(gè)“專業(yè)”的ETag實(shí)現(xiàn))。雖然我們在這里使用了Spring MVC,但這個(gè)技術(shù)適用于其他任何的MVC框架。
在繼續(xù)之前,我們有必要明確,ETag技術(shù)是為了希望改進(jìn)動(dòng)態(tài)產(chǎn)生的頁面的訪問速度而提出的。作為一個(gè)完整的性能優(yōu)化方案和性能分析,其他的性能優(yōu)化技術(shù)依然應(yīng)當(dāng)被考慮。
自頂向下的Web緩存
本文首先討論將HTTP緩存技術(shù)應(yīng)用于動(dòng)態(tài)頁面。尋求Web應(yīng)用程序優(yōu)化方案時(shí),我們應(yīng)當(dāng)采用一個(gè)完整的,自頂向下的步驟。從根本上說,理解HTTP請求的過程是很重要的,采用哪種具體的技術(shù)取決于你在什么場合。例如:
Apache可以放在你的Servlet容易之前,來接受如圖片,js請求,同時(shí)也可以使用FileETag指令產(chǎn)生ETag響應(yīng)頭。
使用Javascript優(yōu)化技術(shù),例如將多個(gè)js文件合并,并去除空格等無用信息。
利用GZip和Cache-Control響應(yīng)頭。
使用JamonPerformanceMonitorInterceptor確定你的Spring應(yīng)用系統(tǒng)中的性能瓶頸。
確定你充分地使用了ORM工具的緩存機(jī)制,從而使得實(shí)體信息不是頻繁的從數(shù)據(jù)庫中重新加載。搞清楚如何讓查詢緩存很好的工作需要一定的時(shí)間。
確保盡量少聰數(shù)據(jù)庫中重新加載數(shù)據(jù),特別是一些大的列表。大列表應(yīng)當(dāng)被按頁分割,對每一頁的請求返回大列表的一個(gè)小的子集。
Session中保存盡量少的信息。這降低了內(nèi)存要求,在建立應(yīng)用層集群時(shí)將會(huì)顯得非常有用。
使用一個(gè)數(shù)據(jù)庫調(diào)試工具,確定查詢時(shí)使用了哪些索引,查詢時(shí)數(shù)據(jù)表將不會(huì)被鎖定。
當(dāng)然了,性能優(yōu)化的最佳格言是適用的:測量兩次,切割一次。(多次測試后再修改)
等等,上面的話是對木匠說的,但雖然如此,它一樣適用于我們!
?一個(gè)內(nèi)容主體ETag過濾器
我們將看到的第一種方式是建立一個(gè)Servlet過濾器基于頁面內(nèi)容(MVC中的View)來產(chǎn)生ETag標(biāo)記。乍一看,使用這種方式對性能的提升 似乎沒什么大的作用。服務(wù)器依然需要聲稱頁面,并且增加了計(jì)算標(biāo)記值的時(shí)間。但是,在這里我們的目的是減少帶寬占用。這對于很多的反應(yīng)時(shí)間很長的情形是一 個(gè)很大的益處,例如如果你的應(yīng)用的服務(wù)器和客戶端分別在地球的不同半球上。我曾看到一個(gè)從東京發(fā)出的對紐約的某臺(tái)服務(wù)器的請求,響應(yīng)長達(dá)350毫秒。考慮 并發(fā)用戶因素后,這將成為一個(gè)重大的瓶頸。
代碼
我們用于產(chǎn)生標(biāo)記的技術(shù)是計(jì)算頁面返回內(nèi)容的MD5值。創(chuàng)建一個(gè)響應(yīng)包裝器將完成這個(gè)工作。包裝器使用一個(gè)字節(jié)數(shù)組來保存返回內(nèi)容,在過濾器鏈處理完成之后,我們計(jì)算這個(gè)字節(jié)數(shù)組的MD5哈希值。
doFilter方法的實(shí)現(xiàn)如下:

?2

?3

?4

?5

?6

?7

?8

?9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

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

?2

?3

?4

?5

?6

?7

?8

?9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

在Web.xml中調(diào)用這個(gè)過濾器是很簡單的:

2

3

4

5

6

7

8

9

Listing 3: Configuration of the filter in web.xml.
每一個(gè)htm文件將被EtagContentFilter過濾,如果該文件在上次請求后沒有發(fā)生變化,則返回一個(gè)空的HTTP響應(yīng)體。
上面討論的方式對于確定類型的頁面很有用,但也有一些缺點(diǎn)。
頁面在服務(wù)器段生成之后,在返回給客戶端之前,我們計(jì)算了ETag值,如果ETag匹配,那么我們實(shí)在是沒有必要去取出模型數(shù)據(jù),因?yàn)殇秩境鰜淼捻撁鎸⒉粫?huì)返回給客戶端。
對于在頁腳呈現(xiàn)日期和時(shí)間的頁面,每次請求都是不同的,即使頁面的主題內(nèi)容并沒有發(fā)生改變。
下面,我們將看到另一種可選的方法——通過理解構(gòu)建頁面的底層數(shù)據(jù)來解決上面的限制帶來的問題。
ETag攔截器
Spring MVC中的HTTP請求傳遞途徑包含了一種可以在控制器處理請求之前插入一個(gè)攔截器的能力。這對于插入ETag對比邏輯來說是一個(gè)極其合適的切入點(diǎn),在這里,如果發(fā)現(xiàn)構(gòu)建頁面的數(shù)據(jù)沒有發(fā)生變化,我們就可以停止更進(jìn)一步的處理。
這
里的訣竅是如何知道構(gòu)建所請求的頁面的數(shù)據(jù)沒有發(fā)生變化。為了本文的目的,我創(chuàng)建了一個(gè)簡單的ModifiedObjectTracker,通過
Hiberante事件監(jiān)聽器來跟蹤新增、更新、刪除操作。跟蹤器將為每一個(gè)頁面保持一個(gè)為一個(gè)數(shù)字,以及一個(gè)影響到該頁面的持久化實(shí)體的Map。如果一
個(gè)POJO發(fā)生了變化,那么一個(gè)技術(shù)其將增加所有用到了這個(gè)POJO的頁面對應(yīng)的數(shù)字。將這個(gè)數(shù)字作為ETag,當(dāng)客戶端將ETag返回時(shí),我們將會(huì)知道
一個(gè)頁面所用到的模型是否發(fā)生了變化。
代碼
從ModifiedObjectTracker開始:
2?????void?notifyModified(>?String?entity);
3?}?
很簡單吧?它的實(shí)現(xiàn)會(huì)比較有意思。每當(dāng)一個(gè)實(shí)體發(fā)生了變化,我們?yōu)槊恳粋€(gè)用到了該實(shí)體的頁面更新對應(yīng)的計(jì)數(shù)器。
?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??}?
一次“變化”就是一次新增、修改或者刪除操作。下面是針對刪除操作的處理器列表(作為事件監(jiān)聽器配置在Hibernate 3 LocalSessionFactoryBean中)。
??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將通過Spring配置注射到DeleteHandler中。同時(shí),將會(huì)有一個(gè)SaveOrUpdateHandler處理實(shí)體的新增和修改。
如果客戶端發(fā)回了一個(gè)當(dāng)前有效的ETag(意思是內(nèi)容在上次請求后未曾發(fā)生改變),我們將阻止更多的處理邏輯,以實(shí)現(xiàn)我們的性能提升。在Spring MVC中,可以使用一個(gè)HandlerInterceptorAdaptor ,并重寫preHandle方法:
?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請求(ETag可以在客戶端發(fā)出PUT請求時(shí)驗(yàn)證更新是否沖突,但那已經(jīng)超出了本文的范圍)。如果
標(biāo)記和服務(wù)器上次返回的標(biāo)記相匹配,則返回一個(gè)304位發(fā)生改變響應(yīng),并繞過后面的處理鏈。否則,我們設(shè)置一個(gè)ETag響應(yīng)頭,以備客戶端下次請求同樣的
頁面。
可以看到,我將產(chǎn)生標(biāo)記的邏輯抽象出來形成了一個(gè)接口,如此我們則可以使用不同的標(biāo)記生成策略。該接口只有一個(gè)方法:
??String?getToken(HttpServletRequest?request);
}?
為了少列出一些代碼,我的SampleTokenFactory實(shí)現(xiàn)同時(shí)承擔(dān)了ETagTokenFactory的任務(wù)。如此,我們簡單的將被請求的URL的修改次數(shù)作為標(biāo)記返回。
??String?view?=?request.getRequestURI();
??Integer?count?=?counts.get(view);
??if?(count?==?null)?{
???return?null;
??}
???return?count.toString();
?}?
就這樣!
討論
在這里,我們的攔截器將在沒有相關(guān)數(shù)據(jù)發(fā)生變化時(shí)阻止一切收集數(shù)據(jù)和渲染頁面的處理過程。現(xiàn)在,讓我們來看一下HTTP頭,以及在表象之下到底發(fā)生了些什么。示例程序中包含了使得owner.htm使用ETag的配置介紹。
第一次請求說明用戶已經(jīng)看到了該頁面:
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)槲覀儧]有為addPet.htm配置ETag,所以不設(shè)置相關(guān)的響應(yīng)頭。現(xià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
最后,我們再次請求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ì)粒度的對象變化跟蹤,例如使用對象標(biāo)識。可以更大程度的提高效率。但是,頁面和實(shí)體之間的關(guān)聯(lián)很大程度上是由系統(tǒng)中的數(shù)據(jù)模型設(shè)計(jì)決定
的。上面的實(shí)現(xiàn)(ModifiedObjectTracker)是一個(gè)說明性的例子,謎底是為更深入的嘗試提供思路。上面的實(shí)現(xiàn)的目的不是應(yīng)用于實(shí)際的生
產(chǎn)環(huán)境中(例如不適用于集群環(huán)境),一種更遠(yuǎn)的考慮是使用數(shù)據(jù)庫的觸發(fā)器跟蹤數(shù)據(jù)變化,讓攔截器監(jiān)測觸發(fā)器輸出結(jié)果所在的數(shù)據(jù)表。
結(jié)論
我們已經(jīng)看到了使用ETag降低貸款占用和縮短處理周期的兩種方法。我所希望的是這篇文章為你現(xiàn)在和將來的Web應(yīng)用項(xiàng)目提供了一種思路,以及對底層的ETag響應(yīng)頭的正確理解和使用。
正
如牛頓所說,“如果我看得更遠(yuǎn),那是因?yàn)槲艺驹诰奕说募绨蛏稀薄W鳛镽EST的核心,這種風(fēng)格的應(yīng)用程序講的是簡單、優(yōu)雅的軟件設(shè)計(jì),不重復(fù)發(fā)明輪子。我
相信了解和使用REST風(fēng)格的架構(gòu)的核心是主流應(yīng)用程序開發(fā)的一個(gè)好的發(fā)展,并且我盼望著在以后的開發(fā)中能夠抬起它的未來。