spring3.X mvc 使用Session属性的策略
本文部分内容参考至:http://anffvf.blog.163.com/blog/static/314754201101342148699/
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 属性的几种策略,并将在本博的后续文章继续探究解决方案选择后面的深层含义。
(一)通过方法参数传入HttpServletRequest对象或者HttpSession对象
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.暴露了底层ServletAPI,暴露了很多并不需要的底层方法和类,开发人员容易滥用这些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类去掉了对ServletAPI的依赖,更POJO和通用。
5. controller 类是通过对 interface 的声明来辅助完成注入的,并不存在任何继承依赖。不足:
1. SpringMVC 对 controller 默认是按照单例(singleton)处理的,在 controller 类中添加一个成员变量,可能会引起多线程的安全问题。
2.因为User对象是定义为controller的成员变量,而且是通过setter注入进来,在测试时需要很小心地保证对controller注入了User对象,否则有可能我们拿到的就不一定是一个“好公民”(GoodCitizen)。
其实,一言而蔽之,这些不足之所以出现,是因为我们把某个action级别需要的User对象上提到controller级别,破坏了theconventionofstatelessforcontrollerclasses,而setter方式的注入又带来了一些隐含的繁琐和不足。当然,我们可以通过把controller声明为“prototype”来绕过stateless的约定,也可以保证每次new一个controller的同时给其注入一个User对象。但是我们有没有更简单更OO的方式来实现呢?答案是有的。
(三)通过方法参数处理类(MethodArgumentResolver)在方法级别注入User对象
正如前面所看到的,SpringMVC 提供了不少扩展点给开发人员扩展,让开发人员可以按需索取,plugin 上自定义的类或 handler。那么,在 controller 类的层次上,SpringMVC 提供了 Interceptor 扩展,在 action 上有没有提供相应的 handler 呢?如果我们能够对 action 实现注入,出现的种种不足了。
通过查阅SpringMVCAPI文档,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 中取指定属性名的具体对象。文档里说;
Thetype-level@SessionAttributesannotationdeclaressessionattributesusedbyaspecifichandler.Thiswilltypicallylistthenamesofmodelattributeswhichshouldbetransparentlystoredinthesessionorsomeconversationalstorage,servingasform-backingbeansbetweensubsequentrequests.
很明显,@SessionAttributes是用来在controller内部共享model属性的。从文档自带的例子来看,标注成@SessionAttributes属性的对象,会一直保留在Session或者其他会话存储中,直到SessionStatus被显式setComplete()。那这个annotation对我们有什么帮助呢?
答案就是我们可以在需要访问Session属性的controller上加上@SessionAttributes,然后在action需要的User参数上加上@ModelAttribute,并保证两者的属性名称一致。SpringMVC就会自动将@SessionAttributes定义的属性注入到ModelMap对象,在setupaction的参数列表时,去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的具体对象。每种方案都有各自的优点和不足:第一种方案虽然精确,但可惜引入了对ServletAPI的依赖,不利于controller的测试和逻辑复用。第二、三种方案虽然解决了对ServletAPI的依赖,也分别在controller和action级别上提供了对Session属性的访问,但注入粒度在一定程度上还是不够细,要想对具体属性进行访问可能会比较繁琐。不过,这在另一方面也提供了简便而统一的方法来对一系列相同类型的参数进行注入。第四种方案通过使用Annotation,不仅摆脱了ServletAPI的依赖,而且在action级别上提供了对Session具体属性的访问控制。但是这种访问有可能会粒度过细,需要在很多不同action上声明相同的annotation。而且,毕竟这种用法并不是SpringMVC的初衷和推荐的,可能会带来一些争议。
本文演示了 Spring2.5 访问 Session 属性的几种不同解决方案,并分析了各自的优点和不足。本文并不打算对这些解决方案评出对错,只是试图列出在选择方案时的思维过程以及选择标准。每种方案都能满足某一类上下文的需求,在特定的开发环境和团队中都可能会是最优的选择。但是笔者还是发现,整个过程中,一些平常容易忽视的 OOP 的准则或者原则在发挥着效应,鉴于本文篇幅已经较长,就留到后续文章中继续探讨解决方案选择背后的深层含义,敬请期待。