这篇博客的源码解析处有错误,请大家斟酌的看,等以后再改。
SpringMVC主要运行过程图:(深黄色背景的为重要方法)
Begin
1、根据request调用getHandler()方法获取执行链对象
这里的执行链指的是什么?
执行一次完整的请求我们不仅仅执行处理器方法,还要执行拦截器等方法
所以这个执行链对象中包含了处理器方法、拦截器等对象属性,想调用时就可以调用
2、根据执行链对象中的处理器对象属性调用getHandlerAdapter()获取处理器适配器
3、插曲1——拦截器preHandle方法执行
4、处理器适配器调用handle()方法返回ModelAndView视图
5、插曲2——拦截器postHandle方法执行
6、调用processDispatchResult()解析、渲染视图
7、插曲3——拦截器afterCompletion方法执行
Over
以上就是SpringMVC的基本流程
SpringMVC的详细流程
执行SpringMVC的主要流程服务器必须在启动时将SpringMVC的九大组件初始化好,
接下来的函数才能根据这些资源寻找合适的适配器、解析器等等
SpringMVC九大组件
1、SpringMVC在工作的时候,关键位置都是由这些组件完成的
2、共同点:九大组件都是接口类型(规范性、扩展性)
@Nullable
private MultipartResolver multipartResolver; //文件上传解析器
@Nullable
private LocaleResolver localeResolver; //区域信息解析器,和国际化有关
@Nullable
private ThemeResolver themeResolver; //主题解析器——主题效果更换(没啥用)
@Nullable
private List<HandlerMapping> handlerMappings; //处理器映射信息
@Nullable
private List<HandlerAdapter> handlerAdapters; //处理器适配器信息
@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers; //异常解析器
@Nullable
private RequestToViewNameTranslator viewNameTranslator; //视图名称转换器(没啥用)
@Nullable
private FlashMapManager flashMapManager; //SpringMVC中运行重定向携带数据的功能
//(存到session域中,取完立即移除session)
@Nullable
private List<ViewResolver> viewResolvers; //视图解析器
它们是如何被初始化的呢?
没错,onRefresh就是初始化九大组件的方法,它是Spring构建IOC容器的时候被调用的
让我们仔细看看initStrategies方法它初始化了哪些组件
我们点开一个initHandlerMappings方法
1、detectAllHandlerMappings是Boolean类型,默认为true。是否检测所有的处理器映射类型
2、BeanFactoryUtils.beansOfTypeIncludingAncestors方法作用是从SpringIOC容器中查找所有
HandlerMapping.class的类获得Map matchingBeans对象
3、只抽取Map matchingBeans对象中的value值最后初始化了this.handlerMappings对象
4、当然你也可以不检测所有,在配置初始化DispatchServlet前端控制器的web.xml文件中添加初始化属性设置detectAllHandlerMappings为false即可。这样你就可以采取只获得你配置的HandlerMapping
5、如果以上两种方式都没有初始化this.handlerMappings对象
那么就会采用默认策略,默认策略是根据Properties defaultStrategies对象,其实就是
DispatcherServlet.properties,里面已经指定了所有组件的默认值(注意:值是包含许多个不同的HandlerMapping对象,用逗号分隔)。
执行时会获取DispatcherServlet.properties的属性并在SpringIOC容器中创建对应的Bean对象
DispatcherServlet.properties文件
总结初始化九大组件:
有就从SpringIOC容器中去找,没有呢就使用默认策略在SpringIOC容器中创建默认的组件对象
初始化步骤已经结束,接下来我们根据以上流程步骤看看它们函数中执行了哪些基本操作
一、getHandler()——获取执行链
我们可以清楚看到它在遍历handlerMappings,handlerMappings是调用initStrategies()
初始化九大组件时从SpringIOC容器中获取的数组信息。
进入循环出现有三种HandlerMapping
Handler处理器(Controller)类型
1、使用@Controller注释的类 —— RequestMappingHandlerMapping
2、实现Controller接口的类,通过key访问value值对应的处理器(很少用)
(还要加@Component添加到Spring容器中)—— SimpleUrlHandlerMapping
3、实现Controller接口的类,访问bean的名称直接访问处理器(更少用)
(bean名称必须以“/”开头)—— BeanNameUrlHandlerMapping
因为我们的处理器都是使用@Controller注解标识的类,所以所有处理器类中的方法信息都储存在
RequestMappingHandlerMapping之中
此时这里循环的作用是遍历所有HandlerMapping种类校验是否能根据请求获取到相应的执行链,
只要成功返回执行链对象即退出循环
很明显,现在处理器方法对象方法的所有信息都在RequestMappingHandlerMapping中,而我们要在这些信息中找到我们相应的处理器方法对象并将拦截器等属性合并成执行链的。
所以我们现在点进getHandler方法看看它是如何获取这个执行链的
很明显,我们要能获取执行链对象,必须先获取处理器方法对象,其后再做其它处理最终形成执行链,
所以我们点进去看看如何获取处理器方法对象
这里跳到了子类方法,明显这里没有重要的执行语句,所以我们不管,直接看父类的getHandlerInternal方法
1、getUrlPathHelper().getLookupPathForRequest方法获取了请求的uri路径
2、根据uri和请求调用lookupHandlerMethod查找处理器方法对象的方法
(注意获取的是处理器方法对象,并不是处理器对象!所以这就是我为什么之前一直强调为什么是处理器方法对象,而不是处理器)
我们继续点进去看看。
mappingRegistry.getMappingsByUrl(lookupPath)很明显是根据uri获取一些对象,点进去看看。
很明显,这里根据uri获取到了处理器方法对象的相应信息!
好了,这里就成功获取到处理器方法对象了。
大家不要被这个返回名称handler所迷惑了,其实它真正的类型是 HandlerMethod!
也就是处理器方法类型,所以handler也就是处理器方法对象,而不是处理器对象!
好了,我们继续整合执行链对象
这里根据处理器方法对象和请求调用了getHandlerExecutionChain方法获取执行链
我们继续点进去看看。
1、明显第一步创建一个执行链对象并将处理器方法对象囊入其中
2、根据uri遍历拦截器数组查看是否符合,符合则将该拦截器添加到执行链中
好了,最终执行链对象成功获取了!
二、getHandlerAdapter()获取处理器适配器
我们点进去看看
进入方法后,也是像获取执行链那样遍历数组。这里的handlerAdapters数组有三个成员,
handlerAdapters数组也是经过 initStrategies() 方法初始化的
HandlerAdapter有三种:
1、HttpRequestHandlerAdapter 要求该处理器继承HttpRequest
2、SimpleControllerHandlerAdapter 要求该处理器继承Controller抽象类
3、RequestMappingHandlerAdapter 要求处理器里有方法即可
最终是RequestMappingHandlerAdapter匹配成功并且返回该处理器适配器对象
获取处理器适配器成功~
三、拦截器preHandle方法
很明显,只要拦截器执行的preHandle方法不放行(return false),则后面包括目标方法的所有代码都不
会继续执行(我们继续进里面看看)
这里有三个关键点:
1、interceptorIndex是用来记录已经执行放行的拦截器,标志拦截器数组中哪个拦截器已放行
2、preHandle方法执行预处理
3、triggerAfterCompletion方法根据interceptorIndex来执行已经放行的拦截器的afterCompletion方法
我们再进入triggerAfterCompletion方法中看看
这里很明显的看到根据interceptorIndex的记录数来执行拦截器的执行已经放行的拦截器的afterCompletion方法(倒序执行)
Over
四、处理器适配器调用handle()执行处理器对象(困难,目前能力不足)
然后Steo Into 两层进入handleInternal()方法
很明显,invokeHandlerMethod方法执行后返回ModelAndView mv对象,我们点进去看看
请注意initModel方法,它的方法名看起来虽然不显眼,但很重要,
内部就是执行@ModelAndView标识的方法的步骤!(补充:@ModelAndView方法比处理器方法先执行) 我们点进去看看。
很明显,invokeModelAttributeMethods方法看方法名就是要执行@ModelAndView标识的方法了。
我们继续点进去看看。
invokeForRequest方法继续点进去看看。
很明显,getMethodArgumentValues方法是获取ModelAndView方法的参数和参数值,我们点进去看看。
getMethodParameters方法是获取方法的参数(是未解析的参数数组)
resolvers.resolveArgument方法就是解析参数对象并获取值,我们继续点进去看看
getArgumentResolver方法根据参数获取相应的参数解析对象(如引用类型、基本类型等)
(我的第一个参数是自定义引用类型,第二三个是String、Integer)
继续点击进入resolveArgument方法
getNameForParameter方法是获取参数的名称,
parameter.getParameterAnnotation方法获取参数的注解,
this.bindRequestParameters方法是获取该对象所有属性(request.getParameter)并封装成一个对象最后返回。
对象参数的解析就此结束!
接下来是普通参数的解析
this.resolveStringValue方法获取参数名,
很明显,resolveName方法解析参数(request.getParameter)并返回属性值。
最后经过多次循环,我们的参数值就获取在args[ ]数组中啦!!
参数我们准备好了,那接下来是不是准备执行 ModelAttribute目标方法了呢!
doInvoke方法携带参数数组执行,我们点进去看看。
makeAccessible方法如果没记错的话应该是设置反射可访问私有字段权限,
this.getBridgedMethod().invoke方法我相信大家都很熟悉了,这就是反射调用方法的语法了,
getBridgedMethod()代表处理器方法对象,
this.getBean()代表处理器类对象。
args就是参数啦~
所以执行完这一步后理所当然的会执行 ModelAttribute目标方法中的语句啦!
等等,执行完ModelAttribute方法就应该轮到处理器方法了,但其实我们可以知道,
利用反射方式调用方法其实都是一样的,所以执行处理器方法和执行ModelAttribute方法的步骤是一样的
准备参数——>反射调用
我们来粗略的观摩一下
执行完ModelAttribute方法接下来执行处理器方法,我们点进去看看。
继续点进去。
咦!结局是不是惊人的相似呢?没错,按之前所说的,执行处理器方法和执行ModelAttribute方法方式是相同的,都是准备参数——>反射调用方法的步骤
因为和ModelAttribute方法执行步骤一样,所以我们就不再点进去啦!有兴趣的小伙伴可以继续点进去观察观察
此时此刻,ModelAttribute方法和处理器方法都执行完毕了,但按照规程,应该还有个ModelAndView的
mv对象,所以接下来执行。(但小伙伴们不要误以为之前的两个方法和视图的返回是无关的,其实这里存在一个参数ModelAndViewContainer mavContainer,这个参数保存了之前两个方法的最终Model数据,还有处理器方法的返回值——视图名)
很明显,this.getModelAndView方法就是获取ModelAndView mv 对象的,我们点进去看看。
很明显。new ModelAndView方法就是创建一个ModelAndView mv 对象并把Model数据、视图名等等对其进行初始化的简单过程。
就这样,ModelAndView mv 对象创建成功了。最后成功返回ModelAndView mv 对象。
五、拦截器postHandle方法
拦截器postHandle方法倒序执行
六、processDispatchResult()视图解析
明显该方法携带了ModelAndView mv对象等等(其他对象在此函数中不是关键)
ModelAndView mv对象:有视图名称(字符串)、model(Map)中的数据
明显render方法携带了ModelAndView对象,继续点进去看看
红色圈圈的View view对象是真正渲染页面的视图对象,其实视图解析器目的就是实例化view对象,所以获取如何获取view对象是整个视图渲染步骤的核心!!
我们可以观察一下传入的参数
1、视图名称:viewName
2、model数据:mv.getModelInternal()
3、国际化:“zh_CN”(不重要)
4、request:请求
很明显resolveViewName方法主要依靠 视图名 (model对象在获取视图View不起作用,它是作用于视图渲染过程的) 去获取view对象了,我们继续点进去看看
这里又是循环一个数组了,只不过数组元素类型是视图解析器,没错,就是我们在Spring.XML中配置的
解析器!这个解析器的类型就是 InternalResourceViewResolver
就是这个很简单的视图解析器
这里循环的意思是从数组中每次找到一个视图解析器,用这个视图解析器去获取视图view,如果获取成
功则返回,结束。如果不能获取则换下一个视图解析器,如此循环往复。如果最后视图view仍为空,则
无法实现页面渲染,当然就报异常啦!
所以我们先来尝试第一个视图解析器
(我也只配了一个视图解析器,这个视图解析器一般都正确的啦!所以最后会获取到视图view)
请记住,我们现在采用的视图解析器类型是 InternalResourceViewResolver
继续点进viewResolver.resolveViewName方法看看
以下这个方法调用InternalResourceViewResolver类的爷爷类AbstractCachingViewResolver的方法
明显这里上上下下的方法都只有一个目的——实例化视图view!(前面的方法也是获取视图view,但是是根据缓存获取的,所以如果有缓存的话就不用创建了。但我们此处为了更加清楚如何创建,不适用缓存方式获取视图view。不想用缓存方式的可以重新启动一次项目即可)
实例化视图view对应的方法是createView()
我们继续点进去看看
以下的这个方法是调用InternalResourceViewResolver类的父类UrlBasedViewResolver的方法
咦,这里看到重定向和转发是否有点熟悉了呢!
很明显,此时它会根据viewName来判断创建哪一种视图对象
重定向创建RedirectView对象
转发创建InternalResourceView对象
但很明显,我们的viewName是仅仅是“success”,只能到else代码块中继续调用createView方法。。
我们继续点进去看看(点两次)
很明显,如果要使用默认方法创建视图对象要经过两个步骤
buildView方法和applyLifecycleMethods方法
我们点进buildView方法看看
这里buidView这个方法是先调用的子类方法InternalResourceView对象的方法
很明显,默认创建视图view的类型也是和转发一样的视图类型InternalResourceView
我们再跳到父类的同名方法中,继续点进去看看。
明显创建了一个AbstractUrlBasedView类的视图view(抽象的),
这里也由UrlBasedViewResolve(实质上是 InternalResourceView )将我们配置的视图解析器前缀和后缀都给视图名拼接上了
下面也就是配置参数了,跳过
现在轮到执行applyLifecycleMethods方法,
很明显就是获取Spring容器然后里面再利用反射技术初始化视图view(由于太菜看不懂里面的了)
好了, InternalResourceView 视图解析器终于把视图对象创建并获取了。接下来就是根据视图对象执行页面渲染了!
我们将方法跳出来,来到了真正的页面渲染步骤!我们继续点进去看看
1、createMergedOutputModel方法创建合并输出模型,对model数据进行整合(体现不是很明显,略)
2、prepareResponse方法,对响应response对象设置响应头参数
3、getRequestToExpose方法,要求公开请求设置(默认不公开)
4、前面三个方法都是为了准备页面渲染renderMergedOutputModel方法而准备的步骤
所以我们直接看renderMergedOutputModel方法,继续点进去看看
这里有三个函数
1、exposeModelAsRequestAttributes()
2、prepareForRendering()
3、getRequestDispatcher()
我们先看第一个exposeModelAsRequestAttributes(),点进去看看
终于看到熟悉的语句了,感动。。
明显这里是将model(Map, modelMap)中的所有数据置于request域中,让页面获取到这些数据
prepareForRendering方法了,轮到我们继续点进去看看。
而prepareForRendering方法作用是根据视图名是否以 “/” 开头分两种情况判断请求uri是否和视图名相同,相同则抛出异常
再轮到getRequestDispatcher方法,我们继续点进去看看。
咦,对这个语句是不是有点熟悉呢!没错,就是转发请求,但写法和我们以往的有一点不一样哦,
聪明的小伙伴发现后面没有 .forward(request, response),
所以它getRequestDispatcher方法的作用就是返回一个RequestDispatcher对象,但不进行转发!
(原谅我现在才知道…)
往后我们跳出来继续看。
没错!这里才是真正的跳转方法!调用RequestDispatcher对象的forward方法!
(当然,执行此语句后不会立即跳转到页面,仍需要经过拦截器、过滤器、DispatchServlet前端处理器等等流程才能真正成功到达页面。)
Over…
七、拦截器调用afterCompletion方法
拦截器afterCompletion方法是在视图解析方法中执行的
很明显可以看到,拦截器的afterCompletion方法是在render方法后执行的(当然也是倒序执行)
还有一个地方,视图解析器出错也会调用拦截器的afterCompletion方法
所以这就是拦截器的afterCompletion方法正常执行会调用(内部调用),
报异常也会调用(catch调用)的原因了。