《SpringBoot启动流程七》:源码分析SpringBoot如何内嵌并启动Tomcat服务器的?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 《SpringBoot启动流程七》:源码分析SpringBoot如何内嵌并启动Tomcat服务器的?

@[toc]

一、前言

在前面的文章我们聊了Spring Boot的整体启动流程、自动装配、条件装配等内容:

1> 《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息
2> 《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段
3> 《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机)
4> 《SpringBoot启动流程四》:图文带你debug源码分析SpringApplication运行阶段和运行后阶段
5> 《SpringBoot启动流程五》:你真的知道SpringBoot自动装配原理吗(两万字图文源码分析)
6> 《SpringBoot启动流程六》:SpringBoot自动装配时做条件装配的原理

在使用springboot搭建一个web应用程序的时候,我们发现不需要自己搭建一个tomcat服务器,只需要引入spring-boot-starter-web,在应用启动时会自动启动嵌入式的tomcat作为服务器。

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

注意:SpringBoot版本:2.3.7.RELEASE(博主写博客时最新Spring-boot版本 – 2.6.X代码逻辑几乎一样)

本文我们接着讨论Spring Boot 如何内嵌 并 启动 Tomcat的?

二、整体执行流程图

在这里插入图片描述

三、内嵌Tomcat入口 --> onRefresh()

在这里插入图片描述
从SpringApplication#run()开始往下追,追到AbstractApplicationContext#refresh()方法中,其内部会调用onRefresh()方法,这里负责开始内嵌Tomcat服务器。

在开始讨论onRefresh()方法之前,我们先找到当前Web应用的ApplicationContext具体是哪个(即AbstractApplicationContext的子类)?

下面结合整个Spring Boot的启动流程,有两点是有迹可循的:

1、推断Web应用类型

在博文 <《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段> 中我们讨论过SpringApplication的构建过程中会推断Web应用的类型;

WebApplicationType.deduceFromClasspath();

因为Web应用类型可能在SpringApplication构造后及run方法之前,再通过setWebApplicatioinType(WebApplicationType)方法调整;又在推断Web应用类型的过程中,由于当前Spring应用上下文尚未准备,所以采用检查当前ClassLoader下基准Class的存在性来推断Web应用类型。

public enum WebApplicationType {

    NONE,
    SERVLET,
    REACTIVE;

    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";
    
    ....

    static WebApplicationType deduceFromClasspath() {
        // 1. 如果`DispatcherHandler`存在,并且`DispatcherServlet`和`ServletContainer`不存在时,Web应用类型为REACTIVE;
        if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
                && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
            return WebApplicationType.REACTIVE;
        }
        // 2. 如果`Servlet`和`ConfigurableWebApplicationContext`不存在,则当前应用为非Web引应用,即NONE。
        for (String className : SERVLET_INDICATOR_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return WebApplicationType.NONE;
            }
        }
        // 3.当Spring WebFlux和Spring Web MVC同时存在时,Web应用依旧是SERVLET。
        return WebApplicationType.SERVLET;
    }
    }
    ....
}

WEB 应用类型,一共有三种:NONE,SERVLET,REACTIVE。

deduceFromClasspath()方法利用ClassUtils.isPresent(String, ClassLoader)方法依次判断reactive.DispatcherHandlerConfigurableWebApplicationContextServletservlet.DispatcherServlet的存在性组合情况,从而判断Web 引用类型,具体逻辑如下:

  1. 如果DispatcherHandler存在,并且DispatcherServletServletContainer不存在时,即:Spring Boot仅依赖WebFlux时,Web应用类型为REACTIVE;
  2. 如果ServletConfigurableWebApplicationContext不存在,则当前应用为非Web应用,即NONE。因为这两个API是Spring Web MVC必须的依赖。
  3. 当Spring WebFlux和Spring Web MVC同时存在时,Web应用类型依旧是SERVLET。

2、创建应用上下文

在博文 <《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段 > 中 我们讨论过的SpringAppliation准备阶段的第八步会根据上面推断出的Web应用来创建相应的 ApplicationContext应用上下文对象。
在这里插入图片描述

根据应用类型利用反射创建Spring应用上下文,可以理解为创建一个容器;就SERVLET而言:实例化AnnotationConfigServletWebServerApplicationContext。
在这里插入图片描述

3、AnnotationConfigServletWebServerApplicationContext的类图

在这里插入图片描述
AnnotationConfigServletWebServerApplicationContext 继承自 ServletWebServerApplicationContext,ServletWebServerApplicationContext 又间接继承自AbstractApplicationContext,这样再回到AbstractApplicationContext#onRefresh(),我们便知道这里的应用上下文是哪个实例了。

4、AbstractApplicationContext#onRefresh()

由于AnnotationConfigServletWebServerApplicationContext类中没有重写onRefresh方法,所以从类图的最下方往上找到ServletWebServerApplicationContext#onRefresh()方法。

// ServletWebServerApplicationContext#onRefresh()
@Override
protected void onRefresh() {
    //创建主题对象,不用在意
    super.onRefresh();
    try {
        //开始创建web服务
        createWebServer();
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}

方法逻辑:

  1. 首先调用父类AbstractApplicationContext的onRefresh()方法,创建一个主题对象(无需特意关注)。
  2. 接着调用自己的createWebServer()方法创建WebServer。

下面接着看createWebServer()方法做了什么?

1> createWebServer() --> 创建WebServer

private void createWebServer() {
    // 第一次进来,默认webServer 是 null
    WebServer webServer = this.webServer;
    // 第一次进行,默认servletContext 是 null
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
        // 从BeanFactory中获取ServletWebServerFactory的实现类
        ServletWebServerFactory factory = getWebServerFactory();
        // 获取servletContextInitializer(getSelfInitializer()方法会初始化Tomcat对象),获取webServer(完成内嵌Tomcat的API调用)
        // todo 注意getSelfInitializer()返回一个lambdab表达式,其中的内容不会执行,而是在启动TomcatEmbeddedContext时才会执行lambda
        this.webServer = factory.getWebServer(getSelfInitializer());
        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);
        }
    }
    // 根据上下文的配置属性 替换servlet相关的属性资源
    initPropertySources();
}

方法逻辑:

进入到方法的时候,webServer和servletContext均为null。

  1. 首先从BeanFactory中获取ServletWebServerFactory的实现类;
  2. 然后根据获取到的ServletWebServerFactory,进而获取Servlet上下文初始化器servletContextInitializer、获取WebServer。

    在获取servletContextInitializer时,返回的是一个lambda表达式,lambda表达式中的内容(即:初始化Tomcat对象)在启动TomcatEmbeddedContext时才会执行。
  3. 根据上下文的配置属性 替换servlet相关的属性资源;

(1 从BeanFactory中获取ServletWebServerFactory:

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);
}

getWebServerFactory()方法会从BeanFactory中获取所有ServletWebServerFactory接口的实现类,如果存在多个,则抛异常。

ServletWebServerFactory接口有四个主要的实现类:
在这里插入图片描述
其中默认的 Web 环境就是 TomcatServletWebServerFactory,而UndertowServletWebServerFactory用于响应式编程。
在这里插入图片描述
本文debug应用时用的正是默认的Web环境 --> TomcatServletWebServerFactory

(2 获取Servlet上下文初始化器servletContextInitializer:
在这里插入图片描述
返回一个lambda表达式,在后面启动TomcatEmbeddedContext时才会执行lambda。

(3 获取WebServer:

接着进入到TomcatServletWebServerFactory # getWebServer() 方法:

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    // 走进这里时,initializers还没有执行
    if (this.disableMBeanRegistry) {
        Registry.disableRegistry();
    }
    // 完成Tomcat的API调用,把需要的对象创建好、参数设置好
    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);
    }
    // 准备tomcatEmbeddedContext并将其设置到tomcat中,其中会把上面获取到的servletContextInitializer绑定到tomcatEmbeddedContext。
    prepareContext(tomcat.getHost(), initializers);
    // 构建tomcatWebServer
    return getTomcatWebServer(tomcat);
}

方法逻辑:

  1. 首先完成Tomcat的API调用,把需要的对象创建好、参数设置好;
    而Tomcat有两个核心功能:<处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化>、<加载和管理 Servlet,以及具体处理 Request 请求>。而针对这两个功能,Tomcat 设计了两个核心组件来分别完成这两件事,即:连接器Connector和容器Container(包括:Engine、Host、Context、Wrapper)。
    所以,其中最重要的两件事是:

    1> 把连接器 Connector 对象添加到 Tomcat 中;
    2> 配置容器引擎,configureEngine(tomcat.getEngine());
    1. 准备tomcatEmbeddedContext并将其设置到tomcat中,其中会把上面获取到的servletContextInitializer绑定到tomcatEmbeddedContext。
    2. 构建tomcatWebServer。

下面我们着重讨论如何构建Tomcat服务的?

2> getTomcatWebServer(tomcat) --> 构建Tomcat服务

整个构建Tomcat服务的代码执行流程如下:
在这里插入图片描述
其中牵扯到Tomcat其他组件(StandardServer、StandardService、StandardEngine、MapperListener、Connector)的初始化,整个生命周期流转如下:

在这里插入图片描述

3> TomcatServletWebServerFactory#initialize()

初始化Tomcat服务详细代码如下:

private void initialize() throws WebServerException {
    logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
    synchronized (this.monitor) {
        try {
            // 将engineName和instanceId用-拼接到一起
            addInstanceIdToEngineName();

            Context context = findContext();
            context.addLifecycleListener((event) -> {
                if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
                    // Remove service connectors so that protocol binding doesn't
                    // happen when the service is started.
                    // 删除Connectors,以便再启动服务时不发生协议绑定,点进去看一下
                    removeServiceConnectors();
                }
            });

            // 启动服务触发初始化监听器
            this.tomcat.start();

            // 在主线程中重新抛出失败异常
            rethrowDeferredStartupExceptions();

            try {
                ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
            }
            catch (NamingException ex) {
                // Naming is not enabled. Continue
            }

            // Unlike Jetty, all Tomcat threads are daemon threads. We create a
            // blocking non-daemon to stop immediate shutdown
            // 所有的tomcat线程都是守护线程,所以创建一个阻塞非守护线程来避免立即关闭
            startDaemonAwaitThread();
        }
        catch (Exception ex) {
            // 出现异常时,停止Tomcat
            stopSilently();
            destroySilently();
            throw new WebServerException("Unable to start embedded Tomcat", ex);
        }
    }
}

4> 执行完createWebServer()方法之后的日志输出

在这里插入图片描述
从日志输出来看,createWebServer() 方法看似是用来启动web服务的,并没有真正启动 Tomcat,只是通过ServletWebServerFactory 创建了一个 WebServer,初始化了一堆设置(包括:Port、Service、Engine、embeddedWebApplicationContext)。真正的启动发生在AbstractApplicationContext#finishRefresh()中。

四、真正启动Tomcat --> finishRefresh()

代码整理执行流程如下:
在这里插入图片描述
WebServerStartStopLifecycle类负责处理WebServer(Tomcat)的启动和关闭;

1、启动Tomcat

WebServerStartStopLifecycle#start()代码执行流程如下:
在这里插入图片描述
TomcatWebServer#start()详细代码如下:

@Override
public void start() throws WebServerException {
    synchronized (this.monitor) {
        if (this.started) {
            return;
        }
        try {
            // 添加之前移除的connector,绑定service和Connector
            addPreviouslyRemovedConnectors();
            // 获取当前Tomcat绑定的Connector
            Connector connector = this.tomcat.getConnector();
            // 默认会走进去
            if (connector != null && this.autoStart) {
                // 启动时执行延迟加载
                performDeferredLoadOnStartup();
            }
            // 检查connector启动状态是否为失败,失败抛出异常
            checkThatConnectorsHaveStarted();
            this.started = true;
            // Tomcat启动成功之后打印日志
            logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '"
                    + getContextPath() + "'");
        }
        catch (ConnectorStartFailedException ex) {
            stopSilently();
            throw ex;
        }
        catch (Exception ex) {
            PortInUseException.throwIfPortBindingException(ex, () -> this.tomcat.getConnector().getPort());
            throw new WebServerException("Unable to start embedded Tomcat server", ex);
        }
        finally {
            Context context = findContext();
            ContextBindings.unbindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
        }
    }
}

方法逻辑:

  1. 首先添加之前移除的connector,绑定service和Connector;
  2. 获取到当前Tomcat绑定的Connector,接着进行执行延迟加载启动;
  3. 然后 检查connector启动状态是否为失败,失败抛出异常,否则打印Tomcat启动成功日志。

1)addPreviouslyRemovedConnectors()

private void addPreviouslyRemovedConnectors() {
    Service[] services = this.tomcat.getServer().findServices();
    for (Service service : services) {
        // 从上面移除connector添加的缓存中取出connector
        Connector[] connectors = this.serviceConnectors.get(service);
        if (connectors != null) {
            for (Connector connector : connectors) {
                // connector添加到tomcat service中
                service.addConnector(connector);
                if (!this.autoStart) {
                    // 如果不是自动启动,则暂停connector
                    stopProtocolHandler(connector);
                }
            }
            // 添加完成后移除connector
            this.serviceConnectors.remove(service);
        }
    }
}

2)performDeferredLoadOnStartup()

private void performDeferredLoadOnStartup() {
    try {
        for (Container child : this.tomcat.getHost().findChildren()) {
            if (child instanceof TomcatEmbeddedContext) {
                // 延迟加载启动
                ((TomcatEmbeddedContext) child).deferredLoadOnStartup();
            }
        }
    }
    catch (Exception ex) {
        if (ex instanceof WebServerException) {
            throw (WebServerException) ex;
        }
        throw new WebServerException("Unable to start embedded Tomcat connectors", ex);
    }
}

3)checkThatConnectorsHaveStarted()

private void checkThatConnectorsHaveStarted() {
    checkConnectorHasStarted(this.tomcat.getConnector());
    for (Connector connector : this.tomcat.getService().findConnectors()) {
        checkConnectorHasStarted(connector);
    }
}

TomcatWebServer#start()方法执行完之后的日志输出:
在这里插入图片描述

2、关闭Tomcat

在这里插入图片描述
在refreshContext()方法中会通过AbstractApplicationContext#registerShutdownHook()方法注册一个shutdownhook线程,当JVM退出时,确保后续Spring应用上下文所管理的Bean能够在标准的Spring生命周期中回调,从而合理的销毁Bean所依赖的资源(即:注册一个关闭webServer的钩子函数,而钩子函数可以完成关闭的功能)。
在这里插入图片描述
我们知道应用的上下文实例是ServletWebServerApplicationContext,而它重写了其父类AbstractApplicationContext中的doClose()方法,所以进入到ServletWebServerApplicationContext#doClose()方法;
在这里插入图片描述
发布一个事件之后,调用其父类AbstractApplicationContext#doClose()方法;

整体代码执行流程如下:
在这里插入图片描述
最后进入到WebServerStartStopLifecycle#stop():
在这里插入图片描述

五、总结

在SpringApplication的运行阶段会通过refreshContext()方法进行上下文的刷新操作,其会进入到AbstractApplicationContext#refresh()方法中,进而调用onRefresh()方法内嵌Tomcat,进行Tomcat的初始化,在finishRefresh()方法中进行Tomcat的启动。

1> 创建WebServer:

  • 从BeanFactory中获取ServletWebServerFactory的实现类TomcatServletWebServerFactory ,然后通过其获取到WebServer;
  • 在获取WebServer的同时,初始化相关的Tomcat对象,包括:Connector、Container。

2> 启动WebServer:

  • 通过生命周期回调的方式将Tomcat和Connector绑定、延时加载启动Connector、启动成功后打印Tomcat启动成功日志。
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3天前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
19 1
|
1月前
|
SQL NoSQL Java
springboot操作nosql的mongodb,或者是如何在mongodb官网创建服务器并进行操作
本文介绍了如何在Spring Boot中操作NoSQL数据库MongoDB,包括在MongoDB官网创建服务器、配置Spring Boot项目、创建实体类、仓库类、服务类和控制器类,以及如何进行测试。
19 1
springboot操作nosql的mongodb,或者是如何在mongodb官网创建服务器并进行操作
|
5月前
|
XML 网络协议 Java
JavaWeb -- HTTP -- WEB服务器TOMCAT
JavaWeb -- HTTP -- WEB服务器TOMCAT
|
1月前
|
前端开发 Java
学习SpringMVC,建立连接,请求,响应 SpringBoot初学,如何前后端交互(后端版)?最简单的能通过网址访问的后端服务器代码举例
文章介绍了如何使用SpringBoot创建简单的后端服务器来处理HTTP请求,包括建立连接、编写Controller处理请求,并返回响应给前端或网址。
53 0
学习SpringMVC,建立连接,请求,响应 SpringBoot初学,如何前后端交互(后端版)?最简单的能通过网址访问的后端服务器代码举例
|
2月前
|
Java 应用服务中间件 Windows
windows服务器重装系统之后,Tomcat服务如何恢复?
windows服务器重装系统之后,Tomcat服务如何恢复?
59 10
|
1月前
|
安全 Java 应用服务中间件
【服务器知识】Tomcat简单入门
【服务器知识】Tomcat简单入门
|
3月前
|
应用服务中间件 Docker 容器
在服务器中使用Docker安装Tomcat、同时实现目录挂载、并且部署War包到服务器
这篇文章介绍了在Docker中安装Tomcat的过程,包括搜索Tomcat镜像、拉取镜像、目录挂载的准备、创建并挂载容器,以及如何进入容器和进行测试。文中还说明了如何将WAR包部署到Tomcat服务器并访问部署的应用。
在服务器中使用Docker安装Tomcat、同时实现目录挂载、并且部署War包到服务器
|
3月前
|
网络协议 Java 应用服务中间件
Tomcat源码分析 (一)----- 手撕Java Web服务器需要准备哪些工作
本文探讨了后端开发中Web服务器的重要性,特别是Tomcat框架的地位与作用。通过解析Tomcat的内部机制,文章引导读者理解其复杂性,并提出了一种实践方式——手工构建简易Web服务器,以此加深对Web服务器运作原理的认识。文章还详细介绍了HTTP协议的工作流程,包括请求与响应的具体格式,并通过Socket编程在Java中的应用实例,展示了客户端与服务器间的数据交换过程。最后,通过一个简单的Java Web服务器实现案例,说明了如何处理HTTP请求及响应,强调虽然构建基本的Web服务器相对直接,但诸如Tomcat这样的成熟框架提供了更为丰富和必要的功能。
|
3月前
|
Java 应用服务中间件 Apache
在 Debian 服务器上安装和配置 Apache Tomcat 的方法
在 Debian 服务器上安装和配置 Apache Tomcat 的方法
53 0
|
4月前
|
Java
软件开发常用之SpringBoot文件下载接口编写(下),Vue+SpringBoot文件上传下载预览,服务器默认上传是1M,可以调节,调节文件上传大小写法,图片预览,如何预览后下次还能看到,预览写法
软件开发常用之SpringBoot文件下载接口编写(下),Vue+SpringBoot文件上传下载预览,服务器默认上传是1M,可以调节,调节文件上传大小写法,图片预览,如何预览后下次还能看到,预览写法