浅谈SpringMVC源码的DispatcherServlet组件执行流程
Posted 默辨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈SpringMVC源码的DispatcherServlet组件执行流程相关的知识,希望对你有一定的参考价值。
文章目录
一、基础概念
在阅读该篇博客前,你一定一定要了解前端控制器的作用以及对应的处理流程。
如下图:
用户前台发起请求,请求发送给前端控制器(DispatcherServlet),前端控制器会获取对应的处理器映射器(HandlerMapping),然后返回给前端控制器;前端控制器根据返回的处理器映射器,选择处理器适配器(HandlerAdapter)完成对应的功能逻辑后,再次返回到前端控制器;最后再调用视图解析器(ViewResolver),进行相关的处理。最后的最后再进行视图渲染,最终返回给我们的前台用户。(我相信在看这篇博客的小伙伴对于这个概念肯定是烂熟于心了。本文主要内容就是带你跟你一下相关细节的源码,即SpringMVC的DispatcherServlet组件的源码)

1、初始化变量数据
建议在看该篇博客之前,可以简单浏览一下我之前的博客:浅谈SpringMVC源码之SpringServletContainerInitializer的完整加载流程。如果你不看,很有可能你就不知道我们的数据是在什么地方加载以及获取的,你只能根据方法的名字去猜测它的功能是什么。
需要用到前文的知识点为:SpringMVC会在容器启动阶段,初始化对应的数据变量

这里就会初始化我们后面需要用到的最终要的三个map集合:处理器映射器、处理器适配器、视图解析器

2、urlLookup集合
除了上面说的那几个变量,你还需要了解这两个集合。
urlLookup作用:用来存放键值对,映射url(key)-RequestMappingInfo对象(value)。RequestMappingInfo可以理解为为我们添加的@RequestMapping注解的信息。
补充:RequestMappingInfo对象内部维护的各种xxxCondition变量,对应为@RequestMapping注解上我们能够自己配置的参数
AbstractHandlerMethodMapping类实现了InitializingBean接口,那么我么就可以联想到Spring中Bean的生命周期的初始化步骤,即执行afterPropertiesSet方法。
我们可以发现这个类是抽象类,在Spring中抽象类是无法实例化为Bean的,所以该抽象类有具体的实现,在将子类实例化为Bean的时候,调用对应的初始化步骤,完成数据的填充。

对应方法的调用逻辑,如下图,

3、策略设计模式
这个设计模式在SpringMVC中十分常见,我认为SpringMVC的大的骨架都是基于策略设计模式进行设计的。
特别是三个核心组件,全部是基于策略模式进行配置。
不熟悉策略模式的小伙伴,可以就把策略模式理解为switch case。当为1,使用A方法,当为2,使用B方法。使用策略模式,可以让我们的代码整理上看起来更加的更加的美观和优雅,更符合代码设计的开闭原则。
下面是我自己搭建的一个小Demo,仅供参考:
1)定义一个顶层接口

2)编写三个不同的策略实现类(对应不同的数据类型)



3)测试类

这就是一个策略模式的简单小实现,根据我么传入参数的不同类型,调用不同的业务实现类。
4、前期的基本流程
DispatcherServlet类的继承体系图示
顺着Servlet的doGet方法的调用逻辑,这里的代码使用的是模板方法设计模式。

以Get请求为例,当我们发起一次请求后
1、首先会根据请求的类型选择调用不同的方法,此处会调用doGet

2、doGet方法在FrameworkServlet抽象类中给出了实现,会调用当前类的processRequest方法

3、该类会处理一些基本的数据信息,然后又调用子类的doService方法(doService方法是一个模板方法)

4、此时就会调用到DispatcherServlet类中的doService方法,然后调用当前类的doDispatch方法

二、doDispatch
我将该方法中需要执行的逻辑分为三类:文件相关、拦截器相关、主体流程相关。

1、文件相关
开始
选择合适的文件解析器资源,完成对应的解析操作
1)选择使用何种策略,满足条件返回true
2)根据返回的策略,调用具体的resolveMultipart方法,完成处理逻辑


回想SpringMVC阶段,我们是如何配置文件下载的?是不是就是配置这么一个Bean,换句话说,是不是就是指定对应的解析器?

结束
清除生成的临时文件

2、拦截器
让我们先来观察对应的拦截器接口,是不是像极了Bean的生命周期的处理方法?

前置拦截
如果方法返回false,则直接结束该方法,后面的逻辑就不再执行。回想前置处理器的作用是不是就是这样?

方法内部调用逻辑:
1)获取对应处理数组(数据在构建处理器映射器的时候添加,后文会提及)
2)遍历执行
3)只要有一个拦截器返回为false,就就执行对应的处理完成后拦截,也就是afterCompletion方法。最终返回false,然后方法调用结束

后置拦截
这里的处理逻辑大同小异,就不再赘述

三、getHandler(重点)
为方便记忆理解,我将我们的处理器映射器对象的处理方法称为handlerMethod对象(父类是HandlerMethod类型),但是其实它是一个Object类(子类是Object类型),即这种说话在某种程度上来说不准确,但是却方便记忆。
1、HandlerMapping
1)handlerMappings肯定不为空
2)遍历对应的mappings集合
3)根据当前的request请求,调用对应的getHandler方法
4)如果返回的结果不为null,则说明是与我们当前请求匹配的handlerMapping

所以对应的获取逻辑在最后面得hm.gethandler(request)方法中
该代码片段来自AbstractHandlerMapping类,该类主要执行两件事情
1)根据路径找到合适得handlerMethod
2)将找到得handlerMethod对象,封装成HandlerExecutionChain类对象,返回

2、getHandlerInternal
根据请求路径,找到对应的HandlerMethod类对象,根据已有得request请求,拿到我们的访问路径,这不难吧?
底层就是各种getAttribute(xxx),然后辅助各种判断,感兴趣的小伙伴可以自己去跟代码

lookupHandlerMethod方法
在根据路径找HandlerMethod类对象时,会调用这个方法。在这个方法中,就会去根据我们的映射url路径,找到对应的HandlerMethod类对象。这里就会联系到我们在第一节中提及的urlLookup集合,忘记的小伙伴,记得往前看。
当我们的请求路径无法直接匹配映射路径,但是却满足一些通过通配符的路径,如*、?。此时就会使用到另一个集合mappingLookup,该集合的数据与urlLookup集合的数据处理相同。复杂的地方就是此时SpringMVC会初始化一个比较器,然后根据指定的比较规则,选择优先级最高的映射,进行对应的逻辑处理。
该代码片段来自AbstractHandlerMethodMapping类

3、getHandlerExecutionChain
看名字,这里显然使用了一个责任链的设计模式
第3步,已经返回了对应的handlerMethod对象,这一步是进行拦截器的封装。
前面我们在doDispatch方法中有提及,会调用对应的拦截器方法执行,可是拦截器的数据来自哪里?就是在这里
1)获取当前的映射路径
2)找到我们容器中配置的所有的拦截器
3)如果拦截器类继承自MappedInterceptor类,调用拦截器中的matches方法,如果路径匹配就添加到对应的拦截器链条中
4)其他常规的拦截器就都添加到拦截器链条中
5)最终返回封装好的带有拦截器链条和handlerMethod对象的HandlerExecutionChain

四、getHandlerAdapter(重点)
截至到目前,我们已经做好的工作为(仅考虑主体逻辑),找到对应的handlerMethod对象,即处理的逻辑,封装好了对应的拦截器链
1、getHandlerAdapter方法
传入前面封装好的chain对象的handlerMethod属性。这里就会根据对应的handlerMethod类型,选择合适的处理器适配器(HandlerAdapter)

判断supports逻辑比较简单,这里就不再说明
2、ha.handle()
前面流程是选择合适的handlerAdapter,这一步就是根据具体的adapter处理器对象,执行我们方法里面的具体逻辑
我们以使用注解的方式为例,其使用的HandlerAdapter是RequestMappingHandlerAdapter。
注意:一定要是org.springframework.web.servlet.mvc.method.annotation路径下的HandlerAdapter。因为在org.springframework.web.reactive.result.method.annotation路径下也有一个HandlerAdapter。
点进handle方法,就会执行下面的方法逻辑
该代码片段来自RequestMappingHandlerAdapter类

3、invokeHandlerMethod
上面方法的核心方法就是invokeHandlerMethod,我们熟悉的解析@ModelAttribute注解,就是在该方法中。具体的方法功能已在注释中给出,这里就不再赘述。
该代码片段来自RequestMappingHandlerAdapter类

4、invokeAndHandle
该方法会调用我们的业务逻辑(invokeForRequest),并且完成返回内容的解析(handleReturnValue)。具体功能参考代码注释
该代码片段来自ServletInvocableHandlerMethod类

5、invokeForRequest
我们最想看的还是在什么位置调用了我们controller层里面的方法,这个位置就是在invokeForRequest方法中。
该代码片段来自InvocableHandlerMethod类中

6、handleReturnValue
我们都知道如果我们添加了@ResponseBody注解,那么返回值就是JSON字符串,这个处理逻辑,就是在这个方法中进行处理的。
1)根据返回的值和返回的类型,找到合适的返回值处理器
2)使用具体的返回值处理器处理我们的返回值
该代码片段来自HandlerMethodReturnValueHandlerComposite类

这个selectHandler方法,会调用supportsReturnType方法,即调用具体返回值处理器支持的类型方法,如下图。是否有ResponseBody注解


该方法的调用逻辑过长(writeWithMessageConverters),仅展示关键部分。即在添加了@RequestBody注解的基础上,会根据具体的json实现类完成将结果转换成json字符串的业务逻辑
该代码片段来自AbstractMessageConverterMethodProcessor类

五、processDispatchResult(重点)
该方法就是我们的视图解析器。根据方法内容很容易就能看出来,它会调用render方法

1、render
该方法主要处理三件事情
1)根据我们的视图名称 解析成为我们真正的物理视图,通过视图解析器对象(resolveViewName)
2)渲染模型视图(view.render)
2、resolveViewName
就是完成的映射路径的前缀和后缀的拼接
获取我们配置的视图解析器。


执行方法后,完成对应的url拼接

3、view.render
根据我们配置的视图解析器,执行对应的处理逻辑

具体的处理逻辑,请参考注释。也就是在这一步,完成我们请求的转发,完成页面的逻辑处理

六、核心流程图
根据上面的代码,再回忆一下这个执行流程,看看自己能理清楚吗?

至此,前端控制器这个大的组件的基本骨架算是梳理完成,感兴趣的小伙伴可以自己去跟源码,完成其他细节的处理。整理了接近4个小时,如果对你有帮助,期待你的收藏。
好了,不说了,现在是下午两点,吃饭去!!!
以上是关于浅谈SpringMVC源码的DispatcherServlet组件执行流程的主要内容,如果未能解决你的问题,请参考以下文章
浅谈SpringMVC源码的DispatcherServlet组件执行流程