前面的文章中,我们介绍了Tomcat容器的关键组件和类加载器,但是现在的J2EE开发中更多的是使用SpringBoot内嵌的Tomcat容器,而不是单独安装Tomcat应用。那么Spring是怎么和Tomcat容器进行集成?Spring和Tomcat容器的生命周期是如何同步?本文会详细介绍Spring和Tomcat容器的集成。

SpringBoot与Tomcat

使用SpringBoot搭建一个网页,应该是很多Spring学习者入门的案例。我们只需要在pom添加Spring的web-starter依赖,并添加对应的Controller,一键启动之后就可以得到一个完整的Web应用示例。

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

@RestController
@SpringBootApplication
public class Application { public static void main(String[] args) {
SpringApplication.run(Application.class, args);
} @RequestMapping("/")
public String hello(){
return "hello";
}
}

既然是一个Web应用,那么该应用必定启动了对应的Servlet容器,常见的Servlet容器有Tomcat/Undertow/jetty/netty等,SpringBoot对这些容器都有集成。本文会重点分析SpringBoot是如何集成Tomcat容器的。

如何判断是不是Web应用

我们知道SpringBoot不一定以Web应用的形式运行,还可以以桌面程序的形式运行,那么SpringBoot在应用中如何判断应用是不是一个Web应用程序,是不是需要启动Tomcat容器的呢?

Spring容器在容器启动的时候,会调用WebApplicationType.deduceFromClasspath()方法来推断当前的应用程序类型,从方法名字就可以看出,该方法是通过当前项目中的类来判断是不是Web项目的。以下为该方法的源码,当我们在项目中添加了spring-boot-starter-web的依赖之后,项目路径中会包含webMvc的类,对应的Spring应用也会被识别为Web应用。

private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" };

private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";

private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";

static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}

根据应用类型创建应用

通过项目中包含类的类型,Spring可以判断出当前应用的类型,之后Spring就需要根据应用类型去创建对应的ApplicationContext。从下面的程序中可以看出来,对于我们关注的普通web应用,Spring会创建一个AnnotationConfigServletWebServerApplicationContext

ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
switch (webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
}
catch (Exception ex) {
throw new IllegalStateException("Unable create a default ApplicationContext instance, "
+ "you may need a custom ApplicationContextFactory", ex);
}
};

AnnotationConfigServletWebServerApplicationContext是Web应用的Spring容器,我们可以推断,这个ApplicationContext容器中必定包含了servlet容器的初始化。去查看容器初始化的源码可以发现,在容器Refresh阶段会初始化WebServer,源码如下:

@Override
protected void onRefresh() {
super.onRefresh();
try {
// Spring容器Refresh阶段创建WebServer
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
} private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext(); // 没有初始化好的WebServer就需要初始化一个
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create"); // 获取ServletWebServerFactory,对于Tomcat来说获取到的就是TomcatServletWebServerFactory
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString()); // 创建Tomcat容器的WebServer
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}

Tomcat的初始化

通过上面的内容,我们知道SpringBoot会在启动的时候判断是不是Web应用并创建对应类型的Spring容器,对于Web应用会创建Web类型的ApplicationContext。 在Spring容器启动的时候会初始化WebServer,也就是初始化Tomcat容器。本节我们会分析Tomcat容器初始化源码的各个步骤。

获取ServletWebServerFactory

初始化Tomcat容器的过程中,第一步是获取创建Tomcat WebServer的工厂类TomcatServletWebServerFactory,分析源码可知,Spring是直接通过Bean的类型从Spring容器中获取ServletWebServerFactory的,所以Tomcat容器类型的SpringBoot应该在启动时向容器中注册TomcatServletWebServerFactory的实例作为一个Bean。

// 获取ServletWebServerFactory关键代码
factory = getWebServerFactory(); // 关键代码涉及的函数
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
+ "ServletWebServerFactory bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

创建WebServer的实例

拿到用于创建WebServer的ServletWebServerFactory,我们就可以开始着手创建WebServer了,创建WebServer的关键代码如下所示。

// 创建WebServer的实例关键代码
this.webServer = factory.getWebServer(getSelfInitializer());

创建WebServer的第一步是拿到创建时需要的参数,这个参数的类型是ServletContextInitializer,ServletContextInitializer的作用是用于初始化ServletContext,接口源码如下,从接口的注释中我们就可以看到,这个参数可以用于配置servlet容器的filters,listeners等信息。

@FunctionalInterface
public interface ServletContextInitializer { /**
* Configure the given {@link ServletContext} with any servlets, filters, listeners
* context-params and attributes necessary for initialization.
* @param servletContext the {@code ServletContext} to initialize
* @throws ServletException if any call against the given {@code ServletContext}
* throws a {@code ServletException}
*/
void onStartup(ServletContext servletContext) throws ServletException; }

Spring是通过getSelfInitializer()方法来获取初始化参数,查看getSelfInitializer()方法,可以发现该方法实现了如下功能:

  1. 绑定SpringBoot应用程序和ServletContext;
  2. 向SpringBoot注册ServletContext,Socpe为Application级别;
  3. 向SpringBoot上下文环境注册ServletContext环境相关的Bean;
  4. 获取容器中所有的ServletContextInitializer,依次处理ServletContext。
private ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
} private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}

获取到用于创建WebServer的参数之后,Spring就会调用工厂方法去创建Tomcat对应的WebServer。

    @Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}

Tomcat生命周期

我们在使用基于Spring MVC应用框架,只需要启动/关闭Spring应用,就可以同步启动/关闭Tomcat容器,那么Spring是如何做到的呢?从下面初始化Web容器的代码可以看到,Spring容器会注册两个和WebServer容器相关的生命周期Bean:

  1. 容器的优雅关闭Bea——webServerGracefulShutdown。
  2. 容器的生命周期管理的Bean——webServerStartStop
    getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));

Tomcat容器优雅关闭

这是SpringBoot在最新的2.X.X版本中新增的优雅停机功能,​ 优雅停机指的是Java项目在停机时需要做好断后工作。如果直接使用kill -9 方式暴力的将项目停掉,可能会导致正常处理的请求、定时任务、RMI、注销注册中心等出现数据不一致问题。如何解决优雅停机呢?大致需要解决如下问题:

  • 首先要确保不会再有新的请求进来,所以需要设置一个流量挡板
  • 保证正常处理已进来的请求线程,可以通过计数方式记录项目中的请求数量
  • 如果涉及到注册中心,则需要在第一步结束后注销注册中心
  • 停止项目中的定时任务
  • 停止线程池
  • 关闭其他需要关闭资源等等等

​ SpringBoot优雅停机出现之前,一般需要通过自研方式来保证优雅停机。我也见过有项目组使用 kill -9 或者执行 shutdown脚本直接停止运行的项目,当然这种方式不够优雅。

Spring提供Tomcat优雅关闭的核心类是WebServerGracefulShutdownLifecycle,可以等待用户的所有请求处理完成之后再关闭Tomcat容器,我们查看WebServerGracefulShutdownLifecycle的的关机关键源码如下:

    // WebServerGracefulShutdownLifecycle停机源码
@Override
public void stop(Runnable callback) {
this.running = false;
this.webServer.shutDownGracefully((result) -> callback.run());
} // tomcat web server shutDownGracefully源码
@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
if (this.gracefulShutdown == null) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
return;
}
this.gracefulShutdown.shutDownGracefully(callback);
}

此处出现了优雅关闭的工具类GracefulShutdown,Tomcat容器的GracefulShutdown源码如下所示,可以看到优雅关闭分为以下步骤:

  1. 关闭Tomcat容器的所有的连接器,连接器关闭之后会停止接受新的请求。
  2. 轮询所有的Context容器,等待这些容器中的请求被处理完成。
  3. 如果强行退出,那么就不等待所有容器中的请求处理完成。
  4. 回调优雅关闭的结果,有三种关闭结果:REQUESTS_ACTIVE有活跃请求的情况下强行关闭,IDLE所有请求完成之后关闭,IMMEDIATE没有任何等待立即关闭容器。
final class GracefulShutdown {

    void shutDownGracefully(GracefulShutdownCallback callback) {
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
new Thread(() -> doShutdown(callback), "tomcat-shutdown").start();
} private void doShutdown(GracefulShutdownCallback callback) {
// 关闭Tomcat的所有的连接器,不接受新的请求
List<Connector> connectors = getConnectors();
connectors.forEach(this::close);
try {
for (Container host : this.tomcat.getEngine().findChildren()) { // 轮询所有的Context容器
for (Container context : host.findChildren()) {
// 判断容器中的所有请求是不是已经结束。
while (isActive(context)) { // 强行退出的情况下不等待所有请求处理完成
if (this.aborted) {
logger.info("Graceful shutdown aborted with one or more requests still active");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
return;
}
Thread.sleep(50);
}
}
} }
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
}

Spring 容器是怎么知道关闭并进行回调的呢?本处只介绍kill -15工作原理:Spring容器在启动的时候会向JVM注册销毁回调方法,JVM在收到kill -15之后不会直接退出,而是会一一调用这些回调方法,然后Spring会在这些回调方法中进行优雅关闭,比如从注册中心删除注册信息,优雅关闭Tomcat等等。

我是御狐神,欢迎大家关注我的微信公众号:wzm2zsd

本文最先发布至微信公众号,版权所有,禁止转载!

学习Tomcat(七)之Spring内嵌Tomcat的更多相关文章

  1. Spring Boot内嵌Tomcat session超时问题

    最近让Spring Boot内嵌Tomcat的session超时问题给坑了一把. 在应用中需要设置session超时时间,然后就习惯的在application.properties配置文件中设置如下, ...

  2. 如何优雅的关闭基于Spring Boot 内嵌 Tomcat 的 Web 应用

    背景 最近在搞云化项目的启动脚本,觉得以往kill方式关闭服务项目太粗暴了,这种kill关闭应用的方式会让当前应用将所有处理中的请求丢弃,响应失败.这种形式的响应失败在处理重要业务逻辑中是要极力避免的 ...

  3. Spring Boot移除内嵌Tomcat,使用非web方式启动

    前言:当我们使用Spring Boot编写了一个批处理应用程序,该程序只是用于后台跑批数据,此时不需要内嵌的tomcat,简化启动方式使用非web方式启动项目,步骤如下: 1.在pom.xml文件中去 ...

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

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

  5. Spring Boot 内嵌Tomcat的端口号的修改

    操作非常的简单,不过如果从来没有操作过,也是需要查找一下资料的,所以,在此我简单的记录一下自己的操作步骤以备后用! 1:我的Eclipse版本,不同的开发工具可能有所差异,不过大同小异 2:如何进入对 ...

  6. spring boot 2 内嵌Tomcat Stopping service [Tomcat]

    我在使用springboot时,当代码有问题时,发现控制台打印下面信息: Connected to the target VM, address: '127.0.0.1:42091', transpo ...

  7. 内嵌tomcat最简单用法

    maven项目引入内嵌tomcat依赖 <dependency> <groupId>org.apache.tomcat.embed</groupId> <ar ...

  8. 查看和指定SpringBoot内嵌Tomcat的版本

    查看当前使用的Tomcat版本号 Maven Repository中查看 比如我们需要查Spring Boot 2.1.4-RELEASE的内嵌Tomcat版本, 可以打开链接: https://mv ...

  9. 基于内嵌Tomcat的应用开发

    为什么使用内嵌Tomcat开发? 开发人员无需搭建Tomcat的环境就可以使用内嵌式Tomcat进行开发,减少搭建J2EE容器环境的时间和开发时容器频繁启动所花时间,提高开发的效率. 怎么搭建内嵌To ...

随机推荐

  1. 测试工具Wiremock介绍

    WireMock是一个开源的测试工具,支持HTTP响应存根.请求验证.代理/拦截.记录和回放.最直接的用法: 为Web/移动应用构建Mock Service 快速创建Web API原型 模拟Web S ...

  2. 你不知道的echarts,前端鲍哥带你研究!

    前言 相信不少前端小伙伴刚接触 e-charts 肯定有点陌生,但是echarts咱不清楚,charts我们应该很熟悉,没错,echarts 就是我们日常可见的图表,不同的是 echarts 是用代码 ...

  3. 证明:(a,[b,c]) = [(a,b),(a,c)]

    这题是潘承洞.潘承彪所著<初等数论>(第三版)第一章第5节里一个例题,书中采用算术基本定理证明,并指出要直接用第4节的方法来证是较困难的. 现采用第4节的方法(即最大公约数理论里的几个常用 ...

  4. 基于Ubuntu18.04一站式部署(python-mysql-redis-nginx)

    基于Ubuntu18.04一站式部署 Python3.6.8的安装 1. 安装依赖 ~$ sudo apt install openssl* zlib* 2. 安装python3.6.8(个人建议从官 ...

  5. Redis集群的搭建及与SpringBoot的整合

    1.概述 之前聊了Redis的哨兵模式,哨兵模式解决了读的并发问题,也解决了Master节点单点的问题. 但随着系统越来越庞大,缓存的数据越来越多,服务器的内存容量又成了问题,需要水平扩容,此时哨兵模 ...

  6. AQS深入分析

    一.node概念 1.当线程获取锁失败时,会被打包成一个node放到同步队列中 2.node属性 当线程获取锁失败时,会被打包成一个node放到同步队列中,所以node属性中有一个thread属性; ...

  7. 菜狗、《灵笼》、《时光代理人》,重新审视Z世代的电商逻辑

    来源:懂懂笔记 B站还有多少潜力可以挖掘? 虽然B站的最新财报依然还是亏损,但同时也让人看到更多的可能性. 从财报数据的亮点来看,一是营收增长,B站二季度营收为44.95亿元,同比增长72%.营收上B ...

  8. C# 反射 + Quartz,实现流程处理

    场景: 前不久,公司里项目经理要求我实现流程处理,比如,用户可以定义一个定时任务,每周一查看报表.定时任务很简单,用Quartz可以实现,但是用户自己选择报表就比较麻烦,因为系统的不同模块的生成报表的 ...

  9. vue-cli3 创建多页面应用项目

    1.创建vue项目 cmd命令执行  vue create ruc-continuing  创建vue项目,项目名称:ruc-continuing 选择一个 preset(预置项),或自定义: 选择自 ...

  10. CodeForce-799C Fountains (记忆化DP)

    Fountains CodeForces - 799C 某土豪想要造两座喷泉.现在有 n 个造喷泉的方案,我们已知每个方案的价格以及美观度.有两种合法的货币:金币和钻石.这两种货币之间不能以任何方式转 ...