原帖地址:http://blog.csdn.net/roderick2015/article/details/52908594,转载请注明。
在上篇帖子 SpringMVC源码之解读DispatcherServlet初始化流程 中介绍了DispatchServlet在为我们服务前做了哪些准备,今天接着看它是怎么处理Request请求的。
首先回顾一下DispatchServlet的继承体系,在请求的处理过程中,涉及到了HttpServlet、FrameworkServlet和DispatchServlet,如下图所示。
我们编写Servlet处理Http请求时需要继承HttpServlet,因为在它的service方法中会把Web容器中获取到的HttpServletRequest和HttpServletResponse对象分发到doGet、doPost、doPut等七个方法中进行处理,我们只需覆写对应的方法即可,如覆写doGet方法来处理get类型的请求。
在FrameworkServlet中同样覆写了除doHead外的所有处理方法,但它还做了另外两件事:
1.在覆写的service方法中添加了对patch类型请求的支持。
2.所有类型的请求都交由processRequest方法统一处理。
其中patch是对put类型的补充,侧重局部数据的更新,由于引入较晚在Java的Servlet中没有提供支持。
这样做的意义是什么?个人认为HttpServlet中分成七个方法处理是为了给我们提供细粒度的扩展,可以单独对某一类型的方法进行扩展,而FrameworkServlet先对这几个方法覆写,再统一到processRequest中使用模板方式处理,是为了保持之前的扩展性不受影响,以及对用户的透明性(你该咋弄还是咋弄,方式不变)。
接着看下processRequest的代码(只贴出部分流程代码,下同),如下所示。
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
initContextHolders(request, localeContext, requestAttributes);
/**
* 上面第4~8行代码做了两件事:
* 1.使用ThreadLocal获取当前线程的LocaleContext(本地化信息,如zh-CN)和
* RequestAttributes(后面用来操作request和session的属性),保存起来等请求处理完毕后,
* 在resetContextHolders方法中还原。
* 2.创建当前单次请求的LocaleContext和RequestAttributes。
*
* 第10行代码则是把刚创建的LocaleContext和RequestAttributes与当前线程绑定(ThreadLocal.set())
*
* 这里面涉及到两个问题需要理解
* 1.还原操作干嘛用的?
* 首先ThreadLocal是跟线程绑定的,只要线程不销毁,数据就一直在。
* 而线程从Tomcat的线程池中取出来,到干完活放回去的整个请求处理过程中,DispatchServlet只是其中的一环,
* 期间可能会涉及到其他地方(比如常见的Filter)对这些信息的操作,所以这个信息对外部来说可能是有用的。
* 粗暴点理解就是,我的地盘我做主,不影响别人的数据,别人也甭影响我,自己创建自己用,怎么来的还怎么走。
*
* 2.为什么要线程绑定?
* 因为Servlet在Tomcat中是单例的,所以是多个线程操作同一块代码,而想创建只对当前Request(处理线程)有效
* 的数据,使用ThreadLocal是一个不错的选择。比如上面LocaleContextHolder的getLocaleContext方法是静态的,
* 这就意味着你可以随时调用它来获取返回值,线程绑定后,你完全可以在Controller或者Service等处理逻辑代码
* 的地方直接获取,给我们编码提供了便利性。
*/
try {
//抽象方法,由子类实现,典型的模板方法模式
doService(request, response);
}
catch () {
}
finally {
//还原操作,同时也起到了解除线程绑定的作用
resetContextHolders(request, previousLocaleContext, previousAttributes);
}
}
DispatchServlet中涉及的核心方法只有两个:doService和doDispatch,先看doService方法,代码如下所示。
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
//下面就是向request中放入容器和组件,给后面的请求处理做准备。
//可以看出SpringMVC的数据传递就是围绕request和response来设计的,包括之前的线程绑定
//这也是为什么我们可以在逻辑层使用request和response做很多事情。
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
//默认情况下getThemeSource()调用的是getWebApplicationContext()
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
//这部分是给redirect重定向传参准备的,详见文末扩展部分。
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
try {
//真正的请求处理部分
doDispatch(request, response);
}
finally {
}
}
接下来是doDispatch方法,在正式看代码前得先弄清下面几个概念:
1.Handler
处理器,request请求最终由它来处理,比如我们在Controller层中用@RequestMapping注释的方法就属于Handler,在SpringMVC中Handler是非常灵活的,它可以是方法也可以是类,甚至是其它的形式,因为它的类型是Object — 所有类的父类,就是说Handler可以是任何类型,比常用的接口抽象要更进一步。
2.HandlerInterceptor
Handler拦截器,它定义了三个方法:preHandle、postHandle和afterCompletion,也就是在执行Handler之前、之后及处理完成后分别调用这三个方法做统一的处理,比如SpringMVC会默认添加ConversionServiceExposingInterceptor,在执行Handler之前将request中的参数转换成我们在方法中定义的类型,类似于Spring中的AOP,这么好用的东西当然不会忘了用户啦,我们可以在配置文件中注册自己的拦截器。
3.HandlerAdapter
Handler适配器,刚刚提到Handler非常灵活,它可以是任意类型,但也不能无法无天啊,整个流程的结构可是固定的,那谁来约束它呢?这就是HandlerAdapter要做的事情,由HandlerAdapter调用Handler来处理request。把Handler比作工具的话,那HandlerAdapter就是使用它的人,工具再灵活再好用,咱用的不顺手那也白瞎。
4.ModelAndView
如果把Handler对应MVC模式中的C,那ModelAndView对应的就是M和V,它包含了数据和视图两部分。在HandlerAdapter调用Handler处理完请求后,返回的就是ModelAndView对象,这也意味着处理方法的返回值类型,可以是视图(如JSP页面名称)或者数据(如JSON数据),甚至视图和数据(创建ModelAndView对象)一起返回,为我们处理返回结果提供了很大的灵活性。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
//处理文件上传请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
//找到处理当前request的Handler
mappedHandler = getHandler(processedRequest);
//再根据Handler找到HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
//根据文件的最近修改时间判断是否返回304状态码,默认修改时间返回-1即不支持该缓存方式。
//如果想使用需要另外配置
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//处理前先执行拦截器的preHandle方法,该方法会返回一个boolean值
//如果是true,则交给后面的Interceptor继续处理,返回false则表示自己处理完了,
//包括response也搞定了,后面的不用麻烦了直接做中断处理。
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//HandlerAdapter调用Handler处理请求
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//判断是否需要viewNameTranslator从request里获取view的名字(针对在handler的返回值里没有给出的情况)
applyDefaultViewName(request, mv);
//处理完后执行拦截器的PostHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
//做最后的处理工作,比如页面渲染,调用拦截器的afterCompletion方法等
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
}
finally {
//清理文件上传留下的临时文件
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
看完代码我们再梳理一下在doDispatch方法中做了哪些事情,而DispatchServlet中初始化的九大组件又是如何使用的,每个步骤对应的组件关系如下图所示。
另外还使用了HandlerExceptionResolver来处理流程中抛出的异常,而FlashMapManager在涉及到重定向处理时都有使用,这样一看是不是九大组件都到齐了。doDispatch方法中的代码并不多,而且步骤清晰容易理解,但它却包含了request处理的整个流程。
我们知道九大组件都是接口,也就是说doDispatch方法中定义的是一套结构固定的抽象流程,而每个接口有多少实现类,之间又存在什么联系有什么功能都由具体的对象去处理,从DispatchServlet的初始化到请求的处理,设计思想都是一致的,提供丰富功能的同时也提供了灵活的扩展以及一个职责分明、步骤清晰的顶层脉络,这种设计方式非常值得我们学习。
再进一步就是看这些组件具体是怎么工作的,我会在下面的帖子中对几个比较关键的组件进行分析。
《SpringMVC源码之HandlerMapping与HandlerAdapter组件分析》(还没写。。。)
《SpringMVC源码之ViewResolver组件分析》(还没写。。。)
首先请求会在HttpServlet中分发到七个doXXX()方法中处理,而FrameworkServlet覆写了这些方法并在processRequest中统一处理。processRequest方法执行了请求数据还原、线程绑定等操作,接着调用DispatchServlet的doService方法,添加了webContext、Resolver以及FlashMapManager以备后面使用,最后调用了doDispatch方法。doDispatch定义了处理请求的抽象流程,调用九大组件执行具体的操作。
在DispatchServlet类的doService方法中涉及到了重定向参数的处理,既然碰到了咱就来了解FlashMapManager组件是如何处理重定向的。
先看下FlashMapManager长什么样,代码如下所示。
public interface FlashMapManager {
//这两个方法都是围绕FlashMap来操作的,FlashMap就是一个HashMap,用来存储重定向参数的。
//从session中取出参数并放入FlashMap中
FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response);
//将FlashMap中的参数放入session中
void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response);
}
也就是说FlashMapManager只是定义了两个保存和取出重定向参数的方法,并通过session作为中介存储。流程就是先把参数以固定的key存入session,然后把这个session的id作为cookie返回给浏览器, 浏览器在下次跳转请求的时候把id还给你,FlashMapManager再通过这个固定的key从session中取出参数,这些很好理解,下面我们来更进一步了解里面的操作。
回到DispatcherServlet的doService方法,redirect重定向代码部分如下所示。
//所以这句代码就是取出重定向参数,而且在取出后会将该参数从session中移除,
//取参的固定key是FLASH_MAPS_SESSION_ATTRIBUTE
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
//以INPUT_FLASH_MAP_ATTRIBUTE为key作为request的属性传递,最后会被放入Model中供我们使用
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
//这里的key与上句代码正好相反。
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
下面我们在Controller类中加上下面两个方法,实现跳转功能:让请求从welcome方法跳转到newPage方法中处理。
@RequestMapping(value={"/"}, method = RequestMethod.GET)
public String welcome(RedirectAttributes redirectAttributes, HttpServletRequest request, HttpServletResponse response) {
//添加跳转参数,传递到newPage方法中接收
redirectAttributes.addAttribute("title", "july");
redirectAttributes.addFlashAttribute("msg", "跳转页面");
return "redirect:newPage";
}
@RequestMapping(value={"/newPage"}, method = RequestMethod.GET)
public String newPage(String title, Model model, HttpServletRequest request) {
Map map = model.asMap();
return "newPage";
}
在welcome方法中使用了两种方式来传递参数,其中addAttribute添加的title会拼接到URL中传递,而addFlashAttribute添加的msg才会放入session中作为重定向参数传递,那具体是怎么实现的呢?
首先看看RedirectAttributes接口的继承体系,如下图所示。
其中Model接口定义了对Attribute的操作方法,而实现类RedirectAttributesModelMap则继承了LinkedHashMap,这样就好理解了,addAttribute和addFlashAttribute不过是使用HashMap添加了两个键值对,但是它俩添加的值可没有放在一个地方。如下图所示,在类中定了一个私有对象flashAttributes,使用addFlashAttribute方法添加的值放在了这个Map中,只有接口RedirectAttributes定义的getFlashAttributes方法才能获取这个map的值。
之后title作为普通参数放入ModelAndView传递,并在RedirectView类的createTargetUrl方法中进行拼接,由于需要在URL中拼接,所以传入的value必须能转换为String类型,否则将抛出异常,部分代码截图如下所示。
msg则放入了request的属性中(key值为OUTPUT_FLASH_MAP_ATTRIBUTE),在AbstractFlashMapManager类的saveOutputFlashMap方法中以List< FlashMap >的格式放进了Session中(key值为FLASH_MAPS_SESSION_ATTRIBUTE),部分代码截图如下所示。
最后在sendRedirect方法中向浏览器返回响应,回过头看下过程中涉及的key值,就能对上了。