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