SpringBoot 启动流程探究

作者: 并发量就是我的发量 | 来源:发表于2021-01-25 16:18 被阅读0次

    Spring 的丰富生态备受开发者青睐,尤其是自从 SpringBoot 出现之后去掉了原来的复杂配置,因为 SpringBoot 的理念就是 约定大于配置 ,这让我们省去了很多需要手动配置的过程,就拿 SpringMVC 来说吧各种 XML 配置直接劝退初学者,但是 SpringBoot 的易用性简直是成为了推广 Spring 生态的利器。本篇文章主要是结合 SpringBoot 的源码,来探究 SpringBoot 应用程序的启动流程!

    新建一个 SpringBoot 项目,首先映入眼帘的恐怕就是下面的这个关键的 Main 函数与 @SpringBootApplication 注解吧,我们将从这个注解开始,逐步探究 SpringBoot 应用的启动流程:

    @SpringBootApplication
    public class SpringBootStartApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringBootStartApplication.class, args);
        }
    
    }
    复制代码
    

    @SpringBootApplication 注解实际上是 SpringBoot 提供的一个复合注解,我们来看一看其源码:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
            @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
    public @interface SpringBootApplication {
        ...
    }
    复制代码
    

    关于这里面某些元注解的功能,可以参考我之前的写的一篇博客 《 注解的原理与实现 》 。在这里我们只需要看 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 这三个注解。在 SpringBoot 应用的启动类上用这个三个注解代替 @SpringBootApplication 注解其实也是没问题的:

    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan
    public class SpringBootStartApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringBootStartApplication.class, args);
        }
    
    }
    复制代码
    

    那我们接下来就需要分贝探究这三个注解的功能。

    @SpringBootConfiguration

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Configuration
    public @interface SpringBootConfiguration {
        @AliasFor(
            annotation = Configuration.class
        )
        boolean proxyBeanMethods() default true;
    }
    复制代码
    

    @SpringBootConfiguration 也是来源于 @Configuration,二者功能都是将当前类标注为配置类,@Configuration 用于定义配置类,可替换 xml 配置文件,被注解的类内部包含有一个或多个被 @Bean 注解的方法,这些方法将会被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 类进行扫描,并用于构建 Bean 定义,初始化 Spring 容器,这个貌似一点都不新奇。

    @EnableAutoConfiguration

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {
        ...
    }
    复制代码
    

    @EnableAutoConfiguration 注解启用自动配置,其中最关键的要属 @Import(AutoConfigurationImportSelector.class),借 AutoConfigurationImportSelector,@EnableAutoConfiguration 可以帮助 SpringBoot 应用将所有符合条件的 @Configuration 配置都加载到当前 SpringBoot 创建并使用的 IoC 容器。

    借助于 Spring 框架原有的一个工具类:SpringFactoriesLoader 的支持, @EnableAutoConfiguration 可以智能的自动配置功效才得以大功告成!

    SpringBoot 启动流程探究

    关于这个注解可以参考我的另一篇文章 《SpringBoot 自动配置原理》 ,里面有详细介绍并且有例子。

    @ComponentScan

    @ComponentScan 对应于 XML 配置形式中的 context:component-scan,用于将一些标注了特定注解的 bean 定义批量采集注册到 Spring 的 IoC 容器之中,这些特定的注解大致包括:

    • @Controller* @Entity* @Component* @Service* @Repository

    对于该注解可以通过 basePackages 属性来更细粒度的控制该注解的自动扫描范围,比如:

    @ComponentScan(basePackages = {"xpu.tim.controller","xpu.tim.entity"})
    复制代码
    

    @SpringBootApplication 这个注解看完了, 那么接下来就来看看这个 SpringApplication 以及 run() 方法究竟干了些啥。原始的 SpringCore 中并没有这个类,SpringApplication 里面封装了一套 Spring 应用的启动流程,然而这对用户完全透明,因此我们上手 SpringBoot 时感觉简洁、轻量。

    通过阅读 run 方法的源码我们不难发现,其实是需要构造一个 SpringApplication 对象:

    /**
     * Static helper that can be used to run a {@link SpringApplication} from the
     * specified sources using default settings and user supplied arguments.
     * @param primarySources the primary sources to load
     * @param args the application arguments (usually passed from a Java main method)
     * @return the running {@link ApplicationContext}
     */
    public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return new SpringApplication(primarySources).run(args);
    }
    复制代码
    

    默认的 SpringApplication 执行流程已经可以满足大部分需求,但是若用户想干预这个过程,则可以通过 SpringApplication 在流程某些地方开启的扩展点来完成对流程的扩展,典型的扩展方案那就是使用 set 方法。

    @SpringBootApplication
    public class SpringBootStartApplication {
        public static void main(String[] args) {
            //SpringApplication.run(SpringBootStartApplication.class, args);
            SpringApplication application = new SpringApplication(SpringBootStartApplication.class);
            application.set...(); // 用户自定义扩展点
            application.set...(); // 用户自定义扩展点
            application.run(args);
        }
    }
    复制代码
    

    这样一拆解后我们发现,我们也需要先构造 SpringApplication 类对象,然后调用该对象的 run() 方法。那么接下来就讲讲 SpringApplication 的构造过程以及其 run() 方法的流程,搞清楚了这个,那么也就搞清楚了 SpringBoot 应用是如何运行起来的! 主要需要看以下四个方法:

    SpringBoot 启动流程探究

    1、deduceFromClasspath:用来推断应用的类型:创建的是 REACTIVE 应用、SERVLET 应用、NONE 三种中的一种

    SpringBoot 启动流程探究

    NONE 表示当前的应用即不是一个 web 应用也不是一个 REACTIVE 应用,是一个纯后台的应用。SERVLET 表示当前应用是一个标准的 web 应用。REACTIVE 是 spring5 当中的新特性,表示是一个响应式的 web 应用。而判断的依据就是根据 Classloader 中加载的类。如果是 servlet,则表示是 web,如果是 DispatcherHandler,则表示是一个 REACTIVE 应用,如果两者都不存在,则表示是一个非 web 环境的应用。

    2、setInitializers:使用 SpringFactoriesLoader 查找并加载 classpath 下 META-INF/spring.factories 文件中所有可用的 ApplicationContextInitializer

    [图片上传失败...(image-f8398a-1611562124441)]

    使用 SpringFactoriesLoader 查找并加载 classpath 下 META-INF/spring.factories 文件中的所有可用的 ApplicationListener

    3、setListeners:使用 SpringFactoriesLoader 查找并加载 classpath 下 META-INF/spring.factories 文件中的所有可用的 ApplicationListener

    SpringBoot 启动流程探究

    4、deduceMainApplicationClass:推断并设置 main 方法的定义类

    SpringBoot 启动流程探究

    通过这个几个关键步骤,SpringApplication 完成了实例化。

    之前我们弄清楚了 SpringApplication 的实例化过程,现在看看它的 run 方法究竟干了什么:

    [图片上传失败...(image-99a2d3-1611562124440)]

    1、通过 SpringFactoriesLoader 加载 META-INF/spring.factories 文件,获取并创建 SpringApplicationRunListener 对象;

    2、然后由 SpringApplicationRunListener 来发出 starting 消息;

    3、把参数 args 封装成 DefaultApplicationArguments,并配置当前 SpringBoot 应用将要使用的 Environment;

    4、完成之后,依然有 SpringApplicationRunListener 来发出 environmentPrepared(环境已准备)消息;

    5、创建上下文,根据项目类型创建上下文;

    6、初始化 ApplicationContext,并设置 Environment,加载相关配置等;

    7、由 SpringApplicationRunListener 来发出 contextPrepared 消息,告知 SpringBoot 应用使用的 ApplicationContext 已准备 OK;

    8、将各种 beans 装载入 ApplicationContext,继续由 SpringApplicationRunListener 来发出 contextLoaded 消息,告知 SpringBoot 应用使用的 ApplicationContext 已装填 OK;

    9、refresh ApplicationContext,完成 IoC 容器可用的最后一步;

    10、由 SpringApplicationRunListener 来发出 started 消息 ;

    11、完成最终的程序的启动;

    12、SpringApplicationRunListener 来发出 running 消息,告知程序已运行起来了;

    步骤 4 和 5 之间还有个 PrintBanner,用来打印 Banner

    createApplicationContext()

    下面这段代码主要是根据项目类型创建上下文,并且会注入几个核心组件类:

    protected ConfigurableApplicationContext createApplicationContext() {
        Class<?> contextClass = this.applicationContextClass;
        if (contextClass == null) {
            try {
                switch (this.webApplicationType) {
                    case SERVLET:
                        contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
                        break;
                    case REACTIVE:
                        contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                        break;
                    default:
                        contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
                }
            }
            catch (ClassNotFoundException ex) {
                throw new IllegalStateException(
                    "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
            }
        }
        return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
    }
    复制代码
    

    Web 类型项目创建上下文对象 AnnotationConfigServletWebServerApplicationContext 。这里会把 ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等一些核心组件加入到 Spring 容器。

    refreshContext()

    下面一起来看下 refreshContext(context) 这个方法,这个方法启动 spring 的代码加载了 bean,还启动了内置 web 容器:

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

    点击跟进后发现方法里面是 spring 容器启动代码:

    [图片上传失败...(image-e814be-1611562124440)]

    我们可以看到一个 onRefresh 方法,点进去需要看的是子类实现,我们只看其中一个子类实现:

    SpringBoot 启动流程探究
    @Override
    protected void onRefresh() {
        super.onRefresh();
        try {
            createWebServer();
        }
        catch (Throwable ex) {
            throw new ApplicationContextException("Unable to start web server", ex);
        }
    }
    
    private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = getServletContext();
        if (webServer == null && servletContext == null) {
            // 这个获取webServerFactory还是要进去看看
            ServletWebServerFactory factory = getWebServerFactory();
            this.webServer = factory.getWebServer(getSelfInitializer());
        }
        else if (servletContext != null) {
            try {
                getSelfInitializer().onStartup(servletContext);
            }
            catch (ServletException ex) {
                throw new ApplicationContextException("Cannot initialize servlet context", ex);
            }
        }
        initPropertySources();
    }
    复制代码
    

    我们继续看下 getWebServletFactory() 这个方法,这个里面其实就是选择出哪种类型的 web 容器了:

    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);
    }
    复制代码
    

    我们再去看 factory.getWebServer(getSelfInitializer()) ,转到定义就会看到很熟悉的名字 tomcat:

    SpringBoot 启动流程探究

    内置的 Servlet 容器就是在 onRefresh() 方法里面启动的,至此一个 Servlet 容器就启动 OK 了。

    1、new 了一个 SpringApplication 对象,使用 SPI 技术加载加载 ApplicationContextInitializer、ApplicationListener 接口实例;

    2、调用 SpringApplication.run() 方法;

    3、调用 createApplicationContext() 方法创建上下文对象,创建上下文对象同时会注册 spring 的核心组件类(ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等);

    4、调用 refreshContext() 方法启动 Spring 容器和内置的 Servlet 容器;

    image.png

    如果觉得本文对你有帮助,可以点赞关注支持一下

    相关文章

      网友评论

        本文标题:SpringBoot 启动流程探究

        本文链接:https://www.haomeiwen.com/subject/pjhizktx.html