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