转载请声明出处:[](https://juejin.im/editor/drafts/5a587430518825734859e45d)
前面两篇文章直接对SpringMVC里面的组件进行了源码分析,可能很多小伙伴都会觉得有点摸不着头脑。所以今天再岔回来说一说SpringMVC的核心控制器,以此为轴心来学习整个SpringMVC的知识体系。
前面在《项目开发框架-SSM》一篇文章中已经详细的介绍过了SSM项目中关于Spring的一些配置文件,对于一个Spring应用,必不可少的是:
<context-param> <param-name>contextConfigLocation</param-name> <!-- <param-value>classpath*:config/applicationContext.xml</param-value> --> <param-value>classpath:spring/applicationContext.xml</param-value> </context-param> <!-- 配置一个监听器将请求转发给 Spring框架 --> <!-- Spring监听器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
通过ContextLoadListener来完成Spring容器的初始化以及Bean的装载《 Spring技术内幕学习:Spring的启动过程 》。那么如果在我们需要提供WEB功能,则还需要另外一个,那就是SpringMVC,当然我们同样需要一个用来初始化SpringMVC的配置(初始化9大组件的过程:前面两篇《 SpringMVC源码系列:HandlerMapping 》和《 SpringMVC源码系列:AbstractHandlerMapping 》是关于HnadlerMapping的,当然不仅仅这两个,还有其他几个重要的子类,后续会持续更新):
<servlet> <servlet-name>mvc-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 配置springMVC需要加载的配置文件 spring-dao.xml,spring-service.xml,spring-web.xml Mybatis(如果有) - > spring -> springmvc --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>mvc-dispatcher</servlet-name> <!-- 默认匹配所有的请求 --> <url-pattern>*.htm</url-pattern> </servlet-mapping>
当我们在web.xml中配置好上述内容(当然还得保证咱们的Spring的配置以及SpringMVC的配置文件没有问题的情况下),启动web容器(如jetty),就可以通过在浏览器输入诸如:http://localhost:80/myproject/index.do 的方式来访问我们的应用了。
俗话说知其然,之气所以然;那么为什么在配置好相关的配置文件之后,我们就能访问我们的SSM项目了呢?从发送一条那样的请求(http://localhost:80/myproject/index.do)展示出最后的界面,这个过程在,Spring帮我们做了哪些事情呢?(SpringIOC容器的初始化在《 Spring技术内幕-容器刷新:wac.refresh 》文中已经大概的说了下大家可以参考一下)
先通过下面这张图来整个了解下SpringMVC请求处理的过程;图中从1-13,大体上描述了请求从发送到界面展示的这样一个过程。
从上面这张图中,我们可以很明显的看到有一个DispatcherServlet这样一个类,处于各个请求处理过程中的分发站。实际上,在SpringMVC中,整个处理过程的顶层设计都在这里面。通常我们将DispatcherServlet称为SpringMVC的前端控制器,它是SpringMVC中最核心的类。下面我们就来揭开DispatcherServlet的面纱吧!
OK,我们直接来看DispatcherServlet的类定义:
public class DispatcherServlet extends FrameworkServlet
DispatcherServlet继承自FrameworkServlet,就这样?
下面才是他家的族谱:
首先为什么要有绿色的部门,有的同学可能已经想到了,绿色部分不是Spring的,而是java自己的;Spring通过HttpServletBean这位年轻人成功的拥有了JAVA WEB 血统(本来Spring就是用JAVA写的,哈哈)。关于Servlet这个小伙伴可以看下我之前的文章,有简单的介绍了这个接口。
话说回来,既然DispatcherServlet归根揭底是一个Servlet,那么就肯定具有Servlet功能行为。
敲黑板!!!Servlet的生命周期是啥(init->service->destroy : 加载->实例化->服务->销毁)。
其实这里我想说的就是service这个方法,当然,在DispatcherServlet中并没有service方法,但是它有一个doService方法!(引的好难...)
doService是DispatcherServlet的入口,我们来看下这个方法:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { if (logger.isDebugEnabled()) { String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : ""; logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed + " processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]"); } // 在include的情况下保留请求属性的快照,以便能够在include之后恢复原始属性。 Map<String, Object> attributesSnapshot = null; //确定给定的请求是否是包含请求,即不是从外部进入的顶级HTTP请求。 //检查是否存在“javax.servlet.include.request_uri”请求属性。 可以检查只包含请求中的任何请求属性。 //(可以看下面关于isIncludeRequest解释) if (WebUtils.isIncludeRequest(request)) { attributesSnapshot = new HashMap<String, Object>(); Enumeration<?> attrNames = request.getAttributeNames(); while (attrNames.hasMoreElements()) { String attrName = (String) attrNames.nextElement(); if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) { attributesSnapshot.put(attrName, request.getAttribute(attrName)); } } } // 使框架可用于handler和view对象。 request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource()); //FlashMap用于保存转发请求的参数的 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 { if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Restore the original attribute snapshot, in case of an include. if (attributesSnapshot != null) { restoreAttributesAfterInclude(request, attributesSnapshot); } } } }
PS:“javax.servlet.include.request_uri”是INCLUDE_REQUEST_URI_ATTRIBUTE常量的值。isIncludeRequest(request)方法的作用我们可以借助一条JSP的指令来理解:
<jsp:incluede page="index.jsp"/>
这条指令是指在一个页面中嵌套了另一个页面,那么我们知道JSP在运行期间是会被编译成相应的Servlet类来运行的,所以在Servlet中也会有类似的功能和调用语法,这就是RequestDispatch.include()方法。 那么在一个被别的servlet使用RequestDispatcher的include方法调用过的servlet中,如果它想知道那个调用它的servlet的上下文信息该怎么办呢,那就可以通过request中的attribute中的如下属性获取:
javax.servlet.include.request_uri javax.servlet.include.context_path javax.servlet.include.servlet_path javax.servlet.include.path_info javax.servlet.include.query_string
在doService中,下面的try块中可以看到:
try { doDispatch(request, response); }
doService并没有直接进行处理,二是将请求交给了doDispatch进行具体的处理。当然在调用doDispatch之前,doService也是做了一些事情的,比如说判断请求是不是inclde请求,设置一些request属性等。
在doService中除了webApplicationContext、localeResolver、themeResolve和themeSource四个提供给handler和view使用的四个参数外,后面的三个都是和FlashMap有关的,代码如下:
//FlashMap用于保存转发请求的参数的 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);
注释中提到,FlashMap主要用于Redirect转发时参数的传递;
就拿表单重复提交这个问题来说,一种方案就是:在处理完post请求之后,然后Redirect到一个get的请求,这样即使用户刷新也不会有重复提交的问题。但是问题在于,前面的post请求时提交订单,提交完后redirect到一个显示订单的页面,显然在显示订单的页面我们需要知道订单的信息,但是redirect本身是没有参数传递功能的,按照普通的模式如果想传递参数,就只能将参数拼接在url中,但是url在get请求下又是有长度限制的;另外,对于一些场景下,我们也不希望自己的参数暴露在url中。
对于上述问题,我们就可以用FlashMap来进行参数传递了;我们需要在redirect之前将需要的参数写入OUTPUT_FLASH_MAP_ATTRIBUTE,例如:
ServletRequestAttributes SRAttributes = (ServletRequestAttributes)(RequestContextHolder.getRequestAttributes()); HttpServletRequest req = SRAttributes.getRequest(); FlashMap flashMap = (FlashMap)(req.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE)); flashMap.put("myname","glmapper_2018");
这样在redirect之后的handler中spring就会自动将其设置到model里面。但是如果仅仅是这样,每次redirect时都写上面那样一段代码是不是又显得很鸡肋呢?当然,spring也为我们提供了更加方便的用法,即在我们的handler方法的参数中使用RedirectAttributes类型变量即可(前段时间用到这个,本来是想单独写一篇关于参数传递问题的,借此机会就省略一篇吧,吼吼...),来看一段代码:
@RequestMapping("/detail/{productId}") public ModelAndView detail(HttpServletRequest request,HttpServletResponse response,RedirectAttributes attributes, @PathVariable String productId) { if (StringUtils.isNotBlank(productId)) { logger.info("[产品详情]:detail = {}",JSONObject.toJSONString(map)); mv.addObject("detail",JSONObject.toJSONString(getDetail(productId))); mv.addObject("title", "详情"); mv.setViewName("detail.ftl"); } //如果没有获取到productId else{ attributes.addFlashAttribute("msg", "产品不存在"); attributes.addFlashAttribute("productName", productName); attributes.addFlashAttribute("title", "有点问题!"); mv.setViewName("redirect:"/error/fail.htm"); } return mv; }
这段代码时我前段时间做全局错误处理模块时对原有业务逻辑错误返回的一个抽象,因为要将错误统一处理,就不可能在具体的handler中直接返回到错误界面,所以就将所有的错误处理都redirect到error/fail.htm这个handler method中处理。redirect的参数问题上面已经描述过了,这里就不在细说,就是简单的例子和背景,知道怎么去使用RedirectAttributes。
RedirectAttributes这个原理也很简单,就是相当于存在了一个session中,但是这个session在用过一次之后就销毁了,即在fail.htm这个方法中获取之后如果再进行redirect,参数还会丢失,那么就在fail.htm中继续使用RedirectAttributes来存储参数再传递到下一个handler。
为了偷懒,上面强行插入了对Spring中redirect参数传递问题的解释。回归到咱们的doDispatch方法。
作用:处理实际的调度到handler。handler将通过按顺序应用servlet的HandlerMappings来获得。 HandlerAdapter将通过查询servlet已安装的HandlerAdapter来查找支持处理程序类的第一个HandlerAdapter。所有的HTTP方法都由这个方法处理。这取决于HandlerAdapter或处理程序自己决定哪些方法是可以接受的。
其实在doDispatch中最核心的代码就4行,我们来看下:
// Determine handler for the current request. mappedHandler = getHandler(processedRequest);
// Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
我们以上述为轴心,来看下它的整个源码(具体代码含义在代码中标注):
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { //当前请求request HttpServletRequest processedRequest = request; //处理器链(handler和拦截器) HandlerExecutionChain mappedHandler = null; //用户标识multipartRequest(文件上传请求) boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { //很熟悉吧,这个就是我们返回给用户的包装视图 ModelAndView mv = null; //处理请求过程中抛出的异常。这个异常是不包括渲染过程中抛出的异常的 Exception dispatchException = null; try { //检查是不是上传请求 processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // 通过当前请求确定相应的handler mappedHandler = getHandler(processedRequest); //如果没有找到:就会报异常,这个异常我们在搭建SpringMVC应用时会经常遇到: //No mapping found for HTTP request with URI XXX in //DispatcherServlet with name XXX if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // 根据handler找到HandlerAdapter HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); //处理GET和Head请求的Last-Modified //获取请求方法 String method = request.getMethod(); //这个方法是不是GET方法 boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } //这里就是我们SpringMVC拦截器的preHandle方法的处理 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 调用具体的Handler,并且返回我们的mv对象. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); //如果需要异步处理的话就直接返回 if (asyncManager.isConcurrentHandlingStarted()) { return; } //这个其实就是处理视图(view)为空的情况,会根据request设置默认的view applyDefaultViewName(processedRequest, mv); //这里就是我们SpringMVC拦截器的postHandle方法的处理 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we're processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException("Handler dispatch failed", err); } //处理返回结果;(异常处理、页面渲染、拦截器的afterCompletion触发等) processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { //判断是否执行异步请求 if (asyncManager.isConcurrentHandlingStarted()) { // 如果是的话,就替代拦截器的postHandle 和 afterCompletion方法执行 if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // 删除上传请求的资源 if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } }
整体来看,doDispatch做了两件事情: