• SpringBoot中的Tomcat是如何启动的?


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

    添加如上 Web 的依赖,Spring Boot 就帮我们内置了 Servlet 容器,默认使用的是 Tomcat,同样也支持修改,比如可以使用 jetty、Undertow 等。

    因为内置了启动容器,应用程序可以直接通过 Maven 命令将项目编译成可执行的 jar 包,通过 java -jar 命令直接启动,不需要再像以前一样,打包成 War 包,然后部署在 Tomcat 中。

    那么:你知道内置的 Tomcat 在 Spring Boot 中是怎么启动的吗?

    从启动入口分析

    如果不知道从哪开始,那么至少应该知道 Spring Boot 其实运行的就是一个 main 方法,

    本文环境:Spring Boot:2.2.2.RELEASE

    @SpringBootApplication
    public class MyApplication {
        public static void main(String[] args) {
            SpringApplication.run(MyApplication.class, args);
        }
    }

    我们点进这个 SpringApplication.run() 方法:

    public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
        return run(new Class[]{primarySource}, args);
    }

    这列的 run() 方法返回的是 ConfigurableApplicationContext 对象,我们继续跟踪这个 run() 方法:

    public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return (new SpringApplication(primarySources)).run(args);
    }

    又套了一层,继续点击这个返回的 run() 方法:

    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        /*
         * 1、配置属性
         * 设置系统属性 java.awt.headless,为 true 则启用headless模式
         * headless模式是应用的一种配置模式,在服务器缺少显示设备、键盘、鼠标等外设的情况下可以使用该模式
         * 比如我们使用的Linux服务器就是缺少前述的这些设备,但是又需要使用这些设备提供的能力
         */
        configureHeadlessProperty();
        /*
         * 2、获取监听器,发布应用开始启动事件
         * 通过SpringFactoriesLoader检索META-INF/spring.factories,
         * 找到声明的所有SpringApplicationRunListener的实现类并将其实例化,
         * 之后逐个调用其started()方法,广播SpringBoot要开始执行了
         */
        SpringApplicationRunListeners listeners = getRunListeners(args);
        /* 发布应用开始启动事件 */
        listeners.starting();
        try {
            /* 3、初始化参数 */
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            /*
             * 4、配置环境,输出banner
             * 创建并配置当前SpringBoot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile),
             * 并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法,广播Environment准备完毕。
             */
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            configureIgnoreBeanInfo(environment);
            /* 打印banner,如果在resources目录下创建了我们自己的banner就会进行打印,否则默认使用spring的 */
            Banner printedBanner = printBanner(environment);
            /* 5、创建应用上下文 */
            context = createApplicationContext();
            /* 通过SpringFactoriesLoader检索META-INF/spring.factories,获取并实例化异常分析器。 */
            exceptionReporters = getSpringFactoriesInstances(
                    SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
            /*
             * 6、预处理上下文
             * 为ApplicationContext加载environment,之后逐个执行ApplicationContextInitializer的initialize()方法来进一步封装ApplicationContext,
             * 并调用所有的SpringApplicationRunListener的contextPrepared()方法,【EventPublishingRunListener只提供了一个空的contextPrepared()方法】,
             * 之后初始化IoC容器,并调用SpringApplicationRunListener的contextLoaded()方法,广播ApplicationContext的IoC加载完成,
             * 这里就包括通过@EnableAutoConfiguration导入的各种自动配置类。
             */
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            /* 7、刷新上下文 */
            refreshContext(context);
            /* 8、再一次刷新上下文,其实是空方法,可能是为了后续扩展。 */
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass)
                        .logStarted(getApplicationLog(), stopWatch);
            }
            /* 9、发布应用已经启动的事件 */
            listeners.started(context);
            /*
             * 遍历所有注册的ApplicationRunner和CommandLineRunner,并执行其run()方法。
             * 我们可以实现自己的ApplicationRunner或者CommandLineRunner,来对SpringBoot的启动过程进行扩展。
             */
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, listeners);
            throw new IllegalStateException(ex);
        }

        try {
            /* 10、发布应用已经启动完成的监听事件 */
            listeners.running(context);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, null);
            throw new IllegalStateException(ex);
        }
        return context;
    }

    如果觉得这个方法看的云里雾里的,那么可以概括为如下几步:

    1. 配置系统属性
    2. 获取监听,发布应用开始启动时间
    3. 初始化输入参数
    4. 配置环境,输出banner
    5. 创建上下文
    6. 预处理上下文
    7. 刷新上下文
    8. 再次刷新上下文
    9. 发布应用已经启动事件
    10. 发布应用启动完成事件

    而我们 Tomcat 的启动主要是在第5步创建上下文,以及第 7步刷新上下文实现的。

    创建上下文

    第5步中,创建上下文主要是调用的 createApplicationContext() 方法:

    protected ConfigurableApplicationContext createApplicationContext() {
      /** 1. 根据Web应用类型,获取对应的ApplicationContext子类 **/
        Class<?> contextClass = this.applicationContextClass;
        if (contextClass == null) {
            try {
                switch(this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");
                    break;
                case REACTIVE:
                    contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");
                    break;
                default:
                    contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");
                }
            } catch (ClassNotFoundException var3) {
                throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
            }
        }
      /** 2. 实例化子类 **/
        return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
    }

    代码中主要就是通过 switch 语句,根据 webApplicationType 的类型来创建不同的 ApplicationContext:

    • SERVLET:Web类型,实例化 AnnotationConfigServletWebServerApplicationContext
    • REACTIVE:响应式Web类型,实例化 AnnotationConfigReactiveWebServerApplicationContext
    • default:非Web类型,实例化 AnnotationConfigApplicationContext

    因为我们的应用是 Web 类型,所以实例化的是 AnnotationConfigServletWebServerApplicationContext,如下是该类的关系图(由Diagram截图):

    我们在上图的底部触发,可以看到 AnnotationConfigServletWebServerApplicationContext > ServletWebServerApplicationContext > ... AbstractApplicationContext(>表示继承),总之,最终继承到了 AbstractApplicationContext,这个类是 ApplicationContext 的抽象实现类,该抽象类实现应用上下文的一些具体操作。

    至此,并没有看到 Tomcat 的相关代码,其实这一步主要就是「创建上下文」,拿到「上下文」之后需要传递给「刷新上下文」,交由刷新上下文创建 Web 服务。

    刷新上下文

    第7步中,刷新上下文时调用的 refreshContext(context) 方法,其中 context 就是第5步创建的上下文,方法如下:

    private void refreshContext(ConfigurableApplicationContext context) {
        refresh(context);
        if (this.registerShutdownHook) {
            try {
                context.registerShutdownHook();
            }
            catch (AccessControlException ex) {
                // Not allowed in some environments.
            }
        }
    }

    protected void refresh(ApplicationContext applicationContext) {
        Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
        ((AbstractApplicationContext) applicationContext).refresh();
    }

    refreshContext() 方法传递的 context,经由 refresh() 方法强转成父类 AbstractApplicationContext,具体调用过程如下:

    public void refresh() throws BeansException, IllegalStateException {
        synchronized(this.startupShutdownMonitor) {
            this.prepareRefresh();
            ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
            this.prepareBeanFactory(beanFactory);

            try {
                this.postProcessBeanFactory(beanFactory);
                this.invokeBeanFactoryPostProcessors(beanFactory);
                this.registerBeanPostProcessors(beanFactory);
                this.initMessageSource();
                this.initApplicationEventMulticaster();
                /** 主要关系 onRefresh() 方法 ------------- **/
                this.onRefresh();
                this.registerListeners();
                this.finishBeanFactoryInitialization(beanFactory);
                this.finishRefresh();
            } catch (BeansException var9) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
                }

                this.destroyBeans();
                this.cancelRefresh(var9);
                throw var9;
            } finally {
                this.resetCommonCaches();
            }

        }
    }

    在这个方法中我们主要关心 onRefresh() 方法,onRefresh() 方法是调用其子类实现的,也就是 ServletWebServerApplicationContext,

    如下是子类的 onRefresh() 方法:

    protected void onRefresh() {
        super.onRefresh();

        try {
            this.createWebServer();
        } catch (Throwable var2) {
            throw new ApplicationContextException("Unable to start web server", var2);
        }
    }

    private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = this.getServletContext();
        if (webServer == null && servletContext == null) {
          /** 得到Servlet工厂 **/
            ServletWebServerFactory factory = this.getWebServerFactory();
            this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
        } else if (servletContext != null) {
            try {
                this.getSelfInitializer().onStartup(servletContext);
            } catch (ServletException var4) {
                throw new ApplicationContextException("Cannot initialize servlet context", var4);
            }
        }
        this.initPropertySources();
    }

    其中 createWebServer() 方法是用来启动web服务的,但是还没有真正启动 Tomcat,只是通过ServletWebServerFactory 创建了一个 WebServer,我们继续来看这个 ServletWebServerFactory:

    ServletWebServerFactory 有4个实现类,其中我们最常用的是 TomcatServletWebServerFactory和JettyServletWebServerFactory,而默认的 Web 环境就是 TomcatServletWebServerFactory。

    而到这总算是看到 Tomcat 相关的字眼了。

    来看一下 TomcatServletWebServerFactory 的 getWebServer() 方法:

    public WebServer getWebServer(ServletContextInitializer... initializers) {
        if (this.disableMBeanRegistry) {
            Registry.disableRegistry();
        }
      /** 1、创建Tomcat实例 **/
        Tomcat tomcat = new Tomcat();
        File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        Connector connector = new Connector(this.protocol);
        connector.setThrowOnFailure(true);
        tomcat.getService().addConnector(connector);
        this.customizeConnector(connector);
        /** 2、给创建好的tomcat设置连接器connector **/
        tomcat.setConnector(connector);
        /** 设置不自动部署 **/
        tomcat.getHost().setAutoDeploy(false);
        /** 3、配置Tomcat容器引擎 **/
        this.configureEngine(tomcat.getEngine());
        Iterator var5 = this.additionalTomcatConnectors.iterator();

        while(var5.hasNext()) {
            Connector additionalConnector = (Connector)var5.next();
            tomcat.getService().addConnector(additionalConnector);
        }
      /**
        * 准备Tomcat的StandardContext,并添加到Tomcat中,同时把initializers 注册到类型为
        * TomcatStarter的ServletContainerInitializer中
        **/
        this.prepareContext(tomcat.getHost(), initializers);
        /** 将创建好的Tomcat包装成WebServer返回**/
        return this.getTomcatWebServer(tomcat);
    }

    public Engine getEngine() {
        Service service = this.getServer().findServices()[0];
        if (service.getContainer() != null) {
            return service.getContainer();
        } else {
            Engine engine = new StandardEngine();
            engine.setName("Tomcat");
            engine.setDefaultHost(this.hostname);
            engine.setRealm(this.createDefaultRealm());
            service.setContainer(engine);
            return engine;
        }
    }

    getWebServer() 这个方法创建了 Tomcat 对象,并且做了两件重要的事情:

    1. 把连接器 Connector 对象添加到 Tomcat 中;
    2. 配置容器引擎,configureEngine(tomcat.getEngine());

    首先说一下这个 Connector 连接器,Tomcat 有两个核心功能:

    1. 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
    2. 加载和管理 Servlet,以及具体处理 Request 请求。

    针对这两个功能,Tomcat 设计了两个核心组件来分别完成这两件事,即:连接器(Connector)和容器(Container)。

    整个过程大致就是:Connector 连接器接收连接请求,创建Request和Response对象用于和请求端交换数据,然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine。当Engine处理完请求后,也会通过Connector将响应返回给客户端。

    这里面提到了 Engine,这个是 Tomcat 容器里的顶级容器(Container),我们可以通过 Container 类查看其他的子容器:Engine、Host、Context、Wrapper

    4者的关系是:Engine 是最高级别的容器,Engine 子容器是 Host,Host 的子容器是 Context,Context 子容器是 Wrapper,所以这4个容器的关系就是父子关系,即:Wrapper > Context > Host > Engine (>表示继承)

    至此我们了解了 Engine 这个就是个容器,然后我们再看一下这个 configureEngine(tomcat.getEngine()) 具体干了啥:

    private void configureEngine(Engine engine) {
        engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay);
        Iterator var2 = this.engineValves.iterator();
        while(var2.hasNext()) {
            Valve valve = (Valve)var2.next();
            engine.getPipeline().addValve(valve);
        }
    }

    其中 engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay) 是指定背景线程的执行间隔,例如背景线程会在每隔多长时间后判断session是否失效之类。

    再回到 getWebServer() 方法,最终 getWebServer() 方法返回了 TomcatWebServer。

    return this.getTomcatWebServer(tomcat);

    通过 getTomcatWebServer() 方法,继续下沉:

    /**
     * 构造函数实例化 TomcatWebServer
     **/
    public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
        this.monitor = new Object();
        this.serviceConnectors = new HashMap();
        Assert.notNull(tomcat, "Tomcat Server must not be null");
        this.tomcat = tomcat;
        this.autoStart = autoStart;
        this.initialize();
    }

    private void initialize() throws WebServerException {
      /** 我们在启动 Spring Boot 时经常看到打印这句话 **/
        logger.info("Tomcat initialized with port(s): " + this.getPortsDescription(false));
        synchronized(this.monitor) {
            try {
                this.addInstanceIdToEngineName();
                Context context = this.findContext();
                context.addLifecycleListener((event) -> {
                    if (context.equals(event.getSource()) && "start".equals(event.getType())) {
                        this.removeServiceConnectors();
                    }

                });
                /** 启动 tomcat **/
                this.tomcat.start();
                this.rethrowDeferredStartupExceptions();

                try {
                    ContextBindings.bindClassLoader(context, context.getNamingToken(), this.getClass().getClassLoader());
                } catch (NamingException var5) {
                }

                this.startDaemonAwaitThread();
            } catch (Exception var6) {
                this.stopSilently();
                this.destroySilently();
                throw new WebServerException("Unable to start embedded Tomcat", var6);
            }

        }
    }

    至此,Tomcat 启动过程就很清晰了,总结一下。

    总结

    SpringBoot的启动主要是通过实例化SpringApplication来启动的,启动过程主要做了如下几件事情:

    1. 配置系统属性
    2. 获取监听,发布应用开始启动时间
    3. 初始化输入参数
    4. 配置环境,输出banner
    5. 创建上下文
    6. 预处理上下文
    7. 刷新上下文
    8. 再次刷新上下文
    9. 发布应用已经启动事件
    10. 发布应用启动完成事件

    而启动 Tomcat 是在第7步 刷新上下文 这一步。

    从整个流转过程中我们知道了 Tomcat 的启动主要是实例化两个组件:Connector、Container。

    • Spring Boot 创建 Tomcat 时,会先创建一个根上下文,将 WebApplicationContext 传给 Tomcat;

    • 启动 Web 容器,需要调用 getWebserver(),因为默认的 Web 环境就是 TomcatServletWebServerFactory,所以会创建 Tomcat 的 Webserver,这里会把根上下文作为参数给 TomcatServletWebServerFactory 的 getWebServer();

    • 启动 Tomcat,调用 Tomcat 中 Host、Engine 的启动方法。

    博客园:https://www.cnblogs.com/niceyoo

  • 相关阅读:
    python3 使用opencv 画基本图形
    python 打印 九九表
    Python Date 1–Hello world print
    Linux下 Nginx+vsftpd搭建FTP服务器详细步骤
    Linux 开启端口命令
    No compiler is provided in this environment. Perhaps you are running on a JRE rather than a JDK Maven异常解决方案
    React Fullpage
    Mint-UI组件 MessageBox为prompt 添加判断条件
    简易搭建本地静态服务器
    Mint-UI Picker 三级联动
  • 原文地址:https://www.cnblogs.com/niceyoo/p/14019428.html
Copyright © 2020-2023  润新知