写点什么

深入理解 spring mvc 启动过程与原理

作者:三十而立
  • 2023-03-17
    湖南
  • 本文字数:7419 字

    阅读完需:约 24 分钟

深入理解spring mvc启动过程与原理

spring mvc 的启动,是跟随着 tomcat 启动的,所以要深入理解 spring mvc 的启动过程与原理,需要先了解下 tomcat 启动的一些关键过程。


1、tomcat web 应用启动及初始化过程


参考官方文档,tomcat web 应用启动过程是这样的:


图 1 tomcat web 应用启动过程


大概意思就是,当一个 Web 应用部署到容器内时,在 web 应用开始执行用户请求前,会依次执行以下步骤:


部署描述文件web.xml中<listener>元素标记的事件监听器会被创建和初始化;对于所有事件监听器,如果实现了ServletContextListener接口,将会执行其实现的contextInitialized()方法;部署描述文件中由<filter>元素标记的过滤器会被创建和初始化,并调用其init()方法;部署描述文件中由<servlet>元素标记的servlet会根据<load-on-startup>的权值按顺序创建和初始化,并调用其init()方法;
复制代码


通过上述文档的描述,可知 tomcat web 应用启动初始化流程是这样的:


图 2 tomcat web 应用初始化过程


可以看出,在 tomcat web 应用的初始化流程是,先初始化 listener,接着初始化 filter,最后初始化 servlet。


2、spring mvc 应用的启动初始化


做过 spring mvc 项目开发的伙伴,都会配置一个 web.xml 配置文件,内容一般是这样的:


:spring/spring-mvc*.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>springServlet</servlet-name><url-pattern>/</url-pattern></servlet-mapping></web-app>


web.xml 配置文件中也主要是配置了 Listener,Filter,Servlet。


所以 spring mvc 应用启动的时候,主要是在这三大组件初始化的过程中完成对 spring 及 spring mvc 的初始化。


3、Listener 与 spring 的初始化过程


web.xml 配置文件中首先定义了<context-param>标签,用于配置一个全局变量,<context-param>标签的内容读取后会做为 Web 应用的全局变量使用。当 Listener 创建的时候,会使用到这个全局变量,因此,Web 应用在容器中部署后,进行初始化时会先读取这个全局变量,之后再进行初始化过程。


接着定义了一个 ContextLoaderListener 类的 Listener。查看 ContextLoaderListener 的类声明如图:


图 3 ContextLoaderListener 类源码


ContextLoaderListener 类继承自 ContextLoader 类,并实现了 ServletContextListener 接口。


图 4 ServletContextListener 源码


ServletContextListener 只有两个方法,contextInitialized 和 contextDestroyed,当 Web 应用初始化或销毁时会分别调用这两个方法。


ContextLoaderListener 实现了 ServletContextListener 接口,因此在 Web 应用初始化时会调用 contextInitialized 方法,该方法的具体实现如下:


图 5 contextInitialized 方法


ContextLoaderListener 的 contextInitialized()方法直接调用了 initWebApplicationContext()方法,这个方法是继承自 ContextLoader 类,通过函数名可以知道,该方法是用于初始化 web 应用上下文,即 IOC 容器。


这里是 spring mvc 应用启动的第一个重点,就是在 ContextLoaderListener 初始化的时候,初始化了 spring IOC 容器。


我们继续看 ContextLoader 类的 initWebApplicationContext()方法。


//servletContext,servlet 上下文 public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {// 首先通过 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE// 这个 String 类型的静态变量获取一个根 IoC 容器,根 IoC 容器作为全局变量// 存储在 servletContext 对象中,如果存在则有且只能有一个// 如果在初始化根 WebApplicationContext 即根 IoC 容器时发现已经存在// 则直接抛出异常,因此 web.xml 中只允许存在一个 ContextLoader 类或其子类的对象 if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {throw new IllegalStateException("Cannot initialize context because there is already a root application context present - " +"check whether you have multiple ContextLoader* definitions in your web.xml!");}


servletContext.log("Initializing Spring root WebApplicationContext");Log logger = LogFactory.getLog(ContextLoader.class);if (logger.isInfoEnabled()) {  logger.info("Root WebApplicationContext: initialization started");}long startTime = System.currentTimeMillis();

try { // Store context in local instance variable, to guarantee that // it is available on ServletContext shutdown. // 如果当前成员变量中不存在WebApplicationContext则创建一个根WebApplicationContext if (this.context == null) { this.context = createWebApplicationContext(servletContext); } if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; if (!cwac.isActive()) { // The context has not yet been refreshed -> provide services such as // setting the parent context, setting the application context id, etc if (cwac.getParent() == null) { // The context instance was injected without an explicit parent -> // determine parent for root web application context, if any. // 为根WebApplicationContext设置一个父容器 ApplicationContext parent = loadParentContext(servletContext); cwac.setParent(parent); } // 配置并刷新整个根IoC容器,在这里会进行Bean的创建和初始化 configureAndRefreshWebApplicationContext(cwac, servletContext); } } // 将创建好的IoC容器放入到servletContext对象中,并设置key为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE // 因此,在SpringMVC开发中可以在jsp中通过该key在application对象中获取到根IoC容器,进而获取到相应的Ben servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { currentContextPerThread.put(ccl, this.context); }

if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms"); }

return this.context;}catch (RuntimeException | Error ex) { logger.error("Context initialization failed", ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex;}
复制代码


}


在 jsp 中,可以通过这两种方法获取到 IOC 容器:


WebApplicationContext applicationContext = (WebApplicationContext) servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);


WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());


总的来说,initWebApplicationContext 方法的主要目的是创建 root WebApplicationContext 对象,即根 IOC 容器。


如果整个 Web 应用存在根 IOC 容器则有且只能有一个,根 IOC 容器会作为全局变量存储在 ServletContext 对象中。然后将根 IOC 容器放入到 ServletContext 对象之前进行了 IOC 容器的配置和刷新操作,即调用 configureAndRefreshWebApplicationContext()方法,该方法源码如下:


protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {if (ObjectUtils.identityToString(wac).equals(wac.getId())) {// The application context id is still set to its original default value// -> assign a more useful id based on available informationString idParam = sc.getInitParameter(CONTEXT_ID_PARAM);if (idParam != null) {wac.setId(idParam);}else {// Generate default id...wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +ObjectUtils.getDisplayString(sc.getContextPath()));}}wac.setServletContext(sc);// 获取 web.xml 中<context-param>标签配置的全局变量,其中 key 为 CONFIG_LOCATION_PARAM// 也就是我们配置的相应 Bean 的 xml 文件名,并将其放入到 WebApplicationContext 中 String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);if (configLocationParam != null) {wac.setConfigLocation(configLocationParam);}// The wac environment's #initPropertySources will be called in any case when the context// is refreshed; do it eagerly here to ensure servlet property sources are in place for// use in any post-processing or initialization that occurs below prior to #refreshConfigurableEnvironment env = wac.getEnvironment();if (env instanceof ConfigurableWebEnvironment) {((ConfigurableWebEnvironment) env).initPropertySources(sc, null);}customizeContext(sc, wac);wac.refresh();}


configureAndRefreshWebApplicationContext 方法里获取了<context-param>标签配置的全局变量,并且在方法最后调用了 refresh()方法。


对 spring 容器初始化有一定了解的同学都知道,这是初始化 spring 容器的入口方法,其最终调用的是 AbstractApplicationContext 类中的 refresh 方法。


refresh()方法主要是创建并初始化 contextConfigLocation 类配置的 xml 文件中的 Bean。


具体代码不贴了,有兴趣的同学,可以自行查阅。


到此为止,整个 ContextLoaderListener 类的启动过程就结束了,可以发现,创建 ContextLoaderListener 是比较重要的一个步骤,主要做的事情就是创建根 IOC 容器,并使用特定的 key 将其放入到 servletContext 对象中,供整个 Web 应用使用。


由于在 ContextLoaderListener 类中构造的根 IOC 容器配置的 Bean 是全局共享的,因此,在<context-param>标识的 contextConfigLocation 的 xml 配置文件一般包括:数据库 DataSource、DAO 层、Service 层、事务等相关 Bean。


4、Filter 与 spring 的初始化


在监听器 Listener 初始化完成后,接下来会进行 Filter 的初始化操作,Filter 的创建和初始化没有涉及 IOC 容器的相关操作,因此不是本文讲解的重点。


5、Servlet 与 spring 的初始化


web 应用启动的最后一个步骤就是创建和初始化相关 servlet,servlet 最重要的方法就是:init(),service(),destroy();


图 6 Servlet 源码


在 spring mvc 中,实现了一个非常重要的 servlet,即 DispatcherServlet。它是整个 spring mvc 应用的核心,用于获取分发用户请求并返回响应。


图 7 DispatcherServlet 类图


DispatcherServlet 本质上也是一个 Servlet,其源码实现充分利用了模板模式,将不变的部分统一实现,将变化的部分留给子类实现,上层父类不同程度的实现了相关接口的部分方法,留出了相关方法由子类覆盖。


图 8 DispatcherServlet 类初始化过程


web 应用部署到容器启动后,进行 Servlet 的初始化时会调用相关的 init(ServletConfig)方法,因此,DispatchServlet 类的初始化过程也由该方法开始。


其中比较重要的是,FrameworkServlet 类中的 initServletBean()方法、initWebApplicationContext()方法以及 DispatcherServlet 类中的 onRefresh()方法。


图 9 FrameworkServlet 类中的 initServletBean()方法


FrameworkServlet 的父类 HttpServletBean 中的 initServletBean()方法,HttpServletBean 抽象类在执行 init()方法时会调用 initServletBean()方法。


该方法中比较重要的是 initWebApplicationContext()方法,该方法仍由 FrameworkServlet 抽象类实现,继续查看其源码如下所示:


protected WebApplicationContext initWebApplicationContext() {// 获取由 ContextLoaderListener 创建的根 IoC 容器// 获取根 IoC 容器有两种方法,还可通过 key 直接获取 WebApplicationContext rootContext =WebApplicationContextUtils.getWebApplicationContext(getServletContext());WebApplicationContext wac = null;


if (this.webApplicationContext != null) {  // A context instance was injected at construction time -> use it  wac = this.webApplicationContext;  if (wac instanceof ConfigurableWebApplicationContext) {    ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;    if (!cwac.isActive()) {      // The context has not yet been refreshed -> provide services such as      // setting the parent context, setting the application context id, etc      if (cwac.getParent() == null) {        // The context instance was injected without an explicit parent -> set        // the root application context (if any; may be null) as the parent        // 如果当前Servelt存在一个WebApplicationContext即子IoC容器        // 并且上文获取的根IoC容器存在,则将根IoC容器作为子IoC容器的父容器        cwac.setParent(rootContext);      }      // 配置并刷新当前的子IoC容器,功能与前面讲解根IoC容器时的配置刷新一致,用于构建相关Bean      configureAndRefreshWebApplicationContext(cwac);    }  }}if (wac == null) {  // No context instance was injected at construction time -> see if one  // has been registered in the servlet context. If one exists, it is assumed  // that the parent context (if any) has already been set and that the  // user has performed any initialization such as setting the context id  // 如果仍旧没有查找到子IoC容器则创建一个子IoC容器  wac = findWebApplicationContext();}if (wac == null) {  // No context instance is defined for this servlet -> create a local one  wac = createWebApplicationContext(rootContext);}

if (!this.refreshEventReceived) { // Either the context is not a ConfigurableApplicationContext with refresh // support or the context injected at construction time had already been // refreshed -> trigger initial onRefresh manually here. synchronized (this.onRefreshMonitor) { // 调用子类覆盖的onRefresh方法完成“可变”的初始化过程 onRefresh(wac); }}

if (this.publishContext) { // Publish the context as a servlet context attribute. String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac);}

return wac;
复制代码


}


通过函数名不难发现,该方法的主要作用同样是创建一个 WebApplicationContext 对象,即 IOC 容器,不过这里创建的是 spring mvc 容器,是前面 ContextLoadListener 创建的 IOC 容器的子容器。


为什么需要多个 IOC 容器?还是父子容器?这就要说明下父子 IOC 容器的访问特性了。


父子容器类似于类的继承关系,子类可以访问父类中的成员变量,而父类不可访问子类的成员变量,同样的,子容器可以访问父容器中定义的 Bean,但父容器无法访问子容器定义的 Bean。


创建多个容器的目的是,父 IOC 容器做为全局共享的 IOC 容器,存放 Web 应用共享的 Bean,比如 Service,DAO。而子 IOC 容器根据需求的不同,放入不同的 Bean,比如 Conroller,这样能够做到隔离,保证系统的安全性。


如果你看过 sprin cloud netflix 系列源码,比如 spring-cloud-netflix-ribbon,spring-cloud-openfeign,就会发现它里边创建了很多父子容器,作用与这里是一样的,容器与容器相互隔离,保证系统的安全性。


我们继续讲解 DispatcherServlet 类的子 IOC 容器创建过程,如果当前 Servlet 存在一个 IOC 容器则为其设置根 IOC 容器作为其父类,并刷新该容器,用于初始化定义的 Bean。


这里的方法与前文讲述的根 IOC 容器类似,同样会读取用户在 web.xml 中配置的<servlet>中的<init-param>值,用于查找相关的 xml 配置文件来创建定义的 Bean。如果当前 Servlet 不存在一个子 IoC 容器就去查找一个,如果没有查找到,则调用 createWebApplicationContext()方法去创建一个。


图 10 createWebApplicationContext 源码


该方法用于创建一个子 IOC 容器并将根 IOC 容器做为其父容器,接着执行配置和刷新操作构建相关的 Bean。


至此,根 IOC 容器以及相关 Servlet 的子 IOC 容器已经初始化完成了,子容器中管理的 Bean 一般只被该 Servlet 使用,比如 SpringMVC 中需要的各种重要组件,包括 Controller、Interceptor、Converter、ExceptionResolver 等。


spring mvc 父子容器之间的关系,可以用下图来描述:


图 11 spring mvc 父子容器


当 IOC 子容器构造完成后调用了 onRefresh()方法,其实现是在子类 DispatcherServlet 中,查看 DispatcherServletBean 类的 onRefresh()方法源码如下:


图 12 onRefresh 方法


onRefresh()方法调用了 initStrategies()方法,通过函数名可以判断,该方法主要初始化创建 multipartResovle 来支持图片等文件的上传、本地化解析器、主题解析器、HandlerMapping 处理器映射器、HandlerAdapter 处理器适配器、异常解析器、视图解析器、flashMap 管理器等。这些组件都是 SpringMVC 开发中的重要组件,相关组件的初始化创建过程均在此完成,所以在这里最终完成了 spring mvc 组件的初始化。


至此,整个 spring mvc 应用启动的原理以及过程就讲完了,总的来说,spring 以及 spring mvc 的初始化过程是跟随在 tomcat 的 Listener、Filter、Servlet 的初始化过程完成的。


6、总结


spring 以及 spring mvc 应用初始化过程还是比较清晰明了的,本文做了完整的分析记录。spring mvc 也支持无 web.xml 配置文件的方式开发 web 应用,其原理是类似的,只不过是用代码的方式来创建了 web.xml 中需要的组件。


后续会分析 spring boot 应用的启动以及初始化过程分析,spring boot 是在 spring 及 spring mvc 的基础上做了更深的封装,所以看起来也更复杂。spring boot 应用既可以以 war 包的方式运行在 tomcat 上,也可以以 jar 的方式运行再内嵌的 tomcat 上,两种方式启动的源码有所不同,还需要分别分析。

用户头像

三十而立

关注

还未添加个人签名 2023-02-06 加入

还未添加个人简介

评论

发布
暂无评论
深入理解spring mvc启动过程与原理_Java_三十而立_InfoQ写作社区