小白都能看懂的|小白都能看懂的 Spring 源码揭秘之Spring MVC
目录
- 前言
- Spring MVC 请求流程
- Spring MVC 两大阶段
- 初始化
- HttpServletBean#init()
- FrameworkServlet#initServletBean
- FrameworkServlet#initWebApplicationContext
- DispatchServlet#onRefresh
- Spring MVC 九大组件
- MultipartResolver
- LocaleResolver
- ThemeResolver
- HandlerMapping
- HandlerAdapter
- HandlerExceptionResolver
- RequestToViewNameTranslator
- ViewResolver
- FlashMapManager
- 处理请求
- DispatcherServlet#doDispatch
- DispatcherServlet#getHandler
- AbstractHandlerMapping#getHandler
- AbstractHandlerMethodMapping#getHandlerInternal
- AbstractHandlerMethodMapping#lookupHandlerMethod
- AbstractHandlerMethodMapping 的初始化
- AbstractHandlerMethodMapping#initHandlerMethods
- AbstractHandlerMethodMapping#detectHandlerMethods
- AbstractHandlerMethodMapping#register
- 初始化
- Spring MVC 两大阶段
- 总结
前言 对于
Web
应用程序而言,我们从浏览器发起一个请求,请求经过一系列的分发和处理,最终会进入到我们指定的方法之中,这一系列的的具体流程到底是怎么样的呢?Spring MVC 请求流程 记得在初入职场的时候,面试前经常会背一背
Spring MVC
流程,印象最深的就是一个请求最先会经过 DispatcherServlet
进行分发处理,DispatcherServlet
就是我们 Spring MVC
的入口类,下面就是一个请求的大致流转流程(图片参考自 Spring In Action
):文章图片
- 一个请求过来之后会到达
DispatcherServlet
,但是DispatcherServlet
也并不知道这个请求要去哪里。 DispatcherServlet
收到请求之后会去查询处理器映射(HandlerMapping),从而根据浏览器发送过来的URL
解析出请求最终应该调用哪个控制器。- 到达对应控制器(Controller)之后,会完成一些逻辑处理,而且在处理完成之后会生成一些返回信息,也就是
Model
,然后还需要选择对应的视图名。 - 将模型(
Model
)和视图(View
)传递给对应的视图解析器(View Resolver),视图解析器会将模型和视图进行结合。 - 模型和视图结合之后就会得到一个完整的视图,最终将视图返回前端。
Spring MVC
流程,为什么要说这是传统的流程呢?因为这个流程是用于前后端没有分离的时候,后台直接返回页面给浏览器进行渲染,而现在大部分应用都是前后端分离,后台直接生成一个 Json
字符串就直接返回前端,不需要经过视图解析器进行处理,也就是说前后端分离之后,流程就简化成了 1-2-3-4-7
(其中第四步返回的一般是 Json 格式数据)。Spring MVC 两大阶段 Spring MVC主要可以分为两大过程,一是初始化,二就是处理请求。初始化的过程主要就是将我们定义好的
RequestMapping
映射路径和 Controller
中的方法进行一一映射存储,这样当收到请求之后就可以处理请求调用对应的方法,从而响应请求。初始化
初始化过程的入口方法是
DispatchServlet
的 init()
方法,而实际上 DispatchServlet
中并没有这个方法,所以我们就继续寻找父类,会发现 init
方法在其父类(FrameworkServlet)的父类 HttpServletBean
中。HttpServletBean#init() 在这个方法中,首先会去家在一些 Servlet 相关配置(web.xml),然后会调用
initServletBean()
方法,这个方法是一个空的模板方法,业务逻辑由子类 FrameworkServlet
来实现。文章图片
FrameworkServlet#initServletBean 这个方法本身没有什么业务逻辑,主要是初始化
WebApplicationContext
对象,WebApplicationContext
继承自 ApplicationContext
,主要是用来处理 web
应用的上下文。文章图片
FrameworkServlet#initWebApplicationContext
initWebApplicationContext()
方法主要就是为了找到一个上下文,找不到就会创建一个上下文,创建之后,最终会调用方法 configureAndRefreshWebApplicationContext(cwac)
方法,而这个方法最终在设置一些基本容器标识信息之后会去调用 refresh()
方法,也就是初始化 ioc
容器。文章图片
当调用
refresh()
方法初始化 ioc
容器之后,最终会调用方法 onRefresh()
,这个方法也是一个模板钩子方法,由子类实现,也就是回到了我们 Spring MVC
的入口类 DispatcherServlet
。DispatchServlet#onRefresh
onRefresh()
方法就是 Spring MVC
初始化的最后一个步骤,在这个步骤当中会初始化 Spring MVC
流程中可能需要使用到的九大组件。文章图片
Spring MVC 九大组件 MultipartResolver 这个组件比较熟悉,主要就是用来处理文件上传请求,通过将普通的
Request
对象包装成 MultipartHttpServletRequest
对象来进行处理。LocaleResolver
LocaleResolver
用于初始化本地语言环境,其从 Request
对象中解析出当前所处的语言环境,如中国大陆则会解析出 zh-CN
等等,模板解析以及国际化的时候都会用到本地语言环境。ThemeResolver 这个主要是用户主题解析,在
Spring MVC
中,一套主题对应一个 .properties
文件,可以存放和当前主题相关的所有资源,如图片,css样式等。HandlerMapping 用于查找处理器(
Handler
),比如我们 Controller
中的方法,这个其实最主要就是用来存储 url
和 调用方法的映射关系,存储好映射关系之后,后续有请求进来,就可以知道调用哪个 Controller
中的哪个方法,以及方法的参数是哪些。HandlerAdapter 这是一个适配器,因为
Spring MVC
中支持很多种 Handler
,但是最终将请求交给 Servlet
时,只能是 doService(req,resp)
形式,所以 HandlerAdapter
就是用来适配转换格式的。HandlerExceptionResolver 这个组件主要是用来处理异常,不过看名字也很明显,这个只会对处理
Handler
时产生的异常进行处理,然后会根据异常设置对应的 ModelAndView
,然后交给 Render
渲染成页面。RequestToViewNameTranslator 这个主键主要是从
Request
中获取到视图名称。ViewResolver 这个组件会依赖于
RequestToViewNameTranslator
组件获取到的视图名称,因为视图名称是字符串格式,所以这里会将字符串格式的视图名称转换成为 View
类型视图,最终经过一系列解析和变量替换等操作返回一个页面到前端。FlashMapManager 这个主键主要是用来管理
FlashMap
,那么 FlashMap
又有什么用呢?要明白这个那就不得不提到重定向了,有时候我们提交一个请求的时候会需要重定向,那么假如参数过多或者说我们不想把参数拼接到 url
上(比如敏感数据之类的),这时候怎么办呢?因为参数不拼接在 url
上重定向是无法携带参数的。FlashMap
就是为了解决这个问题,我们可以在请求发生重定向之前,将参数写入 request
的属性 OUTPUT_FLASH_MAP_ATTRIBUTE
中,这样在重定向之后的 handler
中,Spring
会自动将其设置到 Model
中,这样就可以从 Model
中取到我们传递的参数了。处理请求
在九大组件初始化完成之后,
Spring MVC
的初始化就完成了,接下来就是接收并处理请求了,那么处理请求的入口在哪里呢?处理请求的入口方法就是 DispatcherServlet
中的 doService
方法,而 doService
方法又会调用 doDispatch
方法。DispatcherServlet#doDispatch
文章图片
这个方法最关键的就是调用了
getHandler
方法,这个方法就是会获取到前面九大组件中的 HandlerMapping
,然后进行反射调用对应的方法完成请求,完成请求之后后续还会经过视图转换之类的一些操作,最终返回 ModelAndView
,不过现在都是前后端分离,基本也不需要用到视图模型,在这里我们就不分析后续过程,主要就是分析 HandlerMapping
的初始化和查询过程。DispatcherServlet#getHandler 这个方法里面会遍历
handllerMappings
,这个 handllerMappings
是一个 List
集合,因为 HandlerMapping
有多重实现,也就是 HandlerMapping
不止一个实现,其最常用的两个实现为 RequestMappingHandlerMapping
和 BeanNameUrlHandlerMapping
。文章图片
AbstractHandlerMapping#getHandler
AbstractHandlerMapping
是一个抽象类,其 getHandlerInternal
这个方法也是一个模板方法:文章图片
getHandlerInternal
方法最终其会调用子类实现,而这里的子类实现会有多个,其中最主要的就是 AbstractHandlerMethodMapping
和 AbstractUrlHandlerMapping
两个抽象类,那么最终到底会调用哪个实现类呢?这时候如果拿捏不准我们就可以看一下类图,上面我们提到,
HandlerMapper
有两个非常主要的实现类:RequestMappingHandlerMapping
和 BeanNameUrlHandlerMapping
。那么我们就分别来看一下这两个类的类图关系:文章图片
文章图片
可以看到,这两个实现类的抽象父类正好对应了
AbstractHandlerMapping
的两个子类,所以这时候具体看哪个方法,那就看我们想看哪种类型了。- RequestMappingHandlerMapping:主要用来存储
RequestMapping
注解相关的控制器和url
的映射关系。
- BeanNameUrlHandlerMapping:主要用来处理
Bean name
直接以/
开头的控制器和url
的映射关系。
HandlerMapping
之外,Spring
中还有其他一些 HandllerMapping
,如 SimpleUrlHandlerMapping
等。提到的这几种
HandlerMapping
,对我们来说最常用,最熟悉的那肯定就是 RequestMappingHandlerMapping
,在这里我们就以这个为例来进行分析,所以我们应该AbstractHandlerMethodMapping#getHandlerInternal 这个方法本身也没有什么逻辑,其主要的核心查找
Handler
逻辑在 lookupHandlerMethod
方法中,这个方法主要是为了获取一个 HandlerMethod
对象,前面的方法都是 Object
,而到这里变成了 HandlerMethod
类型,这是因为 Handler
有各种类型,目前我们已经基本跟到了具体类型之下,所以类型就变成了具体类型,而如果我们看的的另一条分支线,那么返回的就会是其他对象,正是因为支持多种不同类型的 HandlerMapping
对象,所以最终为了统一执行,才会需要在获得 Hanlder
之后,DispatcherServlet
中会再次通过调用 getHandlerAdapter
方法来进一步封装成 HandlerAdapter
对象,才能进行方法的调用文章图片
AbstractHandlerMethodMapping#lookupHandlerMethod 这个方法主要会从
mappingRegistry
中获取命中的方法,获取之后还会经过一系列的判断比较判断比较,因为有些 url
会对应多个方法,而方法的请求类型不同,比如一个 GET
方法,一个 POST
方法,或者其他一些属性不相同等等,都会导致最终命中到不同的方法,这些逻辑主要都是在 addMatchingMappings
方法去进一步实现,并最终将命中的结果加入到 matches
集合内。文章图片
在这个方法中,有一个对象非常关键,那就是
mappingRegistry
,因为最终我们根据 url
到这里获取到对应的 HandlerMtthod
,所以这个对象很关键:文章图片
看这个对象其实很明显可以看出来,这个对象其实只是维护了一些
Map
对象,所以我们可以很容易猜测到,一定在某一个地方,将 url
和 HandlerMapping
或者 HandlerMethod
的映射关系存进来了,这时候其实我们可以根据 getMappingsByUrl
方法来进行反推,看看 urlLookup
这个 Map
是什么时候被存入的,结合上面的类图关系,一路反推,很容易就可以找到这个 Map
中的映射关系是 AbstractHandlerMethodMapping
对象的 afterPropertiesSet
方法实现的(AbstractHandlerMethodMapping
实现了 InitializingBean
接口),也就是当这个对象初始化完成之后,我们的 url
和 Handler
映射关系已经存入了 MappingRegistry
对象中的集合 Map
中。AbstractHandlerMethodMapping 的初始化
afterPropertiesSet
方法中并没有任何逻辑,而是直接调用了 initHandlerMethods
。AbstractHandlerMethodMapping#initHandlerMethods
initHandlerMethods
方法中,首先还是会从 Spring
的上下文中获取所有的 Bean
,然后会进一步从带有 RequestMapping
注解和 Controller
注解中的 Bean
去解析并获得 HandlerMethod
。文章图片
AbstractHandlerMethodMapping#detectHandlerMethods 这个方法中,其实就是通过反射获取到
Controller
中的所有方法,然后调用 registerHandlerMethod
方法将相关信息注册到 MappingRegistry
对象中的各种 Map
集合之内:文章图片
AbstractHandlerMethodMapping#register
registerHandlerMethod
方法中会直接调用 AbstractHandlerMethodMapping
对象持有的 mappingRegistry
对象中的 regidter
方法,这里会对 Controller
中方法上的一些元信息进行各种解析,比如参数,路径,请求方式等等,然后会将各种信息注册到对应的 Map
集合中,最终完成了整个初始化。文章图片
总结 【小白都能看懂的|小白都能看懂的 Spring 源码揭秘之Spring MVC】本文重点以
RequestMappingHandlerMapping
为例子分析了在 Spring
当中如何初始化 HandlerMethod
,并最终在调用的时候又是如何根据 url
获取到对应的方法并进行执行最终完成整个流程。推荐阅读
- zookeeper从小白到精通
- Python|我写的 Python 代码,同事都说好
- 不考虑安全的数字化转型都是伪命题
- C语言小白进阶之路|C语言——字符、字符串的输入输出
- 搭建分布式事务组件|搭建分布式事务组件 seata 的Server 端和Client 端详解(小白都能看懂)
- 靠跳槽3年从7K涨到22K(!那些越跳槽越值钱的人,都做对了什么?)
- C语言的常量|C语言的常量,字符串,转义字符,注释你都了解吗
- 黑客都用python还是c-黑客编程为什么首选Python语言(这里告诉你答案!)
- 郭盛华|为什么黑客都用Linux系统(原来是这四大原因)
- 资讯|黑客都使用什么编程语言()