• SpringBoot如何内置Tomcat


    SpringBoot如何内置Tomcat

    一、前言

    在初次接触 SpringBoot 的时候,就很奇怪为什么直接运行主类的main方法就可以启动程序,但却一直没有深究,直到前段时间,突然听说了一个词,叫做 Cargo Cult ,才开始对自己有所反省,这的确是对我目前状态的一种描述,为了从微小的细节开始,尽自己的努力摆脱这个魔鬼一样的词语,今天决定探究一下 SpringBoot 究竟是如何内置 Tomcat 的(能力限制,目前只尝试进行浅易地“溯源”)。

    二、正文

    1. 启动类

    从我们开始学会新建第一个 SpringBoot 项目开始,我们就一定会看到这样一个类。

    package com.xfc;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    /**
     * 启动
     *
     * @Auther: ErDong
     * @Email: xfc_exclave@163.com
     * @Date: 2019/11/30 11:51
     * @Description:
     */
    @SpringBootApplication
    public class MuYiApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(MuYiApplication.class, args);// 进入run()
        }
    
    }
    

    我们把这个类叫做 主类 ,或者说是 启动类 ,于是,我们进入 run() 方法。

    2. SpringApplication类注释

    这里我们首先阅读一下 SpringApplication 的类注释:

    // SpringApplication.class 类注释
    
    /**
     * Class that can be used to bootstrap and launch a Spring application from a Java main
     * method. By default class will perform the following steps to bootstrap your
     * application:
     *
     * <ul>
     * <li>Create an appropriate {@link ApplicationContext} instance (depending on your
     * classpath)</li>
     * <li>Register a {@link CommandLinePropertySource} to expose command line arguments as
     * Spring properties</li>
     * <li>Refresh the application context, loading all singleton beans</li>
     * <li>Trigger any {@link CommandLineRunner} beans</li>
     * </ul>
     *
     * In most circumstances the static {@link #run(Class, String[])} method can be called
     * directly from your {@literal main} method to bootstrap your application:
     *
     * <pre class="code">
     * &#064;Configuration
     * &#064;EnableAutoConfiguration
     * public class MyApplication  {
     *
     *   // ... Bean definitions
     *
     *   public static void main(String[] args) throws Exception {
     *     SpringApplication.run(MyApplication.class, args);
     *   }
     * }
     * </pre>
     *
     * <p>
     * For more advanced configuration a {@link SpringApplication} instance can be created and
     * customized before being run:
     *
     * <pre class="code">
     * public static void main(String[] args) throws Exception {
     *   SpringApplication application = new SpringApplication(MyApplication.class);
     *   // ... customize application settings here
     *   application.run(args)
     * }
     * </pre>
     *
     * {@link SpringApplication}s can read beans from a variety of different sources. It is
     * generally recommended that a single {@code @Configuration} class is used to bootstrap
     * your application, however, you may also set {@link #getSources() sources} from:
     * <ul>
     * <li>The fully qualified class name to be loaded by
     * {@link AnnotatedBeanDefinitionReader}</li>
     * <li>The location of an XML resource to be loaded by {@link XmlBeanDefinitionReader}, or
     * a groovy script to be loaded by {@link GroovyBeanDefinitionReader}</li>
     * <li>The name of a package to be scanned by {@link ClassPathBeanDefinitionScanner}</li>
     * </ul>
     *
     * Configuration properties are also bound to the {@link SpringApplication}. This makes it
     * possible to set {@link SpringApplication} properties dynamically, like additional
     * sources ("spring.main.sources" - a CSV list) the flag to indicate a web environment
     * ("spring.main.web-application-type=none") or the flag to switch off the banner
     * ("spring.main.banner-mode=off").
     */
    

    我们知道了 SpringApplication 是用于从 Java main 方法引导和启动Spring应用程序,默认情况下,将执行下面几个步骤来引导我们的应用程序:

    1. 创建一个恰当的ApplicationContext实例(取决于类路径)
    2. 注册CommandLinePropertySource,将命令行参数公开为Spring属性。
    3. 刷新应用程序上下文,加载所有单例bean。
    4. 触发全部CommandLineRunner bean。

    大多数情况下,静态 run() 方法可以在我们的启动类的 main() 方法中调用。

    SpringApplication可以从各种不同的源读取bean。 通常建议使用单个@Configuration类来引导,但是我们也可以通过以下方式来设置资源:

    1. 通过AnnotatedBeanDefinitionReader加载完全限定类名。
    2. 通过XmlBeanDefinitionReader加载XML资源位置,或者是通过GroovyBeanDefinitionReader加载groovy脚本位置。
    3. 通过ClassPathBeanDefinitionScanner扫描包名称。

    3. 根据注释逐步进入代码查看

    从主类进入 SpringApplication.run() 方法:

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

    继续进入 run() 方法:

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

    从上面两端代码,我们知道,程序将我们创建的 MuYiApplication.class 添加进一个名为 primarySources 的数组,并且使用当前数创建了一个 SpringApplication 实例。

    接着,进入 SpringBoot 启动的主要逻辑代码段:

    // SpringApplication.class
    
    public ConfigurableApplicationContext run(String... args) {
        // 使用StopWatch对程序部分代码进行计时
    	StopWatch stopWatch = new StopWatch();
    	stopWatch.start();// 开始计时
        ConfigurableApplicationContext context = null;
        // 使用Collection收集错误报告并处理
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
        this.configureHeadlessProperty();
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();
    
        Collection exceptionReporters;
        try {
            // 解析参数args
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            // 从这里进入prepareEnvironment()后,可查看注册CommandLinePropertySource,并将命令行参数公开为Spring属性的逻辑,并返回当前程序的配置环境,这里暂不扩展说明
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            // 读取程序配置中的spring.beaninfo.ignore内容
            this.configureIgnoreBeanInfo(environment);
            // 打印资源目录下banner.txt文件中的内容
            Banner printedBanner = this.printBanner(environment);
            // 根据应用类型创建应用上下文
            context = this.createApplicationContext();
            exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            // 进入refreshContext()进行扩展
            this.refreshContext(context);
            // 允许上下文子类对bean工厂进行后置处理
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop();// 计时结束
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }
    
            listeners.started(context);
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, exceptionReporters, listeners);
            throw new IllegalStateException(var10);
        }
    
        try {
            listeners.running(context);
            return context;
        } catch (Throwable var9) {
    	this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
    	throw new IllegalStateException(var9);
    	}
    }
    

    进入 refreshContext() 刷新应用上下文:

    // SpringApplication.class
    
    private void refreshContext(ConfigurableApplicationContext context) {
        this.refresh(context);// 进入refresh()
        if (this.registerShutdownHook) {
            try {
                context.registerShutdownHook();
            } catch (AccessControlException var3) {
            }
        }
    }
    

    继续进入 refresh() 方法:

    // SpringApplication.class
    
    protected void refresh(ApplicationContext applicationContext) {
        Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
        // 转换为AbstractApplicationContext并调用刷新。
        ((AbstractApplicationContext)applicationContext).refresh();// 进入refresh()
    }
    

    4. 进入AbstractApplicationContext

    继续进入 refresh() 方法:

    // AbstractApplicationContext.class
    
    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();// 进入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() 方法:

    // AbstractApplicationContext.class
    
    protected void onRefresh() throws BeansException {// 进入其子类ServletWebServerApplicationContext.class重写的onRefresh()
    }
    

    到这里,我们看到抽象类 AbstractApplicationContextonRefresh() 方法被以下子类重写:

    1. AbstractRefreshableWebApplicationContext
    2. GenericWebApplicationContext
    3. ReactiveWebServerApplicationContext
    4. ServletWebServerApplicationContext
    5. StaticWebApplicationContext

    4. 揭晓

    我们重点关注 ServletWebServerApplicationContext ,进入该实现类:

    // ServletWebServerApplicationContext.class
    
    protected void onRefresh() {
        super.onRefresh();
        try {
            // 创建web服务
            this.createWebServer();// 进入createWebServer()
        } catch (Throwable var2) {
            throw new ApplicationContextException("Unable to start web server", var2);
        }
    }
    

    根据名称,我们知道,这里即将进入一个与服务创建相关的方法,进入 createWebServer()

    // ServletWebServerApplicationContext.class
    
    private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = this.getServletContext();
        if (webServer == null && servletContext == null) {
            ServletWebServerFactory factory = this.getWebServerFactory();// 进入ServletWebServerFactory
            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();
    }
    

    进入ServletWebServerFactory:

    @FunctionalInterface
    public interface ServletWebServerFactory {
        WebServer getWebServer(ServletContextInitializer... initializers);// 查看getWebServer()的所有实现
    }
    

    至此,我们可以看到有三个类均实现了 ServletWebServerFactory 接口中的 getWebServer 方法,他们分别是:

    JettyServletWebServerFactory.class (org.springframework.boot.web.embedded.jetty)

    TomcatServletWebServerFactory.class (org.springframework.boot.web.embedded.tomcat)

    UndertowServletWebServerFactory.class (org.springframework.boot.web.embedded.undertow)

    5. 继续探究

    从这个方向继续探究下去,我们还可以在源代码中找到更多内容。

    当然,从此前的任何一个分支探究下去,都同样会使我们获益匪浅。

    按照这样的方法,我们可以找到很多问题的答案,例如:

    • SpringBoot 如何加载 application.yml
    • 自定义的 MyServletInitializer 何时被加载?
    • 启动类 main() 方法中的 args 有什么用?
    • logback.xml 在什么地方被加载?
    • 等等等……
  • 相关阅读:
    Java中try-catch-finally的一点理解
    子类继承父类的私有属性
    Java中的String[] args
    Java类和类成员的访问权限修饰符
    JAVA中抽象类与接口的区别
    Java C# .net 和 C C++ 跨平台的区别
    Java中的instanceof关键字
    深入理解JAVA的多态性[转]
    Linux文件系统的目录结构
    硬盘分区
  • 原文地址:https://www.cnblogs.com/xfc-exclave/p/13270355.html
Copyright © 2020-2023  润新知