转载请声明出处:https://juejin.im/post/5a587430518825734859e45d
前端
前面两篇文章直接对SpringMVC里面的组件进行了源码分析,可能不少小伙伴都会以为有点摸不着头脑。因此今天再岔回来讲一说SpringMVC的核心控制器,以此为轴心来学习整个SpringMVC的知识体系。java
前面在《项目开发框架-SSM》一篇文章中已经详细的介绍过了SSM项目中关于Spring的一些配置文件,对于一个Spring应用,必不可少的是:ios
<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的,固然不只仅这两个,还有其余几个重要的子类,后续会持续更新):web
<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 的方式来访问咱们的应用了。spring
俗话说知其然,之气因此然;那么为何在配置好相关的配置文件以后,咱们就能访问咱们的SSM项目了呢?从发送一条那样的请求(http://localhost:80/myproject/index.do)展现出最后的界面,这个过程在,Spring帮咱们作了哪些事情呢?(SpringIOC容器的初始化在《Spring技术内幕-容器刷新:wac.refresh》文中已经大概的说了下你们能够参考一下)浏览器
先经过下面这张图来整个了解下SpringMVC请求处理的过程;图中从1-13,大致上描述了请求从发送到界面展现的这样一个过程。 spring-mvc
OK,咱们直接来看DispatcherServlet的类定义:bash
public class DispatcherServlet extends FrameworkServlet 复制代码
DispatcherServlet继承自FrameworkServlet,就这样? session
首先为何要有绿色的部门,有的同窗可能已经想到了,绿色部分不是Spring的,而是java本身的;Spring经过HttpServletBean这位年轻人成功的拥有了JAVA WEB 血统(原本Spring就是用JAVA写的,哈哈)。关于Servlet这个小伙伴能够看下我以前的文章,有简单的介绍了这个接口。mvc
话说回来,既然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转发时参数的传递;
对于上述问题,咱们就能够用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作了两件事情:
那上面就是整个DispatcherServlet的一个大概内容了,关于SpringMVC容器的初始化,咱们在先把DispatcherServlet中涉及到的九大组件撸完以后再回头来学习。关于九大组件目前已经有过两篇是关于HandlerMapping的了,因为咱们打算对于整个SpringMVC体系结构都进行一次梳理,所以,会将九大组件从接口设计以及子类都会经过源码的方式来呈现。
SpringMVC源码系列:AbstractHandlerMapping
你们若是有什么意见或者建议能够在下方评论区留言,也能够给咱们发邮件(glmapper_2018@163.com)!欢迎小伙伴与咱们一块儿交流,一块儿成长。