WEB 應用通常會引入 Session,用來在服務端和客戶端之間保存一系列動作/消息的狀態(tài),比如網上購物維護 user 登錄信息直到 user 退出。在 user 登錄后,Session 周期里有很多 action 都需要從 Session 中得到 user,再驗證身份權限,或者進行其他的操作。這其中就會涉及到程序去訪問 Session屬性的問題。在java中,Servlet 規(guī)范提供了 HttpSession對象來滿足這種需求。開發(fā)人員可以從 HttpServletRquest對象得到 HttpSession,再從HttpSession中得到狀態(tài)信息。
還是回到購物車的例子,假設在 controller 某個方法(本文簡稱為action)中我們要從HttpSession中取到user對象。如果基于Servlet,標準的代碼會是這樣的:
這樣的代碼在傳統(tǒng)的Servlet程序中是很常見的:因為使用了 Servlet API,從而對 Servlet API產生依賴。這樣如果我們要測試 action,我們就必須針對 HttpServletRequest、HttpServletResponse 和 HttpSession類提供 mock 或者 stub 實現(xiàn)。當然現(xiàn)在已經有很多開源的 Servlet 測試框架幫助我們減輕這個痛苦,包括 Spring 就自帶了對了這些類的 stub 實現(xiàn),但那還是太冗繁瑣碎了。那有沒有比較好的辦法來讓我們的 controller 更 POJO,讓我們的 action 脫離 Servlet API 依賴,更有益于測試和復用呢?我們來看看在 Spring2.5 中訪問 Session 屬性的幾種策略,并將在本博的后續(xù)文章繼續(xù)探究解決方案選擇后面的深層含義。
(一)通過方法參數傳入HttpServletRequest對象或者HttpSession對象 筆者的前一篇文章已經簡單介紹了Spring2.5的annotation使得 controller 擺脫了 Servlet API 對方法參數的限制,這里就不贅述了。有興趣的同學可以參考這里。Spring對annotationed的 action 的參數提供自動綁定支持的參數類型包括 Servlet API 里面的 Request/Response/HttpSession(包含Request、Response在Servlet API 中聲明的具體子類)。于是開發(fā)人員可以通過在 action 參數中聲明 Request 對象或者 HttpSession 對象,來讓容器注入相應的對象。
action 的代碼如下:
優(yōu)點: 1. 程序中直接得到底層的 Request/HttpSession 對象,直接使用 Servlet API 規(guī)范中定義的方法操作這些對象中的屬性,直接而簡單。 2. action 需要訪問哪些具體的 Session 屬性,是由自己控制的,真正精確到 Session 中的每個特定屬性。 不足: 1. 程序對 Servlet API 產生依賴。雖然 controller 類已經不需要從 HttpServlet 繼承,但仍需要 Servlet API 才能完成編譯運行,乃至測試。 2. 暴露了底層 Servlet API,暴露了很多并不需要的底層方法和類,開發(fā)人員容易濫用這些 API。
(二)通過定制攔截器(Interceptor)在controller類級別注入需要的User對象 Interceptor 是 Spring 提供的擴展點之一,SpringMVC 會在 handle 某個 request 前后調用在配置中定義的 Interceptor 完成一些切面的工作,比如驗證用戶權限、處理分發(fā)等,類似于 AOP。那么,我們可以提取這樣一個“橫切點”,在 SpringMVC 調用 action 前,在 Interceptor 的 preHandle 方法中給 controller 注入 User 成員變量,使之具有當前登錄的 User 對象。
此外還需要給這些特定 controller 聲明一類 interface,比如 IUserAware。這樣開發(fā)人員就可以只針對這些需要注入 User 對象的 controller 進行注入增強。
IUserAware 的代碼:
controller 的代碼:
Interceptor 的代碼:
為了讓 SpringMVC 能調用我們定義的 Interceptor,我們還需要在 SpringMVC 配置文件中聲明該 Interceptor,比如:
優(yōu)點: 1. 對 Servlet API 的訪問被移到了自 SpringMVC API 擴展的 Interceptor,controller 不需要關心 User 如何得到。 2. 開發(fā)人員可以通過隨時添加或移除 Interceptor 來完成對不同參數在某一類型 controller 上的注入。 3. controller 的 User 對象通過外界注入,測試時開發(fā)人員可以很容易地注入自己想要的 User 對象。 4. controller 類去掉了對 Servlet API 的依賴,更 POJO 和通用。 5. controller 類是通過對 interface 的聲明來輔助完成注入的,并不存在任何繼承依賴。 不足: 1. SpringMVC 對 controller 默認是按照單例(singleton)處理的,在 controller 類中添加一個成員變量,可能會引起多線程的安全問題。 2. 因為 User 對象是定義為 controller 的成員變量,而且是通過 setter 注入進來,在測試時需要很小心地保證對controller 注入了 User 對象,否則有可能我們拿到的就不一定是一個“好公民”(Good Citizen)。
其實,一言而蔽之,這些不足之所以出現(xiàn),是因為我們把某個 action 級別需要的 User 對象上提到 controller 級別,破壞了 the convention of stateless for controller classes,而 setter 方式的注入又帶來了一些隱含的繁瑣和不足。當然,我們可以通過把 controller 聲明為“prototype”來繞過 stateless 的約定,也可以保證每次 new 一個 controller 的同時給其注入一個 User 對象。但是我們有沒有更簡單更 OO 的方式來實現(xiàn)呢?答案是有的。
(三)通過方法參數處理類(MethodArgumentResolver)在方法級別注入User對象 正如前面所看到的,SpringMVC 提供了不少擴展點給開發(fā)人員擴展,讓開發(fā)人員可以按需索取,plugin 上自定義的類或 handler。那么,在 controller 類的層次上,SpringMVC 提供了 Interceptor 擴展,在 action 上有沒有提供相應的 handler 呢?如果我們能夠對 action 實現(xiàn)注入,出現(xiàn)的種種不足了。
通過查閱 SpringMVC API 文檔,SpringMVC 其實也為 action 級別提供了方法參數注入的 Resolver 擴展,允許開發(fā)人員給 HandlerMapper 類 set 自定義的 MethodArgumentResolver。
Resolver 的代碼如下:
配置文件的相關配置如下:
優(yōu)點: 1. 具備第二種方案的所有優(yōu)點 2. 真正做到了按需分配,只在真正需要對象的位置注入具體的對象,減少其他地方對該對象的依賴。 3. 其他人能很容易地從 action 的參數列表得知 action 所需要的依賴,API 更清晰易懂。 4. 對于很多 action 需要的某一類參數,可以在唯一的設置點用很方便一致的方式進行注入。 不足: 1. 對象依賴注入是針對所有 action, 注入粒度還是較粗。不能做到具體 action 訪問具體的 Session 屬性
(四)通過 SpringMVC 的 SessionAttributes Annotation 關聯(lián) User 屬性 SpringMVC 文檔提到了 @SessionAttributes annotation,和 @ModelAttribute 配合使用可以往 Session 中存或者從 Session 中取指定屬性名的具體對象。文檔里說;
The type-level @SessionAttributes annotation declares session attributes used by a specific handler. This will typically list the names of model attributes which should be transparently stored in the session or some conversational storage, serving as form-backing beans between subsequent requests.
很明顯,@SessionAttributes 是用來在 controller 內部共享 model 屬性的。從文檔自帶的例子來看,標注成 @SessionAttributes 屬性的對象,會一直保留在 Session 或者其他會話存儲中,直到 SessionStatus 被顯式 setComplete()。那這個 annotation 對我們有什么幫助呢?
答案就是我們可以在需要訪問 Session 屬性的 controller 上加上 @SessionAttributes,然后在 action 需要的 User 參數上加上 @ModelAttribute,并保證兩者的屬性名稱一致。SpringMVC 就會自動將 @SessionAttributes 定義的屬性注入到 ModelMap 對象,在 setup action 的參數列表時,去 ModelMap 中取到這樣的對象,再添加到參數列表。只要我們不去調用 SessionStatus 的 setComplete() 方法,這個對象就會一直保留在 Session 中,從而實現(xiàn) Session 信息的共享。
controller的代碼如下:
使用這種方案,還需要在 SpringMVC 配置文件的 ViewResolver 定義處,加上 p:allowSessionOverride="true",這樣如果你對 User 對象做了修改,SpringMVC 就會在渲染 View 的同時覆寫 Session 中的相關屬性。
優(yōu)點: 1. 具備第二種方案的所有優(yōu)點 2. 使用 Annotation 聲明對 Session 特定屬性的存取,每個 action 只需要聲明自己想要的 Session 屬性。 3. 其他人能很容易地從 action 的參數列表得知 action 所需要的依賴,API 更清晰易懂。 不足: 1. 對于相同屬性的 Session 對象,需要在每個 action 上定義。 2. 這種方案并不是 SpringMVC 的初衷,因此有可能會引起一些爭議。
縱觀這四類方法,我們可以看出我們對 Session 屬性的訪問控制設置,是從所有 Servlet,到某一類型的 controller 的成員變量,到所有 action 的某一類型參數,再到具體 action 的具體對象。每種方案都有各自的優(yōu)點和不足:第一種方案雖然精確,但可惜引入了對 Servlet API 的依賴,不利于 controller 的測試和邏輯復用。第二、三種方案雖然解決了對 Servlet API 的依賴,也分別在 controller 和 action 級別上提供了對 Session 屬性的訪問,但注入粒度在一定程度上還是不夠細,要想對具體屬性進行訪問可能會比較繁瑣。不過,這在另一方面也提供了簡便而統(tǒng)一的方法來對一系列相同類型的參數進行注入。第四種方案通過使用 Annotation,不僅擺脫了 Servlet API 的依賴,而且在 action 級別上提供了對 Session 具體屬性的訪問控制。但是這種訪問有可能會粒度過細,需要在很多不同 action 上聲明相同的 annotation。而且,畢竟這種用法并不是 SpringMVC 的初衷和推薦的,可能會帶來一些爭議。
Powered by: BlogJava Copyright © mingj
本blog原創(chuàng)文字只代表本人某一時間內的觀點或結論,與本人所在公司沒有任何關系。第三方若用于商業(yè)用途的轉載,須取得本人授權。一般的引用、轉載請標明出處!