该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读

Spring Boot 版本:2.2.x

最好对 Spring 源码有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章导读》 系列文章

如果该篇内容对您有帮助,麻烦点击一下“推荐”,也可以关注博主,感激不尽~

该系列其他文章请查看:《精尽 Spring Boot 源码分析 - 文章导读》

概述

我们知道 Spring Boot 能够创建独立的 Spring 应用,内部嵌入 Tomcat 容器(Jetty、Undertow),让我们的 jar 无需放入 Servlet 容器就能直接运行。那么对于 Spring Boot 内部嵌入 Tomcat 容器的实现你是否深入的学习过?或许你可以通过这篇文章了解到相关内容。

在上一篇 《SpringApplication 启动类的启动过程》 文章分析了 SpringApplication#run(String... args) 启动 Spring 应用的主要流程,不过你是不是没有看到和 Tomcat 相关的初始化工作呢?

别急,在刷新 Spring 应用上下文的过程中会调用 onRefresh() 方法,在 Spring Boot 的 ServletWebServerApplicationContext 中重写了该方法,此时会创建一个 Servlet 容器(默认为 Tomcat),并添加 IoC 容器中的 Servlet、Filter 和 EventListener 至 Servlet 上下文。

例如 Spring MVC 中的核心组件 DispatcherServlet 对象会添加至 Servlet 上下文,不熟悉 Spring MVC 的小伙伴可查看我前面的 《精尽Spring MVC源码分析 - 一个请求的旅行过程》 这篇文章。同时,在 《精尽Spring MVC源码分析 - 寻找遗失的 web.xml》 这篇文章中有提到过 Spring Boot 是如何加载 Servlet 的,感兴趣的可以先去看一看,本文会做更加详细的分析。

接下来,我们一起来看看 Spring Boot 内嵌 Tomcat 的实现。

文章的篇幅有点长,处理过程有点绕,每个小节我都是按照优先顺序来展述的,同时,主要的流程也标注了序号,请耐心查看????

如何使用

在我们的 Spring Boot 项目中通常会引入 spring-boot-starter-web 这个依赖,该模块提供全栈的 WEB 开发特性,包括 Spring MVC 依赖和 Tomcat 容器,这样我们就可以打成 jar 包直接启动我们的应用,如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

如果不想使用内嵌的 Tomcat,我们可以这样做:

dependency>

然后启动类这样写:

return builder.sources(Application.class); } }

这样你打成 war 包就可以放入外部的 Servlet 容器中运行了,具体实现查看下一篇文章,本文分析的主要是 Spring Boot 内嵌 Tomcat 的实现。

回顾

在上一篇 《SpringApplication 启动类的启动过程》 文章分析 SpringApplication#run(String... args) 启动 Spring 应用的过程中讲到,在创建好 Spring 应用上下文后,会调用其 AbstractApplication#refresh() 方法刷新上下文,该方法涉及到 Spring IoC 的所有内容,参考 《死磕Spring之IoC篇 - Spring 应用上下文 ApplicationContext》

// 清除相关缓存,例如通过反射机制缓存的 Method 和 Field 对象,缓存的注解元数据,缓存的泛型类型对象,缓存的类加载器 resetCommonCaches(); } } }

在该方法的第 10 步可以看到会调用 onRefresh() 方法再进行一些初始化工作,这个方法交由子类进行扩展,那么在 Spring Boot 中的 ServletWebServerApplicationContext 重写了该方法,会创建一个 Servlet 容器(默认为 Tomcat),也就是当前 Spring Boot 应用所运行的 Web 环境。

第 13 步会调用 onRefresh() 方法,ServletWebServerApplicationContext 重写了该方法,启动 WebServer,对 Servlet 进行加载并初始化

类图

由于整个 ApplicationContext 体系比较庞大,下面列出了部分类

精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现

DispatcherServlet 自动配置类

在开始之前,我们先来看看 org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration 这个自动配置类,部分如下:

return registration; } } }

这个 DispatcherServletAutoConfiguration 自动配置类,会在你引入 spring-boot-starter-web 模块后生效,因为该模块引入了 spring mvc 和 tomcat 相关依赖,关于 Spring Boot 的自动配置功能在后续文章进行分析。

在这里会注入 DispatcherServletRegistrationBean(继承 RegistrationBean )对象,它关联着一个 DispatcherServlet 对象。在后面会讲到 Spring Boot 会找到所有 RegistrationBean对象,然后往 Servlet 上下文中添加 Servlet 或者 Filter。

ServletWebServerApplicationContext

org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext,Spring Boot 应用 SERVLET 类型(默认)对应的 Spring 上下文对象

接下来,我们一起来看看它重写的 onRefresh() 和 finishRefresh() 方法

1. onRefresh 方法

首先会调用父类方法,初始化 ThemeSource 对象,然后调用自己的 createWebServer() 方法,创建一个 WebServer 服务(默认 Tomcat),并初始化 ServletContext 上下文,如下:

// <5> 将 ServletContext 的一些初始化参数关联到当前 Spring 应用的 Environment 环境中 initPropertySources(); }

过程如下:

  1. 获取当前 WebServer 容器对象,首次进来为

  2. 获取 ServletContext 上下文对象

    this.servletContext; }
  3. 如果 WebServer 和 ServletContext 都为空,则需要创建一个,此时使用 Spring Boot 内嵌 Tomcat 容器则会进入该分支

    1. 获取 Servlet 容器工厂对象(默认为 Tomcat)factory,如下:

      0], ServletWebServerFactory.class); }

      在 spring-boot-autoconfigure 中有一个 ServletWebServerFactoryConfiguration 配置类会注册一个 TomcatServletWebServerFactory 对象

      加上 TomcatServletWebServerFactoryCustomizer 自动配置类,可以将 server.* 相关的配置设置到该对象中,这一步不深入分析,感兴趣可以去看一看

    2. 先创建一个 ServletContextInitializer Servlet 上下文初始器,实现也就是当前类的 this#selfInitialize(ServletContext) 方法,如下:

      this::selfInitialize; }

      这个 ServletContextInitializer 在后面会被调用,请记住这个方法

    3. 从 factory 工厂中创建一个 WebServer 容器对象,例如创建一个 TomcatWebServer 容器对象,并初始化 ServletContext 上下文,该过程会创建一个 Tomcat 容器并启动,启动过程异步触发了 TomcatStarter#onStartup 方法,也就会调用第 2 步的 ServletContextInitializer#selfInitialize(ServletContext) 方法

  4. 否则,如果 ServletContext 不为空,说明使用了外部的 Servlet 容器(例如 Tomcat)

    1. 那么这里主动调用 this#selfInitialize(ServletContext) 方法来注册各种 Servlet、Filter
  5. 将 ServletContext 的一些初始化参数关联到当前 Spring 应用的 Environment 环境中

整个过程有点绕,如果获取到的 WebServer 和 ServletContext 都为空,说明需要使用内嵌的 Tomcat 容器,那么第 3 步就开始进行 Tomcat 的初始化工作;

这里第 4 步的分支也很关键,如果 ServletContext 不为空,说明使用了外部的 Servlet 容器(例如 Tomcat),关于 Spring Boot 应用打成 war 包支持放入外部的 Servlet 容器运行的原理在下一篇文章进行分析。

TomcatServletWebServerFactory

org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory,Tomcat 容器工厂,用于创建 TomcatWebServer 对象

1.1 getWebServer 方法

getWebServer(ServletContextInitializer... initializers) 方法,创建一个 TomcatWebServer 容器对象,并初始化 ServletContext 上下文,创建 Tomcat 容器并启动

return getTomcatWebServer(tomcat); }

过程如下:

  1. 禁用 MBean 注册中心

  2. 创建一个 Tomcat 对象 tomcat

  3. 创建一个临时目录(退出时删除)

  4. 将这个临时目录作为 Tomcat 的目录

  5. 创建一个 NIO 协议的 Connector 连接器对象,并添加到第 2 步创建的 tomcat 中

  6. 对 Connector 进行配置,设置 server.port 端口、编码、server.tomcat.min-spare-threads 最小空闲线程和 server.tomcat.accept-count 最大线程数。这些配置就是我们自己配置的,在前面 1. onRefresh 方法 的第 3 步有提到

    // 例如设置 `server.tomcat.min-spare-threads` 最小空闲线程和 `server.tomcat.accept-count` 最大线程数 customizer.customize(connector); } }
  7. 禁止自动部署

  8. 同时支持多个 Connector 连接器(默认没有)

  9. 调用 prepareContext(..) 方法,创建一个 TomcatEmbeddedContext 上下文对象,并进行初始化工作,配置 TomcatStarter 作为启动器,会将这个上下文对象设置到当前 tomcat 中去

  10. 调用 getTomcatWebServer(Tomcat) 方法,创建一个 TomcatWebServer 容器对象,是对 tomcat 的封装,用于控制 Tomcat 服务器

整个 Tomcat 的初始化过程没有特别的复杂,主要是因为这里没有深入分析,我们知道大致的流程即可,这里我们重点关注第 9 和 10 步,接下来依次分析

1.1.1 prepareContext 方法

prepareContext(Host, ServletContextInitializer[]) 方法,创建一个 TomcatEmbeddedContext 上下文对象,并进行初始化工作,配置 TomcatStarter 作为启动器,会将这个上下文对象设置到 Tomcat 的 Host 中去,如下:

// <6> 对 TomcatEmbeddedContext 进行配置,例如配置 TomcatStarter 启动器,它是对 ServletContext 上下文对象的初始器 `initializersToUse` 的封装 configureContext(context, initializersToUse); postProcessContext(context); }

整个过程我们挑主要的流程来看:

  1. 创建一个 TomcatEmbeddedContext 上下文对象 context,接下来进行一系列的配置

  2. 设置 context-path

  3. 设置 Tomcat 根目录

  4. 注册默认的 Servlet 为 org.apache.catalina.servlets.DefaultServlet

  5. 将这个 context 上下文对象添加到 tomcat 中去

  6. 调用 configureContext(..) 方法,对 context 进行配置,例如配置 TomcatStarter 启动器,它是对 ServletContext 上下文对象的初始器 initializersToUse 的封装

可以看到 Tomcat 上下文对象设置了 context-path,也就是我们的配置的 server.servlet.context-path 属性值。

同时,在第 6 步会调用方法对 Tomcat 上下文对象进一步配置

1.1.2 configureContext 方法

configureContext(Context, ServletContextInitializer[]) 方法,对 Tomcat 上下文对象,主要配置 TomcatStarter 启动器,如下:

this.tomcatContextCustomizers) { customizer.customize(context); } }

配置过程如下:

  1. 创建一个 TomcatStarter 启动器,此时把 ServletContextInitializer 数组传入进去了,并设置到 context 上下文中

  2. 设置错误页面

  3. 配置 context 上下文的 Session 会话,例如超时会话时间

  4. 对 context 上下文进行自定义处理,例如添加 WsContextListener 监听器

重点来了,这里设置了一个 TomcatStarter 对象,它实现了 javax.servlet.ServletContainerInitializer 接口,目的就是触发 Spring Boot 自己的 ServletContextInitializer 这个对象。

注意,入参中的 ServletContextInitializer 数组是什么,你可以一直往回跳,有一个对象就是 ServletWebServerApplicationContext#selfInitialize(ServletContext) 这个方法,到时候会触发它。关键!!!

javax.servlet.ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup(..) 方法处理,我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类,过滤掉不希望给 onStartup(..) 处理的类。

至于为什么这样做,可参考我的 《精尽Spring MVC源码分析 - 寻找遗失的 web.xml》 这篇文章的说明

1.1.3 getTomcatWebServer 方法

getTomcatWebServer(Tomcat) 方法,创建一个 TomcatWebServer 容器对象,是对 tomcat 的封装,用于控制 Tomcat 服务器,如下:

0); }

可以看到,这里创建了一个 TomcatWebServer 对象,是对 tomcat 的封装,用于控制 Tomcat 服务器,但是,Tomcat 在哪启动的呢?

别急,在它的构造方法中还有一些初始化工作

TomcatWebServer

org.springframework.boot.web.embedded.tomcat.TomcatWebServer,对 Tomcat 的封装,用于控制 Tomcat 服务器

@link TomcatStarter#onStartup} 方法 */ initialize(); }

当你创建该对象时,会调用 initialize() 方法进行一些初始化工作

1.1.4 initialize 方法

initialize() 方法,初始化 Tomcat 容器,并异步触发了 TomcatStarter#onStartup 方法

可以看到,这个方法的关键在于 this.tomcat.start() 这一步,启动 Tomcat 容器,那么会触发 javax.servlet.ServletContainerInitializer 的 onStartup(..) 方法

在上面的 1.1.2 configureContext 方法 和 1.1.3 getTomcatWebServer 方法 小节中也讲到过,有一个 TomcatStarter 对象,也就会触发它的 onStartup(..) 方法

那么 TomcatStarter 内部封装了一些 Spring Boot 的 ServletContextInitializer 对象,其中有一个实现类是ServletWebServerApplicationContext#selfInitialize(ServletContext) 匿名方法

TomcatStarter

org.springframework.boot.web.embedded.tomcat.TomcatStarter,实现 javax.servlet.ServletContainerInitializer 接口,用于触发 Spring Boot 的 ServletContextInitializer 对象

this.startUpException; } }

在实现方法 onStartup(..) 中逻辑比较简单,就是调用 Spring Boot 自己的 ServletContextInitializer 实现类,例如 ServletWebServerApplicationContext#selfInitialize(ServletContext) 匿名方法

至于 TomcatStarter 为什么这做,是 Spring Boot 有意而为之,我们在使用 Spring Boot 时,开发阶段一般都是使用内嵌 Tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用 java -jar 的方式运行;另一种是打成 war 包,交给外置容器去运行。

前者就会导致容器搜索算法出现问题,因为这是 jar 包的运行策略,不会按照 Servlet 3.0 的策略去加载 ServletContainerInitializer

所以 Spring Boot 提供了 ServletContextInitializer 去替代。

2. selfInitialize 方法

该方法在 org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext 中,如下:

this::selfInitialize; }

思路是不是清晰明了了,前面一直没有提到 Servlet 和 Filter 是在哪添加至 Servlet 上下文中的,答案将在这里被揭晓

for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); } }

过程如下:

  1. 将当前 Spring 应用上下文设置到 ServletContext 上下文的属性中,同时将 ServletContext 上下文设置到 Spring 应用上下文中

  2. 向 Spring 应用上下文注册一个 ServletContextScope 对象(ServletContext 的封装)

  3. 向 Spring 应用上下文注册 contextParameters 和 contextAttributes 属性(会先被封装成 Map)

  4. 【重点】调用 getServletContextInitializerBeans() 方法,先从 Spring 应用上下文找到所有的 ServletContextInitializer 对象,也就会找到各种 RegistrationBean,然后依次调用他们的 onStartup 方法,向 ServletContext 上下文注册 Servlet、Filter 和 EventListener

    new ServletContextInitializerBeans(getBeanFactory()); }

重点在于上面的第 4 步,创建了一个 ServletContextInitializerBeans 对象,实现了 Collection 集合接口,所以可以遍历

它会找到所有的 RegistrationBean(实现了 ServletContextInitializer 接口),然后调用他们的 onStartup(ServletContext) 方法,也就会往 ServletContext 中添加他们对应的 Servlet 或 Filter 或 EventListener 对象,这个方法比较简单,在后面讲到的 RegistrationBean 小节中会提到

继续往下看

ServletContextInitializerBeans

org.springframework.boot.web.servlet.ServletContextInitializerBeans,对 ServletContextInitializer 实现类的封装,会找到所有的 ServletContextInitializer 实现类

this.initializers); } }

过程如下:

  1. 设置类型为 ServletContextInitializer
  2. 找到 IoC 容器中所有 ServletContextInitializer 类型的 Bean,并将这些信息添加到 seen 和 initializers 集合中
  3. 从 IoC 容器中获取 Servlet or Filter or EventListener 类型的 Bean,适配成 RegistrationBean 对象,并添加到 initializers 和 seen 集合中
  4. 将 initializers 中的所有 ServletContextInitializer 进行排序,并保存至 sortedList 集合中
  5. DEBUG 模式下打印日志

比较简单,这里就不继续往下分析源码了,感兴趣可以看一看 ServletContextInitializerBeans.java

这里你要知道 RegistrationBean 实现了 ServletContextInitializer 接口,我们的 Spring Boot 应用如果要添加 Servlet 或者 Filter,可以注入一个 ServletRegistrationBean<T extends Servlet> 或者 FilterRegistrationBean<T extends Filter> 类型的 Bean

RegistrationBean

org.springframework.boot.web.servlet.RegistrationBean,基于 Servlet 3.0+,往 ServletContext 注册 Servlet、Filter 和 EventListener

// 抽象方法,交由子类实现 register(description, servletContext); } }

类图:

精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现

DynamicRegistrationBean

this.initParameters); } } }

ServletRegistrationBean

this.multipartConfig); } } }

DispatcherServletRegistrationBean

3. finishRefresh 方法

this)); } }

首先会调用父类方法,会发布 ContextRefreshedEvent 上下文刷新事件,然后调用自己的 startWebServer() 方法,启动上面 2. onRefresh 方法 创建的 WebServer

因为上面仅启动 Tomcat 容器,Servlet 添加到了 ServletContext 上下文中,这里启动 TomcatWebServer 容器对象,会对每一个 TomcatEmbeddedContext 中的 Servlet 进行加载并初始化,如下:

return webServer; }

TomcatWebServer

org.springframework.boot.web.embedded.tomcat.TomcatWebServer,对 Tomcat 的封装,用于控制 Tomcat 服务器

3.1 start 方法

start() 方法,启动 TomcatWebServer 服务器,初始化前面已添加的 Servlet 对象们

finally { Context context = findContext(); ContextBindings.unbindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); } } }

加锁启动,已启动则跳过

关键在于 performDeferredLoadOnStartup() 这个方法,对每一个 TomcatEmbeddedContext 中的 Servlet 进行加载并初始化,先找到容器中所有的 org.apache.catalina.Wrapper,它是对 javax.servlet.Servlet 的封装,依次加载并初始化它们

好了,到这里 Spring Boot 内嵌的 Tomcat 容器差不多准备就绪了,继续往下追究就涉及到 Tomcat 底层的东西了,所以这里点到为止

总结

本文分析了 Spring Boot 内嵌 Tomcat 容器的实现,主要是 Spring Boot 的 Spring 应用上下文(ServletWebServerApplicationContext)在 refresh() 刷新阶段进行了扩展,分别在 onRefresh() 和 finishRefresh() 两个地方,可以跳到前面的 回顾 小节中看看,分别做了以下事情:

  1. 创建一个 WebServer 服务对象,例如 TomcatWebServer 对象,对 Tomcat 的封装,用于控制 Tomcat 服务器
    1. 先创建一个 org.apache.catalina.startup.Tomcat 对象 tomcat,使用临时目录作为基础目录(tomcat.端口号),退出时删除,同时会设置端口、编码、最小空闲线程和最大线程数
    2. 为 tomcat 创建一个 TomcatEmbeddedContext 上下文对象,会添加一个 TomcatStarter(实现 javax.servlet.ServletContainerInitializer 接口)到这个上下文对象中
    3. 将 tomcat 封装到 TomcatWebServer 对象中,实例化过程会启动 tomcat,启动后会触发 javax.servlet.ServletContainerInitializer 实现类的回调,也就会触发 TomcatStarter 的回调,在其内部会调用 Spring Boot 自己的 ServletContextInitializer 初始器,例如 ServletWebServerApplicationContext#selfInitialize(ServletContext) 匿名方法
    4. 在这个匿名方法中会找到所有的 RegistrationBean,执行他们的 onStartup 方法,将其关联的 Servlet、Filter 和 EventListener 添加至 Servlet 上下文中,包括 Spring MVC 的 DispatcherServlet 对象
  2. 启动上一步创建的 TomcatWebServer 对象,上面仅启动 Tomcat 容器,Servlet 添加到了 ServletContext 上下文中,这里会将这些 Servlet 进行加载并初始化

这样一来就完成 Spring Boot 内嵌的 Tomcat 就启动完成了,关于 Spring MVC 相关内容可查看 《精尽 Spring MVC 源码分析 - 文章导读》 这篇文章。

ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup() 方法处理,我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类,过滤掉不希望给 onStartup() 处理的类。

你是否有一个疑问,Spring Boot 不也是支持打成 war 包,然后放入外部的 Tomcat 容器运行,这种方式的实现在哪里呢?我们在下一篇文章进行分析

 

相关文章:

  • 2021-12-04
  • 2021-06-19
  • 2019-11-21
  • 2021-10-21
  • 2020-12-22
  • 2020-12-22
  • 2020-12-23
  • 2020-12-23
猜你喜欢
  • 2021-09-02
  • 2021-07-02
  • 2021-07-09
  • 2021-06-29
  • 2021-07-01
  • 2021-07-08
  • 2021-07-07
相关资源
相似解决方案