servlet
在了解dispatcherServlet的流程前,首先要了解servlet这个技术。
狭义的来说,servlet只是一个接口,广义的来说,他是一个规范。
在传统的Javaweb项目里,通过继承HttpServlet(实现了service接口)重写doGet,doPost方法,再在web.xml里标识。通过tomcat作为servlet容器来管理这些servlet。
tomcat在不考虑io模型通信情况下,只有两个主要的功能。
- 封装request,response对象
- 调用servlet的service方法
这个是理解dispatcher流程的关键,首先要知道tomcat到底是干什么的
我们可以看下HttpServlet的service方法的源码
1 | Java复制代码protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { |
也就是说传统的servlet的项目中,tomcat把请求封装后根据url调用对应路径的servlet,执行service方法,再判断请求类型去调用doGet(),doPost()等。
dispatcherServlet和servlet的关系
dispatcherServlet依赖关系很复杂,简单来说就是他间接的实现了servlet接口。先看下servlet接口的方法
1 | Java复制代码public interface Servlet { |
我们一会重点说下init和service方法。
对于springmvc来说,需要在web.xml里配置dispatcherServlet,让tomcat去管理。对于spring的项目,tomcat只需要管理spring自带的servlet就可以了,我们不需要去写servlet,我们是通过bean让spring容器去管理。这里的spring容器和tomcat容器不是一个概念。
springboot基于自动配置不需要去手动配置。
根据上面所说,任何路径下的请求,tomcat都会调用spring的dispatcherServlet的service方法去处理,由于依赖关系复杂,service方法是在dispatcher的父类。总结来说,最后会调用dispatch的doDispatch方法,中间会有些过程,这里就忽略不计了。
流程
丑陋的面经只会甩给你一张图
硬背这种八股文,完全不知道他的理念和实现细节,对我们的思想是没有一点好处的。所以要剖析下他的源码。
初始化
首先要对spring容器初始化刷新容器,才能进行交互。对于springmvc来说,tomcat会先执行dispatcher的init方法(其实是HttpServletBean的init方法)来刷新容器,底层就是调用的refresh方法。对于springboot来说,会执行main方法的run方法,里面执行一个refresh方法来刷新容器。虽然仍然会执行init方法,但里面的refresh方法应该是不会再执行的。(虽然不知道springboot为什么可以不让执行,但根据打断点来说,他确实没有执行)。总之殊途同归。
请求流程
我再把之前的捋一遍,请求过来后,先被tomcat封装request,response对象,在调用service方法,即dispatchServlet的doDispatch方法,直接看源码
1 | Java复制代码protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { |
这么看复杂的一比,实际上对比丑陋面经的图片,只有几个方法比较重要。
1 | java复制代码//从Handler映射器获取handlerchain |
感觉这应该是spring中最容易理解的源码了,我们逐条分析。
映射器
首先是getHandler()方法
1 | Java复制代码 protected HandlerExecutionChain getHandler(HttpServletRequest request) { |
这个理解起来很简单,由于有多种的Handler,就有多种的mapping,需要找到正确的mapping
- RequestMappingUrlHandlerMapping 最常见的使用注解标识的handler映射
- BeanNameUrlHandlerMapping
- SimpleUrlHandlerMapping
- ControllerClassNameHandlerMapping
其中第一个最为常见,使用@requestMapping注解标识的handlermapping,其他的有继承controller,httpservlet的,就不多说了。
通过循环,直到找到正确的处理器映射器,获取到handle,实际上是一个HandlerExecutionChain,他的构成其实就是拦截器数组+handler。
适配器
上一步获取到了处理器链后,获取适配器,通过getHandlerAdapter方法,参数为handler(目前拦截器数组还没有用)
1 | ini复制代码HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); |
这里就用上了大名鼎鼎的适配器模式了
如果不使用适配器,以后再增加handle种类时,由于不同handle之间的逻辑完全不一样,需要通过在dispatch中使用if else的方式去判断handle的种类,再执行操作。
1 | java复制代码 protected HandlerAdapter getHandlerAdapter(Object handler) { |
这里的this.handlerAdapters是获取适配器数组,跟上面有点像。获取到合适的适配器返回。
适配器执行handle方法
适配器使用handle方法执行
1 | ini复制代码mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); |
最后得到了一个modelandview。在古老的jsp时代,是通过modelandview传递数据并解析jsp视图的。但由于现在前后端分离,使用@ResponseBody注释只传送json串,所以这里的modelandview为空。
所以接下来的视图解析部分我们就不关注了
拦截器
所以handerchain里的拦截器作用是什么呢。
其实在适配器执行handle方法前后会进行一个拦截器的处理,只是我没有写而已。
最后
tomcat的处理,我就不说了
小结
springmvc这块的流程如果不深挖其实很简单,基本上有过开发经验的都能够大概了解这块的流程。但其实我上面省略了一部分,就是适配器的handle方法和映射器的getHandler方法。这块比较难理解,所以我这里只介绍前后端分离和常用注解的情况,我尽量从顶向下的方式去说明源码。
映射器的源码分析
面经简略版
- 在ioc初始化时,映射器也会初始化,就把映射对应关系放在一个map里
- 映射器的getHandler方法,通过url的后缀,从map中获取到对应的handler
详细版
先说下HandlerMapping的继承关系
实际上最常用的就是RequestMappingHandlerMapping ,他对应的就是@Controller @RequestMapping这种写法的handle映射器。我这里就只介绍这一个映射器了。
我们直接从请求流程来看
首先先是在dispatcher里循环映射器列表,调用getHandler方法(上面有写),实际就是调用AbstractHandlerMapping的getHandler方法
AbstractHandlerMapping
1 | java复制代码public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { |
这个getHandlerInternal()返回的就是我们需要的handle,点进去会调用AbstractHandlerMethodMapping的方法
getHandlerInternal()
1 | java复制代码protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { |
先看返回值,返回了一个HandlerMethod,这个就是我们的处理器。之前说过,springmvc有多种handler,每个handle的处理逻辑都不一样,通过不同映射器获得的handler都不同,之后需要通过适配器来统一规范执行。这个后面再说,总之HandlerMethod这个对象很重要。
HandlerMethod类
看下HandlerMethod类的构造
1 | java复制代码//省略了很多 |
就介绍下这两个成员变量吧。bean就是controller的bean,Method是映射到controller的方法,Method不懂的话可以去看下反射的原理。总之在后面适配器那里会执行method的invoke方法,也就是执行controller上映射的方法。
我们在返回刚才的方法,看下核心方法lookupHandlerMethod
lookupHandlerMethod()
1 | java复制代码 protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { |
这个代码我进行了疯狂的省略,把一些多映射或者空的情况删掉了,我们先去理解正常代码的一个流程。
我们来分析下第二行重点代码,this.mappingRegistry.getMappingsByDirectPath(lookupPath),我们首先要看这个mappingRegistry是什么。
mappingRegistry类
1 | java复制代码class MappingRegistry { |
mappingRegistry是一个内部类,里面都是一堆map。这个getMappingsByDirectPath方法只是从pathLookup这个map里,以lookupPath(url,例如/user/login)为key获取值而已,这个值就是我们的handler。
这时候问题就来了,这些map是什么时候获取的内容呢
在springioc容器初始化时,映射器也会初始化,就把映射对应关系放在pathLookup这个map里。具体流程就跟容器初始化有关了,我就不详细叙述了。
小结
可以发现,对于这种请求来说,根本就没用到RequestMappingHandlerMapping的方法,都是他的父类AbstractHandlerMapping的方法。之后的流程就很简单了,只是把handlerMethod封装成handerchain,返回了。
适配器的源码分析
面经简略版
- 调用RequestMappingHandlerAdapter的handle方法,实际是通过反射调用controller的方法,并返回值
- 根据返回值选择特定的返回值解析器(例如使用@Response注解返回的是对象,则使用RequestResponseBodyMethodProcessor,将对象序列化为json)
- 最后返回的mv为null,跳过了视图解析器。
详细版
HandlerAdapter
上面流程的模块介绍了适配器有很多种,他们都实现了HandleAdapter接口,看下接口的源码
1 | java复制代码public interface HandlerAdapter { |
同样省略,重要的只有这两个方法。
第一个方法在上文循环判断是否合适时调用过,第二个方法就是dispatcher里的执行方法,返回一个modelAndView。这么一看很简单吗,所以我们来看下他的实现类。
由于handle种类很多,就会对应了很多的适配器
我们平常@Controller出来的HandlerMethod使用的是RequestMappingHandlerAdapter,是这里最复杂的。为什么复杂呢,因为他涉及了很多的url映射,参数和返回值的处理。这里我先举一个简单的例子,SimpleServletHandlerAdapter,servlet类型的handler适配器。
SimpleServletHandlerAdapter
1 | typescript复制代码public class SimpleServletHandlerAdapter implements HandlerAdapter { |
这里先说明一点,spring也是可以使用servlet规范的写法的,例如继承httpservlet,只是还会走mvc的流程,因为这种servlet不归tomcat管理,而是作为一个bean被spring容器管理。
可以看到这个实现类和他的名字一样简单。supports只是判断了handler是否是servlet类型,handle只是调用了service。
AbstractHandlerMethodAdapter
1 | java复制代码//简化版 |
先看RequestMappingHandlerAdapter的抽象类。
- supports判断类型是否是HandlerMethod,除此之外还有个新方法supportsInternal,但是对于RequestMappingHandlerAdapter来说,这个方法一直返回true。所以我们可以认为, 当一个Handler是HandlerMethod 类型的时候, 就会被这个适配器处理。这里的重点就是HandlerMethod,他代表这我们平常使用的handler类型
- handle调用新方法handleInternal,作为参数的handler被强转为HandlerMethod类型
handle方法流程
刚才简单介绍下父类适配器的接口,接下来看流程
上面说到,dispatcher会先循环获取对应的handleAdapeter,通过的就是supports方法,我就忽略掉了。
然后会调用handle方法,其实就是RequestMappingHandlerAdapter的handleInternal方法
RequestMappingHandlerAdapter
handleInternal()
1 | java复制代码protected ModelAndView handleInternal(){ |
在抽象的省略后,只剩下invokeHandlerMethod方法
invokeHandlerMethod()
1 | java复制代码protected ModelAndView invokeHandlerMethod(HttpServletRequest request, |
这里有大量异步和mavcontainer的部分我都删掉了,没意义的同时也是因为我看不懂,我就不分析了。
这块代码核心就是invocableMethod.invokeAndHandle(webRequest, mavContainer),分析下参数。
- webRequest request的封装
- mavcontainer在前后端分离情况都是为空的。
HandlerMethod子类
1 | java复制代码 public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, |
1 | java复制代码public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, |
1 | java复制代码protected Object doInvoke(Object... args) throws Exception { |
这是请求到controller之前的最后一步,这是InvocableHandlerMethod的doInvoke方法。这里只进行了一个简单的反射而已。获取到Method对象后调用invoke方法。也就是执行controller下映射url的方法,我这里举一个例子
1 | java复制代码@GetMapping |
一个极其标准的controller,返回的是一个Object对象
对于返回值和参数值的解析我就不研究了。
小结
对于前后端分离传递json这种写法,modelandview都为空,都是直接对返回值处理的,我就没有关注这个modelandview的处理。
总结
作为最常使用的mvc,应该是对于我们这种web开发来说最容易理解的spring源码了。里面还有很多的学问,包括对不同handler的处理或者不同类之间的区别,jsp的解析等,我才疏学浅就不深究了。如果上面有什么写的不对,请评论告诉我。
本文转载自: 掘金