很多时候,开发Web应用时各种前后台交互让人很烦闷,尤其是各种权限验证啦,购物车商品信息啦等等……
大家第一时间想到的是: 采用HttpSession来存这些对象.然后就是各种参数从Controller传到Service再传到持久层方法.一直传一直传.
在现阶段需求变化极快的前提下,如果架构没事先想好各种参数的传递,很容易导致我们需要的一些参数要通过方法层层传递才能用到.为什么不想一个简单点的,直接通过自定义上下文轻松拿到的方法来实现我们随时随地获取这些对象的方式呢?
改变传统的参数层层传递,让大家写出简洁优美的代码是我的最终理想.所以我做了一点试验,仅供参考.
首先,写一个自定义的简单上下文接口,也可以称之为简单的对象缓存接口.接口加上实现类,大约不到200行代码,很轻松.
然后就是让我们的上下文存点东西.这里要注意在多线程环境下的对象隔离.如果不是前后端分离的做法,可以采用ThreadLocal办到,一个工具类搞定.如果是前后端分离的方式,导致的线程无法跟踪的问题,我们在后面讨论.
接下来就是把自定义的上下文接口用起来.我们先来传统的.大概三种做法:
一. 通过方法参数传入HttpServletRequest对象或者HttpSession对象
自Spring2.5的annotation使得 controller 摆脱了 Servlet API 对方法参数的限制,这里就不赘述了.Spring对annotationed的 action 的参数提供自动绑定支持的参数类型包括 Servlet API 里面的 Request/Response/HttpSession(包含Request、Response在Servlet API 中声明的具体子类)。于是开发人员可以通过在 controller 的方法参数中声明 Request 对象或者 HttpSession 对象,来让容器注入相应的对象。
例如:
@RequestMapping
public void hello(HttpSession session){
User user = (User)session.getAttribute("currentUser");
}
优点:
1. 程序中直接得到底层的 Request/HttpSession 对象,直接使用 Servlet API 规范中定义的方法操作这些对象中的属性,直接而简单。
2. controller 需要访问哪些具体的 Session 属性,是由自己控制的,真正精确到 Session 中的每个特定属性。
缺点:
1. 程序对 Servlet API 产生依赖。虽然 controller 类已经不需要从 HttpServlet 继承,但仍需要 Servlet API 才能完成编译运行,乃至测试。
2. 暴露了底层 Servlet API,暴露了很多并不需要的底层方法和类,开发人员容易滥用这些 API。
二. 通过定制拦截器(Interceptor)在controller类级别注入需要的上下文对象
Interceptor 是 Spring 提供的扩展点之一,SpringMVC 会在 handle 某个 request 前后调用在配置中定义的 Interceptor 完成一些切面的工作,比如验证用户权限、处理分发等,类似于 AOP。那么,我们可以提取这样一个“横切点”,在 SpringMVC 调用方法前,在 Interceptor 的 preHandle 方法中给 controller 注入上下文成员变量,使之具有自定义上下文对象。
此外还需要给这些特定 controller 声明一类 interface,比如 IContextAware。这样开发人员就可以只针对这些需要注入自定义上下文对象的 controller 进行注入增强。
IContextAware接口:
public interface IContextAware {
public void setContext(MyApplicationContext context);
}
UserController类:
@Controller
@RequestMapping(value="/user")
@Scope(value="prototype")
public class UserController implements IContextAware{
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private IUserService service;
private MyApplicationContext context;
@Override
public void setContext(MyApplicationContext context) {
this.context = context;
}
@RequestMapping(value="/get/{id}")
@ResponseBody
public User getUser(@PathVariable("id") String id){
log.info("Find user with id={}", id);
User user = null;
try {
user = service.findUserById(id);
if (user != null) {
context.setAttribute("currentUser", user);
}
} catch (Exception e) {
e.printStackTrace();
}
return (User) context.getAttribute("currentUser");
}}
HandlerInterceptor实现类:
public class MyInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(MyInterceptor.class);
private static final String MY_APPLICATION_CONTEXT = "MYAPPLICATIONCONTEXT";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
IContextAware controllerAware = null;
HandlerMethod handlerMethod = (HandlerMethod) handler;
Class<?> clazz = handlerMethod.getBeanType();
Type[] interfaces = clazz.getGenericInterfaces();
if (interfaces != null && interfaces.length > 0) {
for (int i = 0; i < interfaces.length; i++) {
if (interfaces[i] == IContextAware.class) {
controllerAware = (IContextAware) handlerMethod.getBean();
}
}
}
HttpSession session = request.getSession();
if (session == null) {
session = request.getSession(true);
}
log.info("当前HttpSession的sessionId={}", session.getId());
MyApplicationContext context = (MyApplicationContext) session.getAttribute(MY_APPLICATION_CONTEXT);
if (context == null) {
context = new MyApplicationContextImpl();
session.setAttribute(MY_APPLICATION_CONTEXT, context);
}
log.info("当前自定义上下文对象hashcode={}", context.hashCode());
controllerAware.setContext(context);
return true;
}……
}
为了让 SpringMVC 能调用我们定义的 Interceptor,我们还需要在 SpringMVC 配置文件中声明该 Interceptor
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="interceptors">
<list>
<ref bean="myContextInterceptor"/>
</list>
</property>
</bean>
<bean id="myContextInterceptor" class="net.fantesy84.interceptor.MyInterceptor"></bean>
优点:
1. 对 Servlet API 的访问被移到了自 SpringMVC API 扩展的 Interceptor,controller 不需要关心自定义上下文对象如何得到。
2. 开发人员可以通过随时添加或移除 Interceptor 来完成对不同参数在某一类型 controller 上的注入。
3. controller 的自定义上下文对象通过外界注入,测试时开发人员可以很容易地注入自己想要的自定义上下文对象。
4. controller 类去掉了对 Servlet API 的依赖,更贴近 POJO 和通用。
5. controller 类是通过对 interface 的声明来辅助完成注入的,并不存在任何继承依赖。
缺点:
1. SpringMVC 对 controller 默认是按照单例(singleton)处理的,在 controller 类中添加一个成员变量,可能会引起多线程的安全问题。不过我们可以通过声明@Scope(value="prototype")来解决;
2. 因为自定义上下文对象是定义为 controller 的成员变量,而且是通过 setter 注入进来,在测试时需要很小心地保证对controller 注入了自定义上下文对象,否则有可能我们拿到的就不一定是一个“好公民”(Good Citizen)。
三. 通过方法参数处理类(MethodArgumentResolver)在方法级别注入自定义上下文对象
正如前面所看到的,SpringMVC 提供了不少扩展点给开发人员扩展,让开发人员可以按需索取,plugin 上自定义的类或 handler。那么,在 controller 类的层次上,SpringMVC 提供了 Interceptor 扩展,在 action 上有没有提供相应的 handler 呢?如果我们能够对方法实现注入,出现的种种不足了。
通过查阅 SpringMVC API 文档,SpringMVC 其实也为方法级别提供了方法参数注入的 Resolver 扩展,允许开发人员给 HandlerMapper 类 set 自定义的 MethodArgumentResolver。
UserController类:
@Controller
@RequestMapping(value="/user")
@Scope(value="prototype")
public class UserController{
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private IUserService service;
@RequestMapping(value="/get/{id}")
@ResponseBody
public User getUser(@PathVariable("id") String id, MyApplicationContext context){
log.info("Find user with id={}", id);
User user = null;
try {
user = service.findUserById(id);
if (user != null) {
context.setAttribute("currentUser", user);
}
} catch (Exception e) {
e.printStackTrace();
}
return (User) context.getAttribute("currentUser");
}}
WebArgumentResolver接口实现类:
public class ContextArgResolver implements WebArgumentResolver {
private static final String MY_APPLICATION_CONTEXT = "MYAPPLICATIONCONTEXT";
@Override
public Object resolveArgument(MethodParameter methodParameter,NativeWebRequest webRequest) throws Exception {
if (methodParameter.getParameterType() == MyApplicationContext.class) {
return webRequest.getAttribute(MY_APPLICATION_CONTEXT, RequestAttributes.SCOPE_SESSION);
}
return UNRESOLVED;
}}
配置文件的相关配置如下:
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jacksonConverter"/>
</list>
</property>
<property name="customArgumentResolvers">
<list>
<ref bean="contextArgResolver"/>
</list>
</property>
</bean>
<bean id="contextArgResolver" class="net.fantesy84.resolver.ContextArgResolver"></bean>
<bean id="jacksonConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
优点:
1. 具备第二种方案的所有优点
2. 真正做到了按需分配,只在真正需要对象的位置注入具体的对象,减少其他地方对该对象的依赖。
3. 其他人能很容易地从方法的参数列表得知方法所需要的依赖,API 更清晰易懂。
4. 对于很多方法需要的某一类参数,可以在唯一的设置点用很方便一致的方式进行注入。
不足:
1. 对象依赖注入是针对所有方法的,注入粒度还是较粗。不能做到具体方法访问具体的自定义上下文属性;
以上三种方法,在前后端统一的时候是好用的,但是如果遇到前后端分离的时候,就会变得很痛苦! 针对前后端分离的处理方式,我们下一篇再详细的剖析.