热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Spring/SpringBoot系列之SpringBoot内嵌的Tomcat是如何启动的【十二】

SpringSpringBoot系列之SpringBoot内嵌的Tomcat是如何启动的【十二】,Go语言社区,Golang程序员人脉社
1. 前言

因为SpringBoot 内置了Tomcat容器,所以可以把 web 程序打成 jar 包,直接启动,而且在开发中也不需要像传统Spring项目那样,需要把项目添加到Tomcat中进行启动。不得不说,Springboot确实在方方面面提高了开发效率。
在这里插入图片描述

2. 从 Main 方法说起

SpringBoot 项目有一个启动类,启动类里有一个 main 方法用来启动整个应用:
在这里插入图片描述
run()方法调用链:

  1. SpringApplication.run(DemoApplication.class, args)
  2. run(new Class[] { primarySource }, args)
  3. new SpringApplication(primarySources).run(args)

最后跟踪到:SpringApplication.run()方法:
在这里插入图片描述
各步骤说明如下:

  1. 设置系统属性『java.awt.headless』,为true则启用headless模式支持;
  2. 通过 SpringFactoriesLoader 检索 META-INF/spring.factories ,找到声明的所有SpringApplicationRunListener的实现类并将其实例化,之后逐个调用其started()方法,广播SpringBoot要开始执行了;
  3. 发布应用开始启动事件;
  4. 初始化参数
  5. 创建并配置当前SpringBoot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile),并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法,广播Environment准备完毕。
  6. 打印banner;
  7. 创建应用上下文;
  8. 通过SpringFactoriesLoader检索META-INF/spring.factories,获取并实例化异常分析器;
  9. 为ApplicationContext加载environment,之后逐个执行ApplicationContextInitializer的initialize()方法来进一步封装ApplicationContext,并调用所有的SpringApplicationRunListener的contextPrepared()方法,【EventPublishingRunListener只提供了一个空的contextPrepared()方法】,之后初始化IoC容器,并调用SpringApplicationRunListener的contextLoaded()方法,广播ApplicationContext的IoC加载完成,这里就包括通过@EnableAutoConfiguration导入的各种自动配置类;
  10. 刷新上下文;
  11. 再一次刷新上下文,其实是空方法,可能是为了后续扩展;
  12. 发布应用已经启动的事件;
  13. 遍历所有注册的ApplicationRunner和CommandLineRunner,并执行其run()方法(可以实现自己的ApplicationRunner或者CommandLineRunner,来对SpringBoot的启动过程进行扩展);
  14. 应用已经启动完成的监听事件。

简单的总结下步骤为:1. 配置属性 > 2. 获取监听器,发布应用开始启动事件 > 3. 初始化输入参数 > 4. 配置环境,输出 banner > 5. 创建上下文 > 6. 预处理上下文 > 7. 刷新上下文 > 8. 再刷新上下文 > 9. 发布应用已经启动事件 > 10. 发布应用启动完成事件。

其实上面这段代码,如果只分析 tomcat ,只需要关注上下文是如何创建的和上下文是如何刷新的,分别对应的方法就是 createApplicationContext() 和 refreshContext(context),接下来看看这两个方法。

createApplicationContext()方法:
在这里插入图片描述
这里就是根据 webApplicationType 来判断创建哪种类型的 Servlet,代码中分别对应着 Web 类型(SERVLET),响应式 Web 类型(REACTIVE),非 Web 类型(default),我们建立的是 Web 类型,所以肯定实例化 DEFAULT_SERVLET_WEB_CONTEXT_CLASS 指定的类,也就是 AnnotationConfigServletWebServerApplicationContext 类,下面用图来说明下这个类的继承关系:
在这里插入图片描述
通过类继承关系可知,这个类继承的是 ServletWebServerApplicationContext,而ServletWebServerApplicationContext最后继承了AbstractApplicationContext。

refreshContext(context)方法:
在这里插入图片描述
在这里插入图片描述
然后调用AbstractApplicationContext的refresh()方法:
在这里插入图片描述
其中onRefresh()方法利用模板方法模式,是一个空方法,留给子类实现:
在这里插入图片描述
看ServletWebServerApplicationContext中的onRefresh()方法:
在这里插入图片描述
顾名思义,createWebServer()就是创建web服务的:
在这里插入图片描述
从代码中可知,webServer是从ServletWebServerFactory工厂中获取的,ServletWebServerFactory是一个接口,实现类有:
在这里插入图片描述
所以也可以手动指定内嵌容器为其它容器。

3. 走进 Tomcat

直接进入TomcatServletWebServerFactory的getWebServer()方法:
在这里插入图片描述
该方法主要做了两件事情:

  • 把 Connnctor(我们称之为连接器)对象添加到 Tomcat 中;
  • configureEngine

跟踪configureEngine()方法:
在这里插入图片描述
这个 Engine 其实就是容器Container的子类。

继续跟踪源码,找到Container接口,看其实现:
在这里插入图片描述
其中有4个子接口,分别是Engine,Host,Context,Wrapper。看源码注释可知Engine 是最高级别的容器,其子容器是 Host,Host 的子容器是 Context,Wrapper 是 Context 的子容器,所以这 4 个容器的关系就是父子关系,也就是 Engine>Host>Context>Wrapper。

再看Tomcat源码:

//部分源码,其余部分省略。
public class Tomcat {
	 //设置连接器
     public void setConnector(Connector connector) {
        Service service = getService();
        boolean found = false;
        for (Connector serviceConnector : service.findConnectors()) {
            if (connector == serviceConnector) {
                found = true;
            }
        }
        if (!found) {
            service.addConnector(connector);
        }
    }
    //获取service
    public Service getService() {
        return getServer().findServices()[0];
    }
    //设置Host容器
    public void setHost(Host host) {
        Engine engine = getEngine();
        boolean found = false;
        for (Container engineHost : engine.findChildren()) {
            if (engineHost == host) {
                found = true;
            }
        }
        if (!found) {
            engine.addChild(host);
        }
    }
    //获取Engine容器
    public Engine getEngine() {
        Service service = getServer().findServices()[0];
        if (service.getContainer() != null) {
            return service.getContainer();
        }
        Engine engine = new StandardEngine();
        engine.setName( "Tomcat" );
        engine.setDefaultHost(hostname);
        engine.setRealm(createDefaultRealm());
        service.setContainer(engine);
        return engine;
    }
    //获取server
    public Server getServer() {

        if (server != null) {
            return server;
        }

        System.setProperty("catalina.useNaming", "false");

        server = new StandardServer();

        initBaseDir();

        // Set configuration source
        ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));

        server.setPort( -1 );

        Service service = new StandardService();
        service.setName("Tomcat");
        server.addService(service);
        return server;
    }

    //添加Context容器
    public Context addContext(Host host, String contextPath, String contextName,
            String dir) {
        silence(host, contextName);
        Context ctx = createContext(host, contextPath);
        ctx.setName(contextName);
        ctx.setPath(contextPath);
        ctx.setDocBase(dir);
        ctx.addLifecycleListener(new FixContextListener());

        if (host == null) {
            getHost().addChild(ctx);
        } else {
            host.addChild(ctx);
        }

    //添加Wrapper容器
    public static Wrapper addServlet(Context ctx,
                                      String servletName,
                                      Servlet servlet) {
        // will do class for name and set init params
        Wrapper sw = new ExistingStandardWrapper(servlet);
        sw.setName(servletName);
        ctx.addChild(sw);

        return sw;
    }
}

从 Tomcat 的 getServer() 可以知道,Tomcat 的最顶层是 Server,Server 就是 Tomcat 的实例,一个 Tomcat 一个 Server;通过 getEngine() 了解到 Server 下面是 Service,而且是多个,一个 Service 代表部署的一个应用,而且还可以知道,Engine 容器,一个 service 只有一个。

根据父子关系,看 setHost()源码可以知道,host 容器有多个;同理,从 addContext()源码下知道,Context 也是多个;addServlet()表明 Wrapper 容器也是多个,而且这段代码也暗示了,其实 Wrapper 和 Servlet 是一层意思。另外根据 setConnector 源码可以知道,连接器(Connector)是设置在 service 下的,而且是可以设置多个连接器(Connector)。

根据上面分析可知:Tomcat 主要包含了 2 个核心组件,连接器(Connector)和容器(Container),用图表示如下:
在这里插入图片描述
一个 Tomcat 是一个 Server,一个 Server 下有多个 service,也就是我们部署的多个应用,一个应用下有多个连接器(Connector)和一个容器(Container),容器下有多个子容器,关系用图表示如下:
在这里插入图片描述
Engine 下有多个 Host 子容器,Host 下有多个 Context 子容器,Context 下有多个 Wrapper 子容器。

4. 总结

SpringBoot 的启动是通过 new SpringApplication()实例来启动的,启动过程主要做如下几件事情:> 1. 配置属性 > 2. 获取监听器,发布应用开始启动事件 > 3. 初始化输入参数 > 4. 配置环境,输出 banner > 5. 创建上下文 > 6. 预处理上下文 > 7. 刷新上下文 > 8. 再刷新上下文 > 9. 发布应用已经启动事件 > 10. 发布应用启动完成事件。

而启动 Tomcat 就是在第 7 步“刷新上下文”中;Tomcat 的启动主要是初始化 2 个核心组件,连接器(Connector)和容器(Container),一个 Tomcat 实例就是一个 Server,一个 Server 包含多个 Service,也就是多个应用程序,每个 Service 包含多个连接器(Connetor)和一个容器(Container),而容器下又有多个子容器,按照父子关系分别为:Engine,Host,Context,Wrapper,其中除了 Engine 外,其余的容器都是可以有多个。


推荐阅读
  • 使用C++编写程序实现增加或删除桌面的右键列表项
    本文介绍了使用C++编写程序实现增加或删除桌面的右键列表项的方法。首先通过操作注册表来实现增加或删除右键列表项的目的,然后使用管理注册表的函数来编写程序。文章详细介绍了使用的五种函数:RegCreateKey、RegSetValueEx、RegOpenKeyEx、RegDeleteKey和RegCloseKey,并给出了增加一项的函数写法。通过本文的方法,可以方便地自定义桌面的右键列表项。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 本文介绍了brain的意思、读音、翻译、用法、发音、词组、同反义词等内容,以及脑新东方在线英语词典的相关信息。还包括了brain的词汇搭配、形容词和名词的用法,以及与brain相关的短语和词组。此外,还介绍了与brain相关的医学术语和智囊团等相关内容。 ... [详细]
  • Webpack5内置处理图片资源的配置方法
    本文介绍了在Webpack5中处理图片资源的配置方法。在Webpack4中,我们需要使用file-loader和url-loader来处理图片资源,但是在Webpack5中,这两个Loader的功能已经被内置到Webpack中,我们只需要简单配置即可实现图片资源的处理。本文还介绍了一些常用的配置方法,如匹配不同类型的图片文件、设置输出路径等。通过本文的学习,读者可以快速掌握Webpack5处理图片资源的方法。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • 集合的遍历方式及其局限性
    本文介绍了Java中集合的遍历方式,重点介绍了for-each语句的用法和优势。同时指出了for-each语句无法引用数组或集合的索引的局限性。通过示例代码展示了for-each语句的使用方法,并提供了改写为for语句版本的方法。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • REVERT权限切换的操作步骤和注意事项
    本文介绍了在SQL Server中进行REVERT权限切换的操作步骤和注意事项。首先登录到SQL Server,其中包括一个具有很小权限的普通用户和一个系统管理员角色中的成员。然后通过添加Windows登录到SQL Server,并将其添加到AdventureWorks数据库中的用户列表中。最后通过REVERT命令切换权限。在操作过程中需要注意的是,确保登录名和数据库名的正确性,并遵循安全措施,以防止权限泄露和数据损坏。 ... [详细]
  • 本文介绍了如何在Azure应用服务实例上获取.NetCore 3.0+的支持。作者分享了自己在将代码升级为使用.NET Core 3.0时遇到的问题,并提供了解决方法。文章还介绍了在部署过程中使用Kudu构建的方法,并指出了可能出现的错误。此外,还介绍了开发者应用服务计划和免费产品应用服务计划在不同地区的运行情况。最后,文章指出了当前的.NET SDK不支持目标为.NET Core 3.0的问题,并提供了解决方案。 ... [详细]
  • 本文介绍了贝叶斯垃圾邮件分类的机器学习代码,代码来源于https://www.cnblogs.com/huangyc/p/10327209.html,并对代码进行了简介。朴素贝叶斯分类器训练函数包括求p(Ci)和基于词汇表的p(w|Ci)。 ... [详细]
author-avatar
自由飘落de花瓣
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有