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