SpringMVC源码解析
Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就包含在Spring框架中。
其正式名称“Spring Web MVC”来自它的源模块(Spring -webmvc)的名称,但它更常见的名称是“Spring MVC”。
本节介绍Spring Web MVC。
(1)servlet3.0新特性
通过上图可以知道,servlet3.0给我们提供了一个非常牛逼的规范,只要我们按照这个规范,我们就能在tomcat启动的时候去掉web.xml,而且还能初始化spring环境。
- 定义了一个新的规范,即在资源文件的META-INF/services文件夹下面,有一个以javax.servlet.ServletContainerInitializer命名的文件,里面定义一个你自己的类的全类名。同时该类实现javax.servlet.ServletContainerInitializer接口,并重写onStartup方法
- 在上面这个规范下,所有按照这个规范的servlet服务器,例如:tomcat,在服务启动的时候,会自己反射执行这个类的onStartup()方法
- 通过这个新的规范,我们就不需要按照传统的方法,需要在web.xml文件中,初始化spring等配置和环境,这样做就能实现零配置,springboot就是按照这个思想实现零配置的。
(2)模拟SpringBoot零配置,内嵌tomcat
通过上图,可以看到模拟springboot零配置和内嵌tomcat,主要要注意几点:
- tomcat.addContext和tomcat.addWebapp的区别:
(1) addWebapp表示该项目是一个web项目,tomcat启动的时候,就会默认去加载jsp视图解析器,然后没有添加jsp视图解析器的依赖,就会报错。
(2) addContext表示的仅仅是往tomcat的webapps目录添加一个context,tomcat不会加载jsp视图解析器,也就不会报找不到jsp视图解析器依赖的错了。
(3) springboot基本上是已经默认不再使用jsp技术,例如:thymeleaf,freemarker…等,所以springboot的底层,肯定不会使用addWebapp这个方法。
- 这里不使用上面提到的servlet3.0新特性的规范,主要实现WebApplicationInitializer这个接口,tomcat启动要执行到这个类的代码,一定要在web项目的情况下,才会执行到。即在使用addContext这个方式的情况下,是不会执行到我们的类。那么这里的唯一做法:只能在一个软方法里面,同时初始化spring ioc,spring mvc和tomcat等环境。
在开始spring mvc的源码解析之前,我们先要有这样的一个概念:
- 通过浏览器发起的一个请求,只能请求到一个servlet的方法,它是无法直接请求到一个java类的某个方法,也就是我们经常使用的controller类的方法。
- 那么spring mvc框架可以让一个请求,执行到对应的controller类的某个方法,肯定是先让一个请求去到servlet,然后这个servlet再调用到我们的controller类的某个方法。
request —-> ! servlet.class(这是不可能实现的)
request —-> servlet —-> controller(只能是方法调用,不然无法实现)(方法调用,底层一定是反射技术:indexController:index())
- DispatcherServlet这个servlet,就是spring mvc的核心类。
(3)Spring MVC源码解析
- 先上图:SpringMVC核心流程图
总结:
(1) 首先请求进入DispatcherServlet 由DispatcherServlet 从HandlerMappings中提取对应的Handler。
(2) 此时只是获取到了对应的Handle,然后得去寻找对应的适配器,即:HandlerAdapter。
(3) 拿到对应HandlerAdapter时,这时候开始调用对应的Handler处理业务逻辑了。
(这时候实际上已经执行完了我们的Controller) 执行完成之后返回一个ModeAndView
(4) 这时候交给我们的ViewResolver通过视图名称查找出对应的视图然后返回。
(5) 最后 渲染视图 返回渲染后的视图 –>响应请求。
3.1Spring MVC初始化阶段
我们要从哪里入手呢?
通过上面的分析,我们知道spring mvc的核心类是DispatcherServlet,这是一个servlet类,那么看这个类,就从这个类的init方法开始。
- DispatcherServlet类的结构图
- 查看DispatcherServlet类,没有看法init的方法,那么就只能找这个类的父类。
1 | java复制代码# 1.执行父类HttpServletBean的init()方法 |
(1)通过上面代码分析,可以得到在执行DispatcherServlet的init方法,会执行父类的HttpServletBean的init方法,然后调用了FrameworkServlet的initServletBean()方法。
HttpServletBean#init() —> FrameworkServlet#initServletBean()
(2)执行initWebApplicationContext()方法,就是对spring ioc环境的初始化。那么这里就衍生出了一个面试题:spring容器和spring mvc的容器的区别?通过源码的分析,spring和spring mvc底层,都是调用了同一个refresh()方法,所以spring容器和spring mvc容器是没有区别的,都是指的是同一个容器。
(3)执行到onRefresh()方法,就是开始初始化DispatcherServlet了,也就是开始初始化spring mvc。
1 | java复制代码# 1.执行DispatcherServlet类的initStrategies()方法 |
(1) initHandlerMappings方法,就是初始化我们的handlerMapping(请求映射器)。
(2) handlerMapping的主要作用是,找到请求路径对应的controller的方法。例如:请求的路径 “/index”,然后这个handlerMapping,在初始化的时候,已经将所有controller的请求路径映射保存在一个map集合,当请求过来的时候,就将”/index”作为一个key,从map集合中找到对应的controller的index方法。
(3) 这里初始化handlerMappings ,默认是有两个handlerMappings ,是直接在defaultStrategies配置文件中获取。
(4) 那么defaultStrategies的值是什么时候初始化的呢?通过查看源码,defaultStrategies这个值,是DispatcherServlet类的静态代码块初始化的。
全世界都知道,当一个类被初始化的时候,会执行该类的static静态代码块的。
1 | java复制代码# 1.DispatcherServlet类的static静态代码块 |
从DispatcherServlet.properties配置文件,可以看出handlerMapping默认是有两个:
1.BeanNameUrlHandlerMapping (主要处理object)
2.RequestMappingHandlerMapping(主要处理method)
3.2Spring MVC请求阶段分析
用户的一个请求过来,会由servlet接收到,然后一步一步调用到DispatcherServlet的doService方法。
1 | java复制代码# 1.DispatcherServlet类的doService()方法 |
通过对DispatcherServlet的分析,得到请求的核心处理方法是doDispatch(),主要是分了几步:
(1) 检查请求中是否有文件上传操作
(2) 确定当前请求的处理的handler(重点)
(3) 推断适配器,不同的controller类型,交给不同的适配器去处理
(4) 执行前置拦截器处理interceptor
(5) 通过找到的HandlerAdapter ,反射执行相关的业务代码controller的方法。
(6) 返回结果。
1 | java复制代码# 1.DispatcherServlet类的getHandler()方法 |
(1) getHandler()方法,主要是遍历在DispatcherServlet初始化是,初始化的handlerMappings。
(2) 这个方法的主要思想是,通过request的路径,去匹配对应的controller去处理。
(3) SpringMVC自己自带了2个HandlerMapping 来供我们选择 至于 为什么要有2个呢?
- 我们用2种方式来注册Controller 分别是:
- (1) 作为Bean的形式:实现Controller接口,重写handleRequest方法,请求路径为”/test”
1 | java复制代码@Component("/test") |
- (2) 以Annotation形式:
1 | java复制代码@Controller |
(1) 经过测试,可以得到以Bean方式的controller,是通过BeanNameUrlHandlerMapping去匹配
(2)以注解方法的controller,是通过RequestMappingHandlerMapping去匹配
- BeanNameUrlHandlerMapping处理bean方式的源码分析:
1 | java复制代码# 1.执行到AbstractUrlHandlerMapping的getHandlerInternal()方法 |
(1) 以Bean方式的controller,匹配请求的路径,是通过一个handlerMap去匹配,比较简单。
(2) 这里的问题是,这个handlerMap的值,是什么时候放进去的?通过源码分析,BeanNameUrlHandlerMapping是实现了ApplicationContextAware接口。
如果你精通spring的源码,就知道spring的实例bean的时候,会回调这些类的setApplicationContext()方法。
1 | java复制代码# 1.执行父类的ApplicationObjectSupport的setApplicationContext()方法 |
BeanNameUrlHandlerMapping处理bean方式的源码分析,其实是很简单:
(1) 在类初始化的时候,就已经将所有实现了Controller接口的controller类,拿到他们的@Componet(‘/test’)
(2) 然后将’/test’这个作为key,controller类作为value,放入到一个map集合。
(3) 当一个请求过来的时候,拿到这个请求的uri,在map里面找,找到了即表示匹配上
- RequestMappingHandlerMapping处理注解方式的源码分析:
1 | java复制代码# 1.AbstractHandlerMethodMapping#getHandlerInternal |
RequestMappingHandlerMapping处理注解方式的源码分析,比较复杂,用一个MappingRegistry维护所有的请求路径映射。
MappingRegistry的初始化,也是在该bean实例化的时候,就已经做好的了。
原理也是和上一个差不多,都是从一个map集合里面匹配。所以这里就不再做解析了。
总结:getHandler()
- 接下来到找Apapter适配器了
1 | java复制代码protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { |
其实能看见他是从一个handlerAdapters属性里面遍历了我们的适配器 这个handlerAdapters哪来的呢? 跟我们的HandlerMappings一样 在他的配置文件里面有写,就是我们刚刚所说的 。
至于什么是适配器,我们结合Handler来讲, 就如我们在最开始的总结时所说的, 一开始只是找到了Handler 现在要执行了,但是有个问题,Handler不止一个, 自然而然对应的执行方式就不同了, 这时候适配器的概念就出来了:对应不同的Handler的执行方案。当找到合适的适配器的时候, 基本上就已经收尾了,因为后面在做了一些判断之后(判断请求类型之类的),就开始执行了你的Handler了,上代码:
1 | java复制代码mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); |
这个mv就是我们的ModlAndView 其实执行完这一行 我们的Controller的逻辑已经执行完了, 剩下的就是寻找视图 渲染图的事情了。
总结:
其实我们的SpringMVC关键的概念就在于Handler(处理器) 和Adapter(适配器)
通过一个关键的HandlerMappings 找到合适处理你的Controller的Handler
然后再通过HandlerAdapters找到一个合适的HandlerAdapter 来执行Handler即Controller里面的逻辑。
最后再返回ModlAndView…
总的来说,springmvc的源码,还是很复杂,本博客只是大概的描述了主要的执行流程。
源码注释下载地址:github.com/llsydn/spri…
本文转载自: 掘金