今天小编尝试从源码层面上对 Spring mvc 的初始化过程进行分析, 一起揭开 Spring mvc 的真实面纱, 也许我们都已经学会使用 spring mvc, 或者说对 spring mvc 的原理在理论上已经能倒背如流. 在开始之前, 这可能需要你掌握 Java EE 的一些基本知识, 比如说我们要先学会 Java EE 的 Servlet 技术规范, 因为 Spring mvc 框架实现, 底层是遵循 Servlet 规范的.
在开始源码分析之前, 我们可能需要一个简单的案例工程, 不慌, 小编已经安排好了:
样例工程下载地址 : https://github.com/SmallerCoder/spring-mvc-test
那下面就让我们开始吧!
一, 前置知识
大家都知道, 我们在使用 spring mvc 时通常会在 web.xml 文件中做如下配置:
- web.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
- <!-- 上下文参数, 在监听器中被使用 -->
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>
- classpath:applicationContext.xml
- </param-value>
- </context-param>
- <!-- 监听器配置 -->
- <listener>
- <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
- </listener>
- <!-- 前端控制器配置 -->
- <servlet>
- <servlet-name>dispatcher</servlet-name>
- <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
- <init-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>classpath:applicationContext-mvc.xml</param-value>
- </init-param>
- <load-on-startup>1</load-on-startup>
- </servlet>
- <servlet-mapping>
- <servlet-name>dispatcher</servlet-name>
- <url-pattern>/</url-pattern>
- </servlet-mapping>
- </web-app>
复制代码
上面的配置总结起来有几点内容, 分别是:
1, 配置 Spring Web 上下文监听器, 该监听器同时是 Spring mvc 启动的入口, 至于为什么, 后面第二节将会讲到
2, 前端控制器 DispatcherServlet, 该控制器是 Spring mvc 处理各种请求的入口及处理器
当我们将 spring mvc 应用部署到 tomcat 时, 当你不配置任何的 context-param 和 listener 参数, 只配置一个 DispatcherServlet 时, 那么 tomcat 在启动的时候是不会初始化 spring web 上下文的, 换句话说, tomcat 是不会初始化 spring 框架的, 因为你并没有告诉它们 spring 的配置文件放在什么地方, 以及怎么去加载. 所以 listener 监听器帮了我们这个忙, 那么为什么配置监听器之后就可以告诉 tomcat 怎么去加载呢? 因为 listener 是实现了 servlet 技术规范的监听器组件, tomcat 在启动时会先加载 web.xml 中是否有 servlet 监听器存在, 有则启动它们.
ContextLoaderListener
是 spring 框架对 servlet 监听器的一个封装, 本质上还是一个 servlet 监听器, 所以会被执行, 但由于
ContextLoaderListener
源码中是基于
contextConfigLocation
和 contextClass 两个配置参数去加载相应配置的, 因此就有了我们配置的 context-param 参数了, servlet 标签里的初始化参数也是同样的道理, 即告诉 web 服务器在启动的同时把 spring web 上下文 (
WebApplicationContext
) 也给初始化了.
上面讲了下 tomcat 加载 spring mvc 应用的大致流程, 接下来将从源码入手分析启动原理.
二, Spring MVC web 上下文启动源码分析
假设现在我们把上面 web.xml 文件中的
<load-on-startup>1</load-on-startup>
给去掉, 那么默认 tomcat 启动时只会初始化 spring web 上下文, 也就是说只会加载到
applicationContext.xml
这个文件, 对于
applicationContext-mvc.xml
这个配置文件是加载不到的,
<load-on-startup>1</load-on-startup>
的意思就是让 DispatcherServlet 延迟到使用的时候 (
也就是处理请求的时候
) 再做初始化.
我们已经知道 spring web 是基于 servlet 标准去封装的, 那么很明显, servlet 怎么初始化,
WebApplicationContext
web 上下文就应该怎么初始化. 我们先看看
ContextLoaderListener
的源码是怎样的.
- public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
- // 初始化方法
- @Override
- public void contextInitialized(ServletContextEvent event) {
- initWebApplicationContext(event.getServletContext());
- }
- // 销毁方法
- @Override
- public void contextDestroyed(ServletContextEvent event) {
- closeWebApplicationContext(event.getServletContext());
- ContextCleanupListener.cleanupAttributes(event.getServletContext());
- }
- }
复制代码
ContextLoaderListener
类实现了
ServletContextListener
, 本质上是一个 servlet 监听器, tomcat 将会优先加载 servlet 监听器组件, 并调用 contextInitialized 方法, 在 contextInitialized 方法中调用
initWebApplicationContext
方法初始化 Spring web 上下文, 看到这焕然大悟, 原来 Spring mvc 的入口就在这里, 哈哈~~~ 赶紧跟进去
initWebApplicationContext
方法看看吧!
initWebApplicationContext()
方法:
- // 创建 web 上下文, 默认是 XmlWebApplicationContext
- if (this.context == null) {
- this.context = createWebApplicationContext(servletContext);
- }
- if (this.context instanceof ConfigurableWebApplicationContext) {
- ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
- // 如果该容器还没有刷新过
- if (!cwac.isActive()) {
- if (cwac.getParent() == null) {
- ApplicationContext parent = loadParentContext(servletContext);
- cwac.setParent(parent);
- }
- // 配置并刷新容器
- configureAndRefreshWebApplicationContext(cwac, servletContext);
- }
- }
复制代码
上面的方法只做了两件事:
1, 如果 spring web 容器还没有创建, 那么就创建一个全新的 spring web 容器, 并且该容器为 root 根容器, 下面第三节讲到的 servlet spring web 容器是在此根容器上创建起来的
2, 配置并刷新容器
上面代码注释说到默认创建的上下文容器是
XmlWebApplicationContext
, 为什么不是其他 web 上下文呢? 为啥不是下面上下文的任何一种呢?
我们可以跟进去
createWebApplicationContext
后就可以发现默认是从一个叫
ContextLoader.properties
文件加载配置的, 该文件的内容为:
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
复制代码
具体实现为:
- protected Class<?> determineContextClass(ServletContext servletContext) {
- // 自定义上下文, 否则就默认创建 XmlWebApplicationContext
- String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
- if (contextClassName != null) {
- try {
- return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
- }
- catch (ClassNotFoundException ex) {
- throw new ApplicationContextException(
- "Failed to load custom context class [" + contextClassName + "]", ex);
- }
- }
- else {
- // 从属性文件中加载类名, 也就是 org.springframework.web.context.support.XmlWebApplicationContext
- contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
- try {
- return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
- }
- catch (ClassNotFoundException ex) {
- throw new ApplicationContextException(
- "Failed to load default context class [" + contextClassName + "]", ex);
- }
- }
- }
复制代码
上面可以看出其实我们也可以自定义 spring web 的上下文的, 那么怎么去指定我们自定义的上下文呢? 答案是通过在 web.xml 中指定 contextClass 参数, 因此第一小结结尾时说 contextClass 参数和
contextConfigLocation
很重要~~ 至于
contextConfigLocation
参数, 我们跟进
configureAndRefreshWebApplicationContext
即可看到, 如下图:
总结:
spring mvc 启动流程大致就是从一个叫
ContextLoaderListener
开始的, 它是一个 servlet 监听器, 能够被 web 容器发现并加载, 初始化监听器
ContextLoaderListener
之后, 接着就是根据配置如
contextConfigLocation
和 contextClass 创建 web 容器了, 如果你不指定 contextClass 参数值, 则默认创建的 spring web 容器类型为
XmlWebApplicationContext
, 最后一步就是根据你配置的
contextConfigLocation
文件路径去配置并刷新容器了.
三, DispatcherServlet 控制器的初始化
好了, 上面我们简单地分析了 Spring mvc 容器初始化的源码, 我们永远不会忘记, 我们默认创建的容器类型为
XmlWebApplicationContext
, 当然我们也不会忘记, 在 web.xml 中, 我们还有一个重要的配置, 那就是 DispatcherServlet. 下面我们就来分析下 DispatcherServlet 的初始化过程.
DispatcherServlet, 就是一个 servlet, 一个用来处理 request 请求的 servlet, 它是 spring mvc 的核心, 所有的请求都经过它, 并由它指定后续操作该怎么执行, 咋一看像一扇门, 因此我管它叫 "闸门". 在我们继续之前, 我们应该共同遵守一个常识, 那就是 ------- 无论是监听器还是 servlet, 都是 servlet 规范组件, web 服务器都可以发现并加载它们.
下面我们先看看 DispatcherServlet 的继承关系:
看到这我们是不是一目了然了, DispatcherServlet 继承了 HttpServlet 这个类, HttpServlet 是 servlet 技术规范中专门用于处理 http 请求的 servlet, 这就不难解释为什么 spring mvc 会将 DispatcherServlet 作为统一请求入口了.
因为一个 servlet 的生命周期是 init()->service()->destory(), 那么 DispatcherServlet 怎么初始化呢? 看上面的继承图, 我们进到 HttpServletBean 去看看.
果不其然, HttpServletBean 类中有一个 init() 方法, HttpServletBean 是一个抽象类, init() 方法如下:
可以看出方法采用 final 修饰, 因为 final 修饰的方法是不能被子类继承的, 也就是子类没有同样的 init() 方法了, 这个 init 方法就是 DispatcherServlet 的初始化入口了.
接着我们跟进 FrameworkServlet 的 initServletBean() 方法:
在方法中将会初始化不同于第一小节的 web 容器, 请记住, 这个新的 spring web 容器是专门为 dispactherServlet 服务的, 而且这个新容器是在第一小节根 ROOT 容器的基础上创建的, 我们在 < servlet > 标签中配置的初始化参数被加入到新容器中去.
至此, DispatcherSevlet 的初始化完成了, 听着有点蒙蔽, 但其实也是这样, 上面的分析仅仅只围绕一个方法, 它叫 init(), 所有的 servlet 初始化都将调用该方法.
总结:
dispactherServlet 的初始化做了两件事情, 第一件事情就是根据根 web 容器, 也就是我们第一小节创建的
XmlWebApplicationContext
, 然后创建一个专门为 dispactherServlet 服务的 web 容器, 第二件事情就是将你在 web.xml 文件中对 dispactherServlet 进行的相关配置加载到新容器当中.
三, 每个 request 调用请求经历了哪些过程
其实说到这才是 dispatcherServlet 控制器的核心所在, 因为 web 框架无非就是接受请求, 处理请求, 然后响应请求. 当然了, 如果 dispactherServlet 只是单纯地接受处理然后响应请求, 那未免太弱了, 因此 spring 设计者加入了许许多多的新特性, 比如说拦截器, 消息转换器, 请求处理映射器以及各种各样的 Resolver, 因此 spring mvc 非常强大.
dispatcherServlet 类不做相关源码分析, 因为它就是一个固定的执行步骤, 什么意思呢? 一个 request 进来, 大致就经历这样的过程:
接受请求 -----> 是否有各种各样的处理器 Handler -------> 是否有消息转换器 HandlerAdapter --------> 响应请求
上面每一步如果存在相应的组件, 当然前提是你在项目中有做相关的配置, 则会执行你配置的组件, 最后响应请求. 因此明白大致的流程之后, 如果你想调试一个 request, 那么你完全可以在 dispatcherServlet 类的 doDispatch 方法中打个断点, 跟完代码之后你就会发现其实大致流程就差不多了.
四, 后话
本文的工程是基于传统的 web.xml 加载 web 项目, 当然在 spring mvc 中我们也可以完全基于注解的方式进行配置, 我们可以通过实现
WebApplicationInitializer
来创建自己的 web 启动器, 也可以通过继承
AbstractAnnotationConfigDispatcherServletInitializer
来创建相应的 spring web 容器 (包括上面说到的根容器和 servlet web 容器), 最后通过继承
WebMvcConfigurationSupport
再一步进行自定义配置 (相关拦截器, bean 等)
感谢你的阅读, 期待与你共同进步, 欢迎下方发表评论~~~
来源: https://juejin.im/post/5b207dc86fb9a01e49294f42