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 {
User user = (User)req.getSession().getAttribute("currentUser"); // } 这样的代码在传统的Servlet程序中是很常见的:因为使用了 Servlet API,从而对 Servlet API产生依赖。这样如果我们要测试 action,我们就必须针对 HttpServletRequest、HttpServletResponse 和 HttpSession类提供 mock 或者 stub 实现。当然现在已经有很多开源的 Servlet 测试框架帮助我们减轻这个痛苦,包括 Spring 就自带了对了这些类的 stub 实现,但那还是太冗繁琐碎了。那有没有比较好的办法来让我们的 controller 更 POJO,让我们的 action 脱离 Servlet API 依赖,更有益于测试和复用呢?我们来看看在 Spring2.5 中访问 Session 属性的几种策略,并将在本博的后续文章继续探究解决方案选择后面的深层含义。
action 的代码如下: @RequestMapping
public void hello(HttpSession session){ User user = (User)session.getAttribute("currentUser"); // } 优点: (二)通过定制拦截器(Interceptor)在controller类级别注入需要的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> 优点: 其实,一言而蔽之,这些不足之所以出现,是因为我们把某个 action 级别需要的 User 对象上提到 controller 级别,破坏了 the convention of stateless for controller classes,而 setter 方式的注入又带来了一些隐含的繁琐和不足。当然,我们可以通过把 controller 声明为“prototype”来绕过 stateless 的约定,也可以保证每次 new 一个 controller 的同时给其注入一个 User 对象。但是我们有没有更简单更 OO 的方式来实现呢?答案是有的。 (三)通过方法参数处理类(MethodArgumentResolver)在方法级别注入User对象 通过查阅 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> 优点: (四)通过 SpringMVC 的 SessionAttributes Annotation 关联 User 属性
很明显,@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() } // }
优点: 纵观这四类方法,我们可以看出我们对 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 的准则或者原则在发挥着效应,鉴于本文篇幅已经较长,就留到后续文章中继续探讨解决方案选择背后的深层含义,敬请期待。 |
|