爲什麼不應該重寫service方法?

故事通常是這樣開始的:
從前,有一個程序猿,他語重心長地對孫子說:“孩子,要是你以後寫servlet,最好不要重寫service方法啊”
孫子大爲不解,程序猿又說:“聽爺爺的,準沒錯,爺爺的爺爺就是這麼說的……”

——爲什麼不應該重寫service方法呢?

如果你也曾思考過這個問題,但暫時無解,這篇文章或許可以給你一點啓發。

先來看一個具體的例子:

當時我正在osc看紅薯的一篇大作,只見我右手F12熟練的打開了chrome的開發者工具,左手迅猛的按了幾下F5,然後看到了這個結果。

聰明的你一定已經發現,除了第一個名爲12_77118的請求返回狀態爲200,其他的都爲304,那麼200和304有什麼區別呢?這個稍後解釋。

一切從代碼裏面來,我們先拋開理論,看一個具體的code:

我編寫了一個index.html,如下:

01 <html>
02 <body>
03 <h3>I'm a test page . </h3>
04 <h3>I'm a test page . </h3>
05 <h3>I'm a test page . </h3>
06 <h3>I'm a test page . </h3>
07 <h3>I'm a test page . </h3>
08 <h3>I'm a test page . </h3>
09 <h3>I'm a test page . </h3>
10 </body>
11 </html>


我們來訪問這個頁面看看。

Image(2)

這是我第一次訪問這個頁面(表示本地並沒有對這個文件的緩存):

我們來看看http請求和響應的消息頭:

《圖:一》

爲了作爲對比,我們再F5刷新一次:

《圖:二》

這次請求的頭信息中多了一條If-Modified-Since,而且返回的響應中,狀態變爲了304,這是怎麼回事?還記得紅薯那篇文章頁中的304麼,你會發現,304多出現在對於靜態資源的請求上面。

原來對於靜態資源來說:

1. 當瀏覽器第一次發起請求時(請求頭中沒有If-Modified-Since),server會在響應中告訴瀏覽器這個資源最後修改的時間(響應頭中的Last-Modified)。(見圖一)

2. 瀏覽器也很聰明,當你再次(點擊鏈接,或者F5,或者回車,但是不能是ctrl+F5)請求這個資源時,瀏覽器會詢問server這個資源自上次告訴我的最後修改時間以來有沒有被修改(請求頭中If-Modified-Since)。(見圖二)

3. 如果資源沒有被修改,server返回304狀態碼,並不會再次將資源發送給瀏覽器,瀏覽器則很知趣的使用本地的緩存文件。(見圖二)

所以所有的靜態資源如果沒有發生變化,通常是不會傳遞多次的,不管什麼瀏覽器或者server都應該遵守這種詢問的約定。看起來很爽啊,很智能是不是?這種約定的機制就是 http緩存協商——這是約定優於配置的又一體現。

有了緩存協商的知識,理解爲什麼我們不應該重寫service就很容易了。還是從代碼出發,這次我們看一個複雜一點的例子:

在這個例子中,我們請求一個控制器(MeServlet),然後轉向一個視圖(index.html),爲了簡單起見,web.xml中將只有這個servlet的配置:

01 <web-app>
02 <servlet>
03 <servlet-name>me</servlet-name>
04 <servlet-class>com.me.web.MeServlet</servlet-class>
05 </servlet>
06 <servlet-mapping>
07 <servlet-name>me</servlet-name>
08 <url-pattern>/test</url-pattern>
09 </servlet-mapping>
10 </web-app>

然後是MeServlet:

01 public class MeServlet extends HttpServlet {
02 @Override
03 protected void service(HttpServletRequest req, HttpServletResponse res)
04 throws ServletException, IOException {
05 /**
06 * 1. 處理具體的業務:
07 * -- 處理請求參數
08 * -- 檢查緩存
09 * -- 處理具體數據
10 * -- 更新緩存
11 */
12 doBizLogic(req, res);
13 /**
14 * 2. 根據處理的結果轉向具體的視圖:
15 * -- 這裏假設就是 index.html
16 */
17 getServletContext()
18 .getRequestDispatcher("/index.html").include(req, res);
19 }
20 public void doBizLogic(HttpServletRequest request, HttpServletResponse response) {
21 System.out.println("do biz.");
22 }
23 }

可以看到,每次F5刷新返回的狀態碼都是200,讓我們看看具體的請求和響應頭:

我們發現無論我們如何刷新頁面,每次響應狀態都是200,index.html的內容每次都被完整的發送給瀏覽器,這看起來很笨,爲什麼不像靜態資源一樣進行緩存協商呢?原因是緩存協商是基於http請求和響應頭中的Modified信息的,如果沒有這個信息,是無法進行緩存協商的。而對於動態內容而言,server無法幫我們決定內容是不是有改變,也無法替我們決定動態內容的最後修改時間。

所以它不會幫我們在響應中加上Last-Modified,我們必須自己來做這件事,我們小小地修改一下MeServlet:

01 public class MeServlet extends HttpServlet {
02 @Override
03 protected long getLastModified(HttpServletRequest req) {
04 /**
05 * 這裏你要自己決定動態內容的最後修改時間,例如你可以返回
06 * -- 數據緩存最後更新的時間
07 * -- 簡單起見,我們假設最後的修改時間是 1000
08 */
09 return 1000;
10 }
11 @Override
12 protected void service(HttpServletRequest req, HttpServletResponse res)
13 throws ServletException, IOException {
14 /**
15 * 1. 處理具體的業務:
16 * -- 處理請求參數
17 * -- 檢查緩存
18 * -- 處理具體數據
19 * -- 更新緩存
20 */
21 doBizLogic(req, res);
22 /**
23 * 2. 根據處理的結果轉向具體的視圖:
24 * -- 這裏假設就是 index.html
25 */
26 getServletContext()
27 .getRequestDispatcher("/index.html").include(req, res);
28 }
29 public void doBizLogic(HttpServletRequest request, HttpServletResponse response) {
30 System.out.println("do biz.");
31 }
32 }

你會看到getLastModified這個方法是重寫的,說明HttpServlet中已經有了這個方法,我們使用這個方法來告訴server在這個動態資源中,最後內容變化的時間是多少。最理想的情況是server會自己回調這個方法,那就太省心啦。

我們先訪問的看看:發現依然每次都是200,server沒有告訴瀏覽器最後的修改時間,緩存協商機制無法工作。

先別沮喪,忘了我們要解釋什麼問題嗎——爲什麼不要重寫service方法。也許你已經猜到了,如果你看看service方法的實現,現在你已經明白了,service方法自己實現了緩存協商的機制,如果我們重寫它,反而將這中良好的機制給去掉了。

我們再修改一下,這次我們重寫doGet,在doGet中完成完全相同的邏輯:

01 public class MeServlet extends HttpServlet {
02 @Override
03 protected long getLastModified(HttpServletRequest req) {
04 /**
05 * 這裏你要自己決定動態內容的最後修改時間,例如你可以返回
06 * -- 數據緩存最後更新的時間
07 * -- 簡單起見,我們假設最後的修改時間是 1000
08 */
09 return 1000;
10 }
11 @Override
12 protected void doGet(HttpServletRequest req, HttpServletResponse res)
13 throws ServletException, IOException {
14 /**
15 * 1. 處理具體的業務:
16 * -- 處理請求參數
17 * -- 檢查緩存
18 * -- 處理具體數據
19 * -- 更新緩存
20 */
21 doBizLogic(req, res);
22 /**
23 * 2. 根據處理的結果轉向具體的視圖:
24 * -- 這裏假設就是 index.html
25 */
26 getServletContext()
27 .getRequestDispatcher("/index.html").include(req, res);
28 }
29 public void doBizLogic(HttpServletRequest request, HttpServletResponse response) {
30 System.out.println("do biz.");
31 }
32 }

這次在訪問,

終於出現了久違的Last-Modified,再次回車請求頁面,哈哈變成304了。

現在你也許已經清楚了,爲什麼不應該重寫service方法,似乎是爲了保留HttpServlet默認實現的緩存協商的機制;其實還有另外一個原因:就是禁用你沒有在servlet中重寫的方法,例如post、head等,這樣就從一定程度上提高了安全性。

理論到此爲止,現在讓我們來看看緩存協商機制有什麼實際的好處:

還是紅薯的那邊文章,我們現在全加載(ctrl+F5)一次看看,

我們看到總共發起了45個請求,請求的數據量爲198.93KB,然後F5刷新一次:

這次只有36個請求,數據量只有23.62KB

我們看到這篇文章被9960個id訪問, 而每一個id實際上可能訪問這個頁面多次(像我這樣,實際的數據可能得問問紅薯),然後我們看到很多304靜態資源都是整站通用的:

如果你是osc的常客,並且不經常更換瀏覽器,不經常清理緩存,甚至其他人的頭像都可以是通用的,爲了簡單起見,我們這裏考慮每個id都只訪問這個頁面一次,並且假設所有的資源都已經緩存在用戶本地,得出:

(198.93-23.62)×9960 = 1746086.6KB = 1705.1637M = 1.665G。

很驚人吧,這只是一個頁面,別忘了,我們還假設所有的用戶都只訪問一次,你想想osc上面有多少篇博文,加起來……

流量是什麼,是銀子啊。

幸運的是,這些省銀子的事情瀏覽器和server都已經幫我們做好了,那我們就不需要關心這個了嗎??我們看到12_77118這個請求所佔用的資源也不少,如果文章再長點,再長點的話……還會更大。

如果紅薯願意,也可以讓這個請求實現緩存協商,可以進一步減少流量。

當然這裏的計算並不是完全的精確,實際的情況複雜很多,但是這個計算的量級應該是對的,是值得參考的。

流量涉及的另一個問題就是帶寬,以更小的貸款提供更高的併發是每個站長應該追求的。不過考慮到osc以新聞爲主,一次性消費,所以……不過那時題外話了。

好了,如果你有耐心看到這裏,我想你也許會對service有了新的理解,爲什麼我們不應該重寫這個方法。

萬事有例外,如果你需要實現一個前端控制器的話,就是另外一回事了,這留給大家自己思考。

第一次發文,大家開拍吧,:)~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章