WEB 應用通常會引入 Session用來在服務端和客戶端之間保存一系列動作/消息的狀態比如網上購物維護 user 登錄信息直到 user 退出在 user 登錄後Session 周期裡有很多 action 都需要從 Session 中得到 user再驗證身份權限或者進行其他的操作這其中就會涉及到程序去訪問 Session屬性的問題在java中Servlet 規范提供了 HttpSession對象來滿足這種需求開發人員可以從 HttpServletRquest對象得到 HttpSession再從HttpSession中得到狀態信息
還是回到購物車的例子假設在 controller 某個方法(本文簡稱為action)中我們要從HttpSession中取到user對象如果基於Servlet標准的代碼會是這樣的
Java代碼
public void doGet(HttpServletRequest req HttpServletResponse resp) throws ServletException IOException {
User user = (User)reqgetSession()getAttribute(currentUser);
//
}
這樣的代碼在傳統的Servlet程序中是很常見的因為使用了 Servlet API從而對 Servlet API產生依賴這樣如果我們要測試 action我們就必須針對 HttpServletRequestHttpServletResponse 和 HttpSession類提供 mock 或者 stub 實現當然現在已經有很多開源的 Servlet 測試框架幫助我們減輕這個痛苦包括 Spring 就自帶了對了這些類的 stub 實現但那還是太冗繁瑣碎了那有沒有比較好的辦法來讓我們的 controller 更 POJO讓我們的 action 脫離 Servlet API 依賴更有益於測試和復用呢?我們來看看在 Spring 中訪問 Session 屬性的幾種解決方案並將在本博的後續文章繼續探究解決方案選擇背後的深層含義
(一)通過方法參數傳入HttpServletRequest對象或者HttpSession對象
筆者的前一篇文章已經簡單介紹了Spring的annotation使得 controller 擺脫了 Servlet API 對方法參數的限制這裡就不贅述了有興趣的同學可以參考<a >這裡</a>Spring對annotationed的 action 的參數提供自動綁定支持的參數類型包括 Servlet API 裡面的 Request/Response/HttpSession(包含RequestResponse在Servlet API 中聲明的具體子類)於是開發人員可以通過在 action 參數中聲明 Request 對象或者 HttpSession 對象來讓容器注入相應的對象
action 的代碼如下
Java代碼
@RequestMapping
public void hello(HttpSession session){
User user = (User)sessiongetAttribute(currentUser);
//
}
優點
程序中直接得到底層的 Request/HttpSession 對象直接使用 Servlet API 規范中定義的方法操作這些對象中的屬性直接而簡單
action 需要訪問哪些具體的 Session 屬性是由自己控制的真正精確到 Session 中的每個特定屬性
不足
程序對 Servlet API 產生依賴雖然 controller 類已經不需要從 HttpServlet 繼承但仍需要 Servlet API 才能完成編譯運行乃至測試
暴露了底層 Servlet API暴露了很多並不需要的底層方法和類開發人員容易濫用這些 API
(二)通過定制攔截器(Interceptor)在controller類級別注入需要的User對象
Interceptor 是 Spring 提供的擴展點之一SpringMVC 會在 handle 某個 request 前後調用在配置中定義的 Interceptor 完成一些切面的工作比如驗證用戶權限處理分發等類似於 AOP那麼我們可以提取這樣一個橫切點在 SpringMVC 調用 action 前在 Interceptor 的 preHandle 方法中給 controller 注入 User 成員變量使之具有當前登錄的 User 對象
此外還需要給這些特定 controller 聲明一類 interface比如 IUserAware這樣開發人員就可以只針對這些需要注入 User 對象的 controller 進行注入增強
IUserAware 的代碼
Java代碼
public interface IUserAware {
public void setUser();
}
controller 的代碼
Java代碼
@Controller
public GreetingController implements IUserAware {
private User user;
public void setUser(User user){
thisuser = user;
}
@RequestMapping
public void hello(){
//usersayHello();
}
//
}
Interceptor 的代碼
Java代碼
public class UserInjectInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest HttpServletResponse httpServletResponse Object handler) throws Exception {
if (handlerisAssignableFrom(IUserAware)){
User user = (User)()getAttribute(currentUser);
IUserAware userAware = (IUserAware) handler;
userAwaresetUser(user);
}
return superpreHandle(httpServletRequest httpServletResponse handler);
}
//
}
為了讓 SpringMVC 能調用開發人員定義的 Interceptor我們還需要在 SpringMVC 配置文件中聲明該 Interceptor比如
Xml代碼
<bean class=orgspringframeworkwebservletmvcannotationDefaultAnnotationHandlerMapping>
<property name=interceptors>
<list>
<ref bean=userInjectInterceptor/><! userInjectInterceptor bean 的聲明省略>
</list>
</property>
</bean>
優點
對 Servlet API 的訪問被移到了自 SpringMVC API 擴展的 Interceptorcontroller 不需要關心 User 如何得到
開發人員可以通過隨時添加或移除 Interceptor 來完成對不同參數在某一類型 controller 上的注入
controller 的 User 對象通過外界注入測試時開發人員可以很容易地注入自己想要的 User 對象
controller 類去掉了對 Servlet API 的依賴更 POJO 和通用
controller 類是通過對 interface 的聲明來輔助完成注入的並不存在任何繼承依賴
不足
SpringMVC 對 controller 默認是按照單例(singleton)處理的在 controller 類中添加一個成員變量可能會引起多線程的安全問題
因為 User 對象是定義為 controller 的成員變量而且是通過 setter 注入進來在測試時需要很小心地保證對controller 注入了 User 對象否則有可能我們拿到的就不一定是一個好公民(Good Citizen)
其實一言而蔽之這些不足之所以出現是因為我們把某個 action 級別需要的 User 對象上提到 controller 級別破壞了 the convention of stateless for controller classes而 setter 方式的注入又帶來了一些隱含的繁瑣和不足當然我們可以通過把 controller 聲明為prototype來繞過 stateless 的約定也可以保證每次 new 一個 controller 的同時給其注入一個 User 對象但是我們有沒有更簡單更 OO 的方式來實現呢?答案是有的
(三)通過方法參數處理類(MethodArgumentResolver)在方法級別注入User對象
正如前面所看到的SpringMVC 提供了不少擴展點給開發人員擴展讓開發人員可以按需索取plugin 上自定義的類或 handler那麼在 controller 類的層次上SpringMVC 提供了 Interceptor 擴展在 action 上有沒有提供相應的 handler 呢?如果我們能夠對 action 實現注入出現的種種不足了
通過查閱 SpringMVC API 文檔SpringMVC 其實也為 action 級別提供了方法參數注入的 Resolver 擴展允許開發人員給 HandlerMapper 類 set 自定義的 MethodArgumentResolver
action 的代碼如下
Java代碼
@RequestMapping
public void hello(User user){
//usersayHello()
}
Resolver 的代碼如下
Java代碼
public class UserArgumentResolver implements WebArgumentResolver {
public Object resolveArgument(MethodParameter methodParameter NativeWebRequest webRequest) throws Exception {
if (methodParametergetParameterType()equals(Userclass)) {
return webRequestgetAttribute(currentUser RequestAttributesSCOPE_SESSION);
}
return UNRESOLVED;
}
}
配置文件的相關配置如下
Xml代碼
<bean class=orgspringframeworkwebservletmvcannotationOwnAnnotationMethodHandlerAdapter>
<property name=customArgumentResolver>
<ref bean=userArgumentResolver/><! userArgumentResolver bean 的定義省略 >
</property>
</bean>
優點
具備第二種方案的所有優點
真正做到了按需分配只在真正需要對象的位置注入具體的對象減少其他地方對該對象的依賴
其他人能很容易地從 action 的參數列表得知 action 所需要的依賴API 更清晰易懂
對於很多 action 需要的某一類參數可以在唯一的設置點用很方便一致的方式進行注入
不足
對象依賴注入是針對所有 action 注入粒度還是較粗不能做到具體 action 訪問具體的 Session 屬性
(四)通過 SpringMVC 的 SessionAttributes Annotation 關聯 User 屬性
SpringMVC 文檔提到了 @SessionAttributes annotation和 @ModelAttribute 配合使用可以往 Session 中存或者從 Session 中取指定屬性名的具體對象文檔裡說
引用
The typelevel @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 formbacking 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 中從而實現 Session 信息的共享
controller的代碼如下
Java代碼
@Controller
@SessionAttributes(currentUser)
public class GreetingController{
@RequestMapping
public void hello(@ModelAttribute(currentUser) User user){
//usersayHello()
}
//
}
使用這種方案還需要在 SpringMVC 配置文件中在定義 ViewResolver 時加上 p:allowSessionOverride=true這樣如果你對 User 對象做了修改就會在渲染 View 的同時覆寫 Session 中的相關屬性
優點
具備第二種方案的所有優點
使用 Annotation 聲明對 Session 特定屬性的存取每個 action 只需要聲明自己想要的 Session 屬性
其他人能很容易地從 action 的參數列表得知 action 所需要的依賴API 更清晰易懂
不足
對於相同屬性的 Session 對象需要在每個 action 上定義
這種方案並不是 SpringMVC 的初衷因此有可能會引起一些爭議
縱觀這四類方法我們可以看出我們對 Session 屬性的訪問控制設置是從所有 Servlet到某一類型的 controller 的成員變量到所有 action 的某一類型參數再到具體 action 的具體對象每種方案都有各自的優點和不足第一種方案雖然精確但可惜引入了對 Servlet API 的依賴不利於 controller 的測試和邏輯復用第二三種方案雖然解決了對 Servlet API 的依賴也分別在 controller 和 action 級別上提供了對 Session 屬性的訪問但注入粒度在一定程度上還是不夠細要想對具體屬性進行訪問可能會比較繁瑣不過這在另一方面也提供了簡便而統一的方法來對一系列相同類型的參數進行注入第四種方案通過使用 Annotation不僅擺脫了 Servlet API 的依賴而且在 action 級別上提供了對 Session 具體屬性的訪問控制但是這種訪問有可能會粒度過細需要在很多不同 action 上聲明相同的 annotation而且畢竟這種用法並不是 SpringMVC 的初衷和推薦的可能會帶來一些爭議
本文演示了 Spring 訪問 Session 屬性的幾種不同解決方案並分析了各自的優點和不足本文並不打算對這些解決方案評出對錯只是試圖列出在選擇方案時的思維過程以及選擇標准每種方案都能滿足某一類上下文的需求在特定的開發環境和團隊中都可能會是最優的選擇但是筆者還是發現整個過程中一些平常容易忽視的 OOP 的准則或者原則在發揮著效應鑒於本文篇幅已經較長就留到後續文章中繼續探討解決方案選擇背後的深層含義
From:http://tw.wingwit.com/Article/program/Java/ky/201311/28209.html