【Spring Boot系列1】一文带你了解Spring Boot(下)

简介: 这篇文章其实只讲述了SpringBoot的执行流程,如果只是纯粹使用SpringBoot,也可以不用去了解这些,因为只需要知道怎么用就可以,但是学习一门技术,还是需要多去深究一下,不能仅仅停留在会使用的基础上。

_OI3LP@8~@K2SN6H@(0()QR.jpg

主要讲解Spring Boost的执行流程。(建议大家先看《【Spring Boot系列1】一文带你了解Spring Boot(上)》,然后再看本篇内容。)


SpringApplication.run执行流程


执行流程

我们看到Spring Boot的启动类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

当执行run()方法时,该方法的主要流程大体可以归纳如下:

  1. 如果我们使用的是 SpringApplication 的静态 run 方法,那么,这个方法里面首先需要创建一个 SpringApplication 对象实例,然后调用这个创建好的 SpringApplication 的实例 run方 法。在 SpringApplication 实例初始化的时候,它会提前做几件事情:
  • 根据 classpath 里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该创建一个为 Web 应用使用的 ApplicationContext 类型,还是应该创建一个标准 Standalone 应用使用的 ApplicationContext 类型。
  • 使用 SpringFactoriesLoader 在应用的 classpath 中查找并加载所有可用的 ApplicationContextInitializer。
  • 使用 SpringFactoriesLoader 在应用的 classpath 中查找并加载所有可用的 ApplicationListener。
  • 推断并设置 main 方法的定义类。
  1. SpringApplication 实例初始化完成并且完成设置后,就开始执行 run 方法的逻辑了,方法执行开始,首先遍历执行所有通过 SpringFactoriesLoader 可以查找到并加载的 SpringApplicationRunListener,调用它们的 started() 方法,告诉这些 SpringApplicationRunListener,“嘿,SpringBoot 应用要开始执行咯!”。
  2. 创建并配置当前 SpringBoot 应用将要使用的 Environment(包括配置要使用的 PropertySource 以及 Profile)。
  3. 遍历调用所有 SpringApplicationRunListener 的 environmentPrepared()的方法,告诉它们:“当前 SpringBoot 应用使用的 Environment 准备好咯!”。
  4. 如果 SpringApplication的showBanner 属性被设置为 true,则打印 banner(SpringBoot 1.3.x版本,这里应该是基于 Banner.Mode 决定 banner 的打印行为)。这一步的逻辑其实可以不关心,我认为唯一的用途就是“好玩”(Just For Fun)。
  5. 根据用户是否明确设置了applicationContextClass 类型以及初始化阶段的推断结果,决定该为当前 SpringBoot 应用创建什么类型的 ApplicationContext 并创建完成,然后根据条件决定是否添加 ShutdownHook,决定是否使用自定义的 BeanNameGenerator,决定是否使用自定义的 ResourceLoader,当然,最重要的,将之前准备好的 Environment 设置给创建好的 ApplicationContext 使用。
  6. ApplicationContext 创建好之后,SpringApplication 会再次借助 Spring-FactoriesLoader,查找并加载 classpath 中所有可用的 ApplicationContext-Initializer,然后遍历调用这些 ApplicationContextInitializer 的 initialize(applicationContext)方法来对已经创建好的 ApplicationContext 进行进一步的处理。
  7. 遍历调用所有 SpringApplicationRunListener 的 contextPrepared()方法,通知它们:“SpringBoot 应用使用的 ApplicationContext 准备好啦!”
  8. 最核心的一步,将之前通过 @EnableAutoConfiguration 获取的所有配置以及其他形式的 IoC 容器配置加载到已经准备完毕的 ApplicationContext。
  9. 遍历调用所有 SpringApplicationRunListener 的 contextLoaded() 方法,告知所有 SpringApplicationRunListener,ApplicationContext "装填完毕"!
  10. 调用 ApplicationContext 的 refresh() 方法,完成 IoC 容器可用的最后一道工序。
  11. 查找当前 ApplicationContext 中是否注册有 CommandLineRunner,如果有,则遍历执行它们。
  12. 正常情况下,遍历执行 SpringApplicationRunListener 的 finished() 方法,告知它们:“搞定!”。(如果整个过程出现异常,则依然调用所有 SpringApplicationRunListener 的 finished() 方法,只不过这种情况下会将异常信息一并传入处理)。

至此,一个完整的 SpringBoot 应用启动完毕!

整个过程看起来冗长无比,但其实很多都是一些事件通知的扩展点,如果我们将这些逻辑暂时忽略,那么,其实整个 SpringBoot 应用启动的逻辑就可以压缩到极其精简的几步:

NM}8RJXFI$9{DCK3WCNUH32.png

个人理解:

通过框架扫描出所有的ApplicationListener,然后通过主流程执行以下操作:start->创建并准备Environment->通过Environment创建上下文Context->initialize上下文Context->将Bean注入Spring->通过refresh启动程序->finish。

对于主流程执行的每一步,都会去遍历所有的ApplicationListener,然后告诉他们每一步流程执行到哪里。我理解其实就是通过ApplicationListener提供的钩子,然后主程序通过这些钩子告诉每个Application需要执行哪一步,然后完成所有Application的初始化和启动工作。

前后对比我们就可以发现,其实 SpringApplication 提供的这些各类扩展点近乎“喧宾夺主”,占据了一个 Spring 应用启动逻辑的大部分“江山”,除了初始化并准备好 ApplicationContext,剩下的大部分工作都是通过这些扩展点完成的,所以,我们接下来对各类扩展点进行逐一剖析。


SpringApplicationRunListener

SpringApplicationRunListener 是一个只有 SpringBoot 应用的 main 方法执行过程中接收不同执行时点事件通知的监听者:

public interface SpringApplicationRunListener {
    void started();
    void environmentPrepared(ConfigurableEnvironment environment);
    void contextPrepared(ConfigurableApplicationContext context);
    void contextLoaded(ConfigurableApplicationContext context);
    void finished(ConfigurableApplicationContext context, Throwable exception);
}

对于我们来说,基本没什么常见的场景需要自己实现一个 Spring-ApplicationRunListener,假设我们真的有场景需要自定义一个 SpringApplicationRunListener 实现,可以通过下述方式:

public class DemoSpringApplicationRunListener implements SpringApplicationRunListener {
    @Override
    public void started() {
        // do whatever you want to do
    }
    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        // do whatever you want to do
    }
    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        // do whatever you want to do
    }
    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        // do whatever you want to do
    }
    @Override
    public void finished(ConfigurableApplicationContext context, Throwable exception) {
        // do whatever you want to do
    }
}

之后,我们可以通过 SpringFactoriesLoader 立下的规矩,在当前 SpringBoot 应用的 classpath 下的 META-INF/spring.factories 文件中进行类似如下的配置:

org.springframework.boot.SpringApplicationRunListener=\com.keevol.springboot.demo.DemoSpringApplicationRunListener

然后 SpringApplication 就会在运行的时候调用它啦!

这里就是定义一个自己的SpringApplicationRunListener,只需要实现里面的钩子,剩下的框架可以帮我们做所有的事情。


ApplicationContextInitializer

ApplicationContextInitializer 也是 Spring 框架原有的概念,这个类的主要目的就是在 ConfigurableApplicationContext 类型(或者子类型)的 ApplicationContext 做 refresh 之前,允许我们对 ConfigurableApplicationContext 的实例做进一步的设置或者处理。

实现一个 ApplicationContextInitializer 很简单,因为它只有一个方法需要实现:

public class DemoApplicationContextInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        // do whatever you want with applicationContext,
        // e.g.
        applicationContext.registerShutdownHook();
    }
}

不过,一般情况下我们基本不会需要自定义一个 ApplicationContext-Initializer,即使 SpringBoot 框架默认也只是注册了三个实现:

org.springframework.context.ApplicationContextInitializer=
\org.springframework.boot.context.ConfigurationWarningsApplication-ContextInitializer,
\org.springframework.boot.context.ContextIdApplicationContextInitia-lizer,
\org.springframework.boot.context.config.DelegatingApplicationContex-tInitializer

如果我们真的需要自定义一个 ApplicationContextInitializer,那么只要像上面这样,通过 SpringFactoriesLoader 机制进行配置,或者通过 SpringApplication.addInitializers(..)设置即可。

这个其实就是准备上下文,为应用启动作准备,我们可以定义自己的ApplicationContextInitializer,不过一般不会自己定义,用系统默认的即可。


ApplicationContextInitializer

CommandLineRunner 是很好的扩展接口,不是 Spring 框架原有的“宝贝”,它属于 SpringBoot 应用特定的回调扩展接口。源码如下所示:

public interface CommandLineRunner {
    void run(String... args) throws Exception;
}

CommandLineRunner 需要大家关注的其实就两点:

  • 所有 CommandLineRunner 的执行时点在 SpringBoot 应用的 Application-Context 完全初始化开始工作之后(可以认为是 main 方法执行完成之前最后一步)。
  • 只要存在于当前 SpringBoot 应用的 ApplicationContext 中的任何 Command-LineRunner,都会被加载执行(不管你是手动注册这个 CommandLineRunner 到 IoC 容器,还是自动扫描进去的)。

与其他几个扩展点接口类型相似,建议 CommandLineRunner 的实现类使用 @org.springframework.core.annotation.Order 进行标注或者实现 org.springframework.core.Ordered 接口,便于对它们的执行顺序进行调整,这其实十分重要,我们不希望顺序不当的 CommandLineRunner 实现类阻塞了后面其他 CommandLineRunner 的执行。

这里也就记录一下,其实没有看懂有啥用~~衰!


SpringApplication.run源码分析


上面讲述了Spring Boot的执行流程,我们再从源码的角度,结合上面的讲解,再看看。项目启动的入口就不再贴代码了,直接进入主流程。


SpringApplication实例创建

public SpringApplication(Class<?>... primarySources) {
    this(null, primarySources);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.sources = new LinkedHashSet();
    // Banner 模式
    this.bannerMode = Mode.CONSOLE;
    this.logStartupInfo = true;
    // 是否添加 JVM 启动参数
    this.addCommandLineProperties = true;
    this.addConversionService = true;
    this.headless = true;
    this.registerShutdownHook = true;
    this.additionalProfiles = new HashSet();
    this.isCustomEnvironment = false;
    // 资源加载器
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    //项目启动类 SpringbootDemoApplication.class设置为属性存储起来
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    //设置应用类型是SERVLET应用(Spring 5之前的传统MVC应用)还是REACTIVE应用(Spring 5开始出现的WebFlux交互式应用)
    // deduceFromClasspath()方法用于查看 ClassPath类路径下是否存在某个特征类,从而判断webApplicationType
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 设置初始化器(Initializer),最后会调用这些初始化器
    //所谓的初始化器就是org.springframework.context.ApplicationContextInitializer的实现类,在Spring上下文被刷新之前进行初始化的操作
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 设置监听器(Listener)
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 初始化 mainApplicationClass 属性:用于推断并设置项目main()方法启动的主程序启动类
    this.mainApplicationClass = deduceMainApplicationClass();
}


项目初始化启动

public ConfigurableApplicationContext run(String... args) {
    // 创建 StopWatch 对象,并启动。StopWatch 主要用于简单统计 run 启动过程的时长。
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 初始化应用上下文和异常报告集合
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 配置 headless 属性
    configureHeadlessProperty();
    //(1)获取并启动监听器
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        // 创建  ApplicationArguments 对象 初始化默认应用参数类
        // args是启动Spring应用的命令行参数,该参数可以在Spring应用中被访问。如:--server.port=9000
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        //(2)项目运行环境Environment的预配置
        // 创建并配置当前SpringBoot应用将要使用的Environment
        // 并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        // 排除不需要的运行环境
        configureIgnoreBeanInfo(environment);
        // 准备Banner打印器 - 就是启动Spring Boot的时候打印在console上的ASCII艺术字体
        Banner printedBanner = printBanner(environment);
        // (3)创建Spring容器
        // 根据 webApplicationType 进行判断,确定容器类型,如果为 SERVLET 类型,会反射创建相应的字节码,AnnotationConfigServletWebServerApplicationContext, 接着使用之前传世话设置的context、environment、listeners、applicationArgument 进行应用上下文的组装配置
        context = createApplicationContext();
        // 获得异常报告器 SpringBootExceptionReporter 数组
        //这一步的逻辑和实例化初始化器和监听器的一样,
        // 都是通过调用 getSpringFactoriesInstances 方法来获取配置的异常类名称并实例化所有的异常处理类。
        exceptionReporters = getSpringFactoriesInstances(
            SpringBootExceptionReporter.class,
            new Class[] { ConfigurableApplicationContext.class }, context);
        // (4)Spring容器前置处理
        //这一步主要是在容器刷新之前的准备动作。包含一个非常关键的操作:将启动类注入容器,为后续开启自动化配置奠定基础。
        prepareContext(context, environment, listeners, applicationArguments,
                       printedBanner);
        // (5):刷新容器
        // 开启(刷新)Spring 容器,通过refresh方法对整个IoC容器的初始化(包括Bean资源的定位、解析、注册等等),同时向JVM运行时注册一个关机钩子,在JVM关机时关闭这个上下文,除非它当时已经关闭
        refreshContext(context);
        // (6):Spring容器后置处理
        //扩展接口,设计模式中的模板方法,默认为空实现。
        // 如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它后置处理
        afterRefresh(context, applicationArguments);
        // 停止 StopWatch 统计时长
        stopWatch.stop();
        // 打印 Spring Boot 启动的时长日志。
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        // (7)发出结束执行的事件通知
        listeners.started(context);
        // (8):执行Runners
        //用于调用项目中自定义的执行器XxxRunner类,使得在项目启动完成后立即执行一些特定程序
        //Runner 运行器用于在服务启动时进行一些业务初始化操作,这些操作只在服务启动后执行一次。
        //Spring Boot提供了ApplicationRunner和CommandLineRunner两种服务接口
        callRunners(context, applicationArguments);
    } catch (Throwable ex) {
        // 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }
    //   (9)发布应用上下文就绪事件
    //表示在前面一切初始化启动都没有问题的情况下,使用运行监听器SpringApplicationRunListener持续运行配置好的应用上下文ApplicationContext,
    // 这样整个Spring Boot项目就正式启动完成了。
    try {
        listeners.running(context);
    } catch (Throwable ex) {
        // 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    //返回容器
    return context;
}


执行流程图

其实整个启动流程,主要分2大块:{TO0(OQN@_5~8DR}~UH[O47.png


后记


这篇文章其实只讲述了SpringBoot的执行流程,如果只是纯粹使用SpringBoot,也可以不用去了解这些,因为只需要知道怎么用就可以,但是学习一门技术,还是需要多去深究一下,不能仅仅停留在会使用的基础上。

当然,这些知识也不是我总结的,我也是处于好奇,然后就从网上找到的相关资料,然后加入一些自己的理解,反正初步学习完后,感觉心里是有些底了,不至于一直是雾里探花的感觉。其实本篇文章对SpringBoot执行流程的分析是比较浅显的,网上也有很多详细的源码分析资料,感兴趣的同学可以去Debug,然后再仔细研究。

相关文章
|
2月前
|
前端开发 Java 应用服务中间件
《深入理解Spring》 Spring Boot——约定优于配置的革命者
Spring Boot基于“约定优于配置”理念,通过自动配置、起步依赖、嵌入式容器和Actuator四大特性,简化Spring应用的开发与部署,提升效率,降低门槛,成为现代Java开发的事实标准。
|
2月前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
2月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
401 2
|
3月前
|
人工智能 Java 机器人
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
Spring AI Alibaba集成Ollama,基于Java构建本地大模型应用,支持流式对话、knife4j接口可视化,实现高隐私、免API密钥的离线AI服务。
2340 1
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
存储 JSON Java
511 0
|
3月前
|
人工智能 Java 开发者
【Spring】原理解析:Spring Boot 自动配置
Spring Boot通过“约定优于配置”的设计理念,自动检测项目依赖并根据这些依赖自动装配相应的Bean,从而解放开发者从繁琐的配置工作中解脱出来,专注于业务逻辑实现。
1242 0
|
4月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
743 3
|
8月前
|
前端开发 Java Maven
Spring 和 Spring Boot 之间的比较
本文对比了标准Spring框架与Spring Boot的区别,重点分析两者在模块使用(如MVC、Security)上的差异。Spring提供全面的Java开发基础设施支持,包含依赖注入和多种开箱即用的模块;而Spring Boot作为Spring的扩展,通过自动配置、嵌入式服务器等功能简化开发流程。文章还探讨了两者的Maven依赖、Mvc配置、模板引擎配置、启动方式及打包部署等方面的异同,展示了Spring Boot如何通过减少样板代码和配置提升开发效率。总结指出,Spring Boot是Spring的增强版,使应用开发、测试与部署更加便捷高效。
1158 11