Spring-BootJava学习笔记开发技巧

SpringBoot 启动流程探究

2021-01-25  本文已影响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 容器之中,这些特定的注解大致包括:

对于该注解可以通过 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

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

上一篇下一篇

猜你喜欢

热点阅读