最近在網上看到一篇N. Alex Rupp寫的“Beyond MVC: A New Look at the Servlet Infrastructure”文章,意思大致是說MVC被Struts等架構錯誤地應用到了Servlet架構中。我想只有對Struts有足夠的瞭解再加上在MVC方面有足夠深的功力,才敢發此言論,不是經常聽人說:最熟悉自己的人是你的敵人。本人功力尚淺,沒有引領風潮的能力,而且生活還得繼續,只能先來熟悉熟悉Struts。 申明: 強烈建議在閱讀本文之前先閱讀一下N. Alex Rupp老兄的文章,如果你贊同他的看法,可能你會覺得研究Struts就沒什麼意義了。 說明:本文所講的Struts知識基於Struts 1.1版本,除非特別說明,本文中的Struts都特指Struts 1.1這個版本。 目錄: 精細之處一:“利用Token解決重複提交”背後的前提 精細之處二:頁面流轉控制中的職責分配 精細之處一:“利用Token解決重複提交”背後的前提 我們知道,可以利用同步令牌(Token)機制來解決Web應用中重複提交的問題,Struts也給出了一個參考實現。伺服器端在處理到達的請求之前,會將請求中包含的令牌值與儲存在目前使用者會話中的令牌值進行比較,看是否匹配。在處理完該請求後,且在回覆發送給用戶端之前,將會產生一個新的令牌,該令牌除傳給用戶端以外,也會將使用者會話中儲存的舊的令牌進行替換。這樣如果使用者回退到剛才的提交頁面並再次提交的話,用戶端傳過來的令牌就和伺服器端的令牌不一致,從而有效地防止了重複提交的發生。對應於這段描述,你可能會在你的Action子類中有這麼一段代碼:
if (isTokenValid(request, true)) { // your code herereturn mapping.findForward("success");} else {saveToken(request);return mapping.findForward("submitagain");} |
其中isTokenValid()和saveToken()都是org.apache.struts.action.Action類中的方法,而具體的Token處理邏輯都在org.apache.struts.util.TokenProcessor類中。Struts中是根據使用者會話ID和當前系統時間來產生一個唯一(對於每個會話)令牌的,具體實現可以參考TokenProcessor類中的generateToken()方法。 不知道大家有沒有注意到這樣一個問題,因為Struts是將Token儲存在Session的一個屬性中,也就是說對於每個會話伺服器端只儲存而且只能儲存一個最新Token值。對於這一點,我的同事就提出了疑問:那如果我在同一個會話中開啟兩個頁面,那麼後提交的那個頁面肯定不能提交成功了。他還給出了一個實際的例子:比如現在需要把兩個客戶A和B的地址都改為某個值,那使用者就可能同時開啟兩個頁面,修改A,修改B,提交A,提交B,按照Struts中的處理邏輯,B的修改提交就肯定不能成功,但是這個提交操作對於使用者來說並不存在操作不正確的地方。 在這裡,可能有人要問:怎麼可能在同一個會話中開啟兩個頁面呢?重新開啟一個IE瀏覽器不是重新開始了一個會話嗎?不錯,這種情況下是兩個會話,不存在任何問題。但是,你還可以通過菜單“檔案”-“建立”-“視窗”(或者快速鍵Ctrl+N)來複製當前視窗,這個時候你會發現該頁面與原有頁面同處在一個會話當中。其實,能夠發現這個問題得歸功於我的那位同事對IE習慣性的操作方法。 這下我的那位同事不滿意啦,他於是開始動手修改Struts中的實現方式,讓每個頁面(至少某類頁面)在伺服器端都儲存有一個唯一的Token值。這樣,前面所講的客戶A,B同時修改的限制就不存在了。但是不久,我的那位同事就開始意識到他正在走向一條危險的道路。首先,如果每個頁面都在伺服器端儲存一個Token值,則伺服器端儲存的資料量將越來越大。而且,如果考慮這種同一個會話中開啟多個頁面的情況的話,就好像開啟了潘多拉魔盒,將會給自己帶來無窮無盡的麻煩。比如,首先開啟頁面P1,然後利用Ctrl+N得到頁面P2,P1提交,P2提交,目前為止一切正常。但是如果此時,在P1,P2中點擊[上一頁] 按鈕,然後再提交P1, P2呢,情況會是怎樣?如果在P2中提交完後執行其它操作,而在P1中回退後提交,情況又是怎麼樣呢?如果有P1,P2,P3,那情況又是如何呢?太複雜啦!我想你也會和我們有同感,你需要考慮許多種可能的組合,而且有的時候結果並不是你想象中的那樣簡單。 此路不通,還得回來看看Struts。其實經過以上一番折騰,我們可以發現在Struts中的Token機制背後隱藏著這樣一個前提:不允許你(用戶端)在同一會話中開啟多個頁面。注意是同一會話,如果開啟兩個IE瀏覽器,那已經是兩個會話啦,不受該限制。其實,這個看似不合理的規定卻自有其道理:一是它極大地簡化了Token的實現,二個這種限定也符合大部分人的使用習慣。 精細之處二:頁面流轉控制中的職責分配 我們知道,Struts的執行過程大致如下:首先,控制器接收到用戶端請求,將這些請求映射至相應的Action,並調用Action的execute方法,這中間可能還涉及到ActionForm的建立和填充。Action的execute方法執行完以後,返回一個ActionForward對象,控制器根據該ActionForward對象將請求轉寄至下一個Action或JSP。最後,產生視圖響應客戶。在大的層面上,Struts是採用了MVC這種架構,沒什麼特別之處。但從一些小的地方,我們還是可以看出Craig R. McClanahan老兄的一些考慮。我們看到Action與控制器之間傳遞的是ActionForward對象,由於Action的execute方法要求返回一個ActionForward對象,所以你會經常在Action子類中看到如下語句:
return (new ActionForward(mapping.getInput())); |
或
return (mapping.findForward("success")); |
其實返回的就是一個ActionForward對象。在Action中我們根據程式執行的不同情況,決定接下來的頁面走向(比如返回到輸入頁面或者轉到下一個頁面),並將這些資訊儲存在ActionForward對象中。而接下來控制器就可以直接利用該ActionForward對象來進行頁面的流轉。下面是org.apache.struts.action.RequestProcessor類的processForwardConfig()方法的摘錄,該方法調用發生在Action執行個體調用後。
protected void processForwardConfig(HttpServletRequest request, HttpServletResponse response, ForwardConfig forward) throws IOException, ServletException { … String forwardPath = forward.getPath(); String uri = null; // paths not starting with / should be passed through without any processing // (ie. they're absolute) if (forwardPath.startsWith("/")) { uri = RequestUtils.forwardURL(request, forward); // get module relative uri } else { uri = forwardPath; } if (forward.getRedirect()) { // only prepend context path for relative uri if (uri.startsWith("/")) { uri = request.getContextPath() + uri; } response.sendRedirect(response.encodeRedirectURL(uri)); } else { doForward(uri, request, response); } } |
注意: ForwardConfig是ActionForward的父類 該方法首先調用ForwardConfig的getPath()方法獲得下一步流轉的路徑,在某些條件下還需要進行一些拼裝得到正確的URI,最後根據該URI進行頁面跳轉。可見在processForwardConfig()方法中只是對ActionForward進行了一些“技術上”的處理,沒有任何和業務相關的內容,這樣就將控制器(ActionServlet)和Action完全分開來,兩者互不影響,達到了功能模組之間鬆散耦合的目的。 模組間(系統間)鬆散耦合一直是OO設計所追求的,但是具體如何去實現這樣一種鬆散耦合卻不是那麼容易做到的。Struts中的設計給了我們一些啟示:模組間相互關聯影響因素的傳遞可以用對象的形式來封裝起來。其實,個人覺得Struts中的做法還可以稍微有一點點改進,就是在ActionForward中提供一個getURI()方法來給出最終的URI豈不是更好? 參考: 1、Beyond MVC: A New Look at the Servlet Infrastructure 2、Allen Holub的Build user interfaces for object-oriented systems系列文章,可以從這篇文章中學到很多物件導向設計方面的知識,雖然作者並不認為MVC是一種物件導向的方法,但是我們這些MVC的實踐者仍然可以從中學到物件導向的知識。 3、Struts 1.1的介紹性文章:深入Struts 1.1 4、Apache Struts Website 5、關於重複提交問題的討論及其解決方案,可以參考《Core J2EE Patterns》一書(中文版《J2EE核心模式》)。 Deepak Alur,John Crupi,Dan Malks: Core J2EE Patterns-Best Practices and Design Strategies
|