精尽Spring Boot源码分析 - 支持外部 Tomcat 容器的实现
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读
Spring Boot 版本:2.2.x
最好对 Spring 源码有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章导读》 系列文章
如果该篇内容对您有帮助,麻烦点击一下“推荐”,也可以关注博主,感激不尽~
该系列其他文章请查看:《精尽 Spring Boot 源码分析 - 文章导读》
概述
我们知道 Spring Boot 应用能够被打成 war 包,放入外部 Tomcat 容器中运行。你是否知道 Spring Boot 是如何整合 Spring MVC 的呢?
在上一篇 《Spring Boot 内嵌 Tomcat 容器的实现》 文章中分析了 Spring Boot 白打成 jar 包后是如何创建 Tomcat 容器并启动的,那么这篇文章主要告诉你 Spring Boot 应用被打成 war 包后放入外部 Tomcat 容器是如何运行的。
如何使用
在我们的 Spring Boot 项目中通常会引入 spring-boot-starter-web 这个依赖,该模块提供全栈的 WEB 开发特性,包括 Spring MVC 依赖和 Tomcat 容器,我们将内部 Tomcat 的 Starter 模块排除掉,如下:
<packaging>war</packaging>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
然后启动类这样写:
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
// 可不写
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(Application.class);
}
}
这样你打成 war 包就可以放入外部的 Servlet 容器中运行了。
实现原理
原理在分析 Spring MVC 源码的时候讲过,参考我的 《精尽Spring MVC源码分析 - 寻找遗失的 web.xml》 这篇文章
借助于 Servlet 3.0 的一个新特性,新增的一个 javax.servlet.ServletContainerInitializer 接口,在 Servlet 容器启动时会通过 Java 的 SPI 机制从 META-INF/services/javax.servlet.ServletContainerInitializer 文件中找到这个接口的实现类,然后调用它的 onStartup(..) 方法。
在 Spring 的 spring-web 模块中该文件是这么配置的:
org.springframework.web.SpringServletContainerInitializer
一起来看看这个类:
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
} catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
通过 @HandlesTypes 注解指定只处理 WebApplicationInitializer 类型的类
这个过程很简单,实例化所有 WebApplicationInitializer 类型的对象,然后依次调用它们的 onStartup(ServletContext) 方法

通过打断点你会发现,有一个 DemoApplication 就是我们的启动类
这也就是为什么如果你的 Spring Boot 应用需要打成 war 包放入外部 Tomcat 容器运行的时候,你的启动类需要继承 SpringBootServletInitializer 这个抽象类,因为这个抽象类实现类 WebApplicationInitializer 接口,我们只需要继承它即可
SpringBootServletInitializer
org.springframework.boot.web.servlet.support.SpringBootServletInitializer 抽象类,实现了 WebApplicationInitializer 接口,目的就是支持你将 Spring Boot 应用打包成 war 包放入外部的 Servlet 容器中运行
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
protected Log logger; // Don't initialize early
private boolean registerErrorPageFilter = true;
protected final void setRegisterErrorPageFilter(boolean registerErrorPageFilter) {
this.registerErrorPageFilter = registerErrorPageFilter;
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// Logger initialization is deferred in case an ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
// <1> 创建一个 WebApplicationContext 作为 Root Spring 应用上下文
WebApplicationContext rootAppContext = createRootApplicationContext(servletContext);
if (rootAppContext != null) {
// <2> 添加一个 ContextLoaderListener 监听器,会监听到 ServletContext 的启动事件
// 因为 Spring 应用上下文在上面第 `1` 步已经准备好了,所以这里什么都不用做
servletContext.addListener(new ContextLoaderListener(rootAppContext) {
@Override
public void contextInitialized(ServletContextEvent event) {
// no-op because the application context is already initialized
}
});
} else {
this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not "
+ "return an application context");
}
}
}
在 onStartup(ServletContext) 方法中就两步:
- 调用
createRootApplicationContext(ServletContext)方法,创建一个 WebApplicationContext 作为 Root Spring 应用上下文 - 添加一个 ContextLoaderListener 监听器,会监听到 ServletContext 的启动事件,因为 Spring 应用上下文在上面第
1步已经准备好了,所以这里什么都不用做
第 1 步是不是和 Spring MVC 类似,同样创建一个 Root WebApplicationContext 作为 Spring 应用上下文的父对象
createRootApplicationContext 方法
createRootApplicationContext(ServletContext) 方法,创建一个 Root WebApplicationContext 对象,如下:
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
// <1> 创建一个 SpringApplication 构造器
SpringApplicationBuilder builder = createSpringApplicationBuilder();
// <2> 设置 `mainApplicationClass`,主要用于打印日志
builder.main(getClass());
// <3> 从 ServletContext 上下文中获取最顶部的 Root ApplicationContext 应用上下文
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
// <4> 如果已存在 Root ApplicationContext,则先置空,因为这里会创建一个 ApplicationContext 作为 Root
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
// <4.1> 添加一个 ApplicationContextInitializer 初始器,
// 用于设置现在要创建的 Root ApplicationContext 应用上下文的父容器为 `parent`
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
/**
* <5> 添加一个 ApplicationContextInitializer 初始器
* 目的是往 ServletContext 上下文中设置 Root ApplicationContext 为现在要创建的 Root ApplicationContext 应用上下文
* 并将这个 ServletContext 保存至 ApplicationContext 中,参考 {@link ServletWebServerApplicationContext#createWebServer()} 方法,
* 如果获取到了 ServletContext 那么直接调用其 {@link ServletWebServerApplicationContext#selfInitialize} 方法来注册各个 Servlet、Filter
* 例如 {@link DispatcherServlet}
*/
builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
// <6> 设置要创建的 Root ApplicationContext 应用上下文的类型(Servlet)
builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
// <7> 对 SpringApplicationBuilder 进行扩展
builder = configure(builder);
// <8> 添加一个 ApplicationListener 监听器
// 用于将 ServletContext 中的相关属性关联到 Environment 环境中
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
// <9> 构建一个 SpringApplication 对象,用于启动 Spring 应用
SpringApplication application = builder.build();
// <10> 如果没有设置 `source` 源对象,那么这里尝试设置为当前 Class 对象,需要有 `@Configuration` 注解
if (application.getAllSources().isEmpty()
&& MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
application.addPrimarySources(Collections.singleton(getClass()));
}
// <11> 因为 SpringApplication 在创建 ApplicationContext 应用上下文的过程中需要优先注册 `source` 源对象,如果为空则抛出异常
Assert.state(!application.getAllSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {
// <12> 添加一个错误页面 Filter 作为 `sources`
application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
}
// <13> 调用 `application` 的 `run` 方法启动整个 Spring Boot 应用
return run(application);
}
过程如下:
创建一个 SpringApplication 构造器,目的就是启动 Spring 应用咯
protected SpringApplicationBuilder createSpringApplicationBuilder() {
return new SpringApplicationBuilder();
}
设置
mainApplicationClass,也就是你的启动类,主要用于打印日志从 ServletContext 上下文中获取最顶部的 Root ApplicationContext 应用上下文
parent,通常这里没有父对象,所以为空如果
parent不为空,则先 ServletContext 中的该属性置空,因为这里会创建一个 ApplicationContext 作为 Root- 添加一个
ApplicationContextInitializer初始器,用于设置现在要创建的 Root ApplicationContext 应用上下文的父容器为parent
- 添加一个
添加一个
ApplicationContextInitializer初始器,目的是往 ServletContext 上下文中设置 Root ApplicationContext 为现在要创建的 Root ApplicationContext 应用上下文,并将这个 ServletContext 保存至 ApplicationContext 中注意,这个对象很关键,会将当前 ServletContext 上下文对象设置到 ApplicationContext 对象里面,那么后续就不会再创建 Spring Boot 内嵌的 Tomcat 了
设置要创建的 Root ApplicationContext 应用上下文的类型(Servlet)
对 SpringApplicationBuilder 进行扩展,调用
configure(SpringApplicationBuilder)方法,这也就是为什么我们的启动类可以重写该方法,通常不用做什么添加一个 ApplicationListener 监听器,用于将 ServletContext 中的相关属性关联到 Environment 环境中
构建一个 SpringApplication 对象
application,用于启动 Spring 应用如果没有设置
source源对象,那么这里尝试设置为当前 Class 对象,需要有@Configuration注解因为 SpringApplication 在创建 ApplicationContext 应用上下文的过程中需要优先注册
source源对象,如果为空则抛出异常添加一个错误页面 Filter 作为
sources调用
application的run方法启动整个 Spring Boot 应用
整个过程不复杂,SpringApplication 相关的内容在前面的 《SpringApplication 启动类的启动过程》文章中已经分析过,这里的关键在于第 5 步
添加的 ServletContextApplicationContextInitializer 会将当前 ServletContext 上下文对象设置到 ApplicationContext 对象里面
ServletContextApplicationContextInitializer
public class ServletContextApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableWebApplicationContext>, Ordered {
private int order = Ordered.HIGHEST_PRECEDENCE;
private final ServletContext servletContext;
private final boolean addApplicationContextAttribute;
public ServletContextApplicationContextInitializer(ServletContext servletContext) {
this(servletContext, false);
}
public ServletContextApplicationContextInitializer(ServletContext servletContext,
boolean addApplicationContextAttribute) {
this.servletContext = servletContext;
this.addApplicationContextAttribute = addApplicationContextAttribute;
}
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public void initialize(ConfigurableWebApplicationContext applicationContext) {
// 将这个 ServletContext 上下文对象设置到 ApplicationContext 中
applicationContext.setServletContext(this.servletContext);
if (this.addApplicationContextAttribute) {
this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
applicationContext);
}
}
}
可以看到会将这个 ServletContext 上下文对象设置到 ApplicationContext 中
那么我们回顾到上一篇 《Spring Boot 内嵌 Tomcat 容器的实现》 文章的 1. onRefresh 方法小节调用的 createWebServer() 方法,如下:
// ServletWebServerApplicationContext.java
private void createWebServer() {
// <1> 获取当前 `WebServer` 容器对象,首次进来为空
WebServer webServer = this.webServer;
// <2> 获取 `ServletContext` 上下文对象
ServletContext servletContext = getServletContext();
// <3> 如果 WebServer 和 ServletContext 都为空,则需要创建一个
// 使用 Spring Boot 内嵌 Tomcat 容器则会进入该分支
if (webServer == null && servletContext == null) {
// <3.1> 获取 Servlet 容器工厂对象(默认为 Tomcat)`factory`
ServletWebServerFactory factory = getWebServerFactory();
/**
* <3.2> 先创建一个 {@link ServletContextInitializer} Servlet 上下文初始器,实现也就是当前类的 {@link this#selfInitialize(ServletContext)} 方法
* 至于为什么不用 Servlet 3.0 新增的 {@link javax.servlet.ServletContainerInitializer} 这个类,我在
* [精尽Spring MVC源码分析 - 寻找遗失的 web.xml](https://www.cnblogs.com/lifullmoon/p/14122704.html)有提到过
*
* <3.3> 从 `factory` 工厂中创建一个 WebServer 容器对象
* 例如创建一个 {@link TomcatWebServer} 容器对象,并初始化 `ServletContext` 上下文,创建 {@link Tomcat} 容器并启动
* 启动过程异步触发了 {@link org.springframework.boot.web.embedded.tomcat.TomcatStarter#onStartup} 方法
* 也就会调用这个传入的 {@link ServletContextInitializer} 的 {@link #selfInitialize(ServletContext)} 方法
*/
this.webServer = factory.getWebServer(getSelfInitializer());
}
// <4> 否则,如果 ServletContext 不为空,说明使用了外部的 Servlet 容器(例如 Tomcat)
else if (servletContext != null) {
try {
/** 那么这里主动调用 {@link this#selfInitialize(ServletContext)} 方法来注册各种 Servlet、Filter */
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
// <5> 将 ServletContext 的一些初始化参数关联到当前 Spring 应用的 Environment 环境中
initPropertySources();
}
我们看到上面第 4 步,如果从当前 Spring 应用上下文获取到了 ServletContext 对象,不会走上面的第 3 步,也就是不创建 Spring Boot 内嵌的 Tomcat
主动调用它的 getSelfInitializer() 方法来往这个 ServletContext 对象中注册各种 Servlet、Filter 和 EventListener 对象,包括 Spring MVC 中的 DispatcherServlet 对象,该方法参考上一篇 《Spring Boot 内嵌 Tomcat 容器的实现》 文章的 2. selfInitialize 方法 小节
总结
本文分析了 Spring Boot 应用被打成 war 包后是如何支持放入外部 Tomcat 容器运行的,原理也比较简单,借助 Spring MVC 中的 SpringServletContainerInitializer 这个类,它实现了 Servlet 3.0 新增的 javax.servlet.ServletContainerInitializer 接口
通过 Java 的 SPI 机制,在
META-INF/services/javax.servlet.ServletContainerInitializer文件中写入SpringServletContainerInitializer这个类,那么在 Servlet 容器启动的时候会调用这个类的onStartup(..)方法,会找到WebApplicationInitializer类型的对象,并调用他们的onStartup(ServletContext)方法在我们的 Spring Boot 应用中,如果需要打成
war包放入外部 Tomcat 容器运行,启动类则需要继承SpringBootServletInitializer抽象类,它实现了WebApplicationInitializer接口在
SpringBootServletInitializer中会创建一个 WebApplicationContext 作为 Root Spring 应用上下文,同时会将 ServletContext 对象设置到 Spring 应用上下文中这样一来,因为已经存在 ServletContext 对象,那么不会再创建 Spring Boot 内嵌的 Tomcat 容器,而是对 ServletContext 进行一些初始化工作
好了,到这里关于 Spring Boot 启动 Spring 应用的整个主流程,包括内嵌 Tomcat 容器的实现,以及支持运行在外部 Servlet 容器的实现都分析完了
那么接下来,我们一起来看看 @SpringBootApplication 这个注解,也就是 @EnableAutoConfiguration 自动配置注解的实现原理
精尽Spring Boot源码分析 - 支持外部 Tomcat 容器的实现的更多相关文章
- 精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- 精尽Spring Boot源码分析 - 文章导读
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- 精尽Spring Boot源码分析 - SpringApplication 启动类的启动过程
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- 精尽Spring Boot源码分析 - 剖析 @SpringBootApplication 注解
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- 精尽Spring Boot源码分析 - 配置加载
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- 精尽Spring Boot源码分析 - @ConfigurationProperties 注解的实现
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- 精尽Spring Boot源码分析 - Condition 接口的扩展
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- 精尽Spring Boot源码分析 - 日志系统
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- 精尽Spring Boot源码分析 - 序言
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
随机推荐
- [刷题] PTA 7-64 最长对称子串
7-64 最长对称子串 我的代码: 1 #include<stdio.h> 2 #include<string.h> 3 #define N 1001 4 5 int main ...
- 将top命令的输出,写入到文件中 top -b -n 1 -d 3 >>file.txt
top -b -n 1 -d 3 >>file.txt 解析: -b :batch模式,可以重定向到文件中 -n 1:一共取1次top数据.后边加数字,表示次数 -d 3:每次top时间间 ...
- [文章]Linux宕机故障分析案例
[文章]Linux宕机故障分析案例 已采纳 收藏 0 1669 0 马化辉 2018-07-01发布 背景 在Linux系统环境下,服务器宕机发生的频率比较小,但是不少工程师或多或少都会遇到这 ...
- 040.Python进程和Join
一 进程相关介绍 1.1 进程的概念(process) 进程就是正在运行的程序,它是操作系统中,资源分配的最小单位 资源分配:分配的是cpu和内存等物理资源 进程号是程的唯标识 同-个程序执行两次之 ...
- 2020-1-19 2.港股打新、REITs和分拆
1.港股打新介绍 港股打新升级版 财属目由尽握手中 港股中签率较高 A股提高中签率 港股提高中签之后的收益率 有可能破发 2.第一版港股打新 港股打新第一版 ■第一种策略:只选择低于1.5港币的 配售 ...
- 快成物流科技 x mPaaS | 小程序容器加持下的技术架构“提质增效”
导言 从 2017 年开始,GMTC"移动技术大会"就更名为"大前端技术大会".发展至今,混合开发.原生开发.前端开发等概念正在深度融合,组成"大 ...
- Mysql索引的创建与删除
1. 创建索引 1.1 使用Alter创建索引 1 添加主键索引 特点:数据列不允许重复,不能为null,一张表只能有一个主键:Mysql主动将该字段进行排序 ALTER TABLE 表名 ADD P ...
- JVM-运行时数据区之PC寄存器
1.运行时数据区图 运行时数据区是在类加载完成后所经历的阶段,当我们通过前面的:类的加载 --> 验证 --> 准备 --> 解析 --> 初始化,这几个阶段完成后,执行引擎就 ...
- Caffe实现概述
Caffe实现概述 目录 一.caffe配置文件介绍 二.标准层的定义 三.网络微调技巧 四.Linux脚本使用及LMDB文件生成 五.带你设计一个Caffe网络,用于分类任务 一.caffe配置文件 ...
- 共享CUDA内存
共享CUDA内存 进程间共享 此功能仅限于Linux. 将设备阵列导出到另一个进程 使用CUDA IPC API,可以与同一台计算机上的另一个进程共享设备阵列.为此,请使用.get_ipc_handl ...