很多時候,開發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. 對象依賴注入是針對所有方法的,注入粒度還是較粗。不能做到具體方法訪問具體的自定義上下文屬性;
以上三種方法,在前後端統一的時候是好用的,但是如果遇到前後端分離的時候,就會變得很痛苦!