深入理解Spring IOC之扩展篇(九)、SpringBoot中重要event介绍,顺便简单讲下SB的启动流程(一)

简介: 深入理解Spring IOC之扩展篇(九)、SpringBoot中重要event介绍,顺便简单讲下SB的启动流程(一)

之前我在这篇 Spring中的event以及自定义event中介绍了event的概念以及自定义我们event及其对应的listener,现在我们已经能够自定义我们自己的event了,但是其实这种扩展在实际的开发中用的并不多,更多的时候,我们更期望在容器启动或者容器销毁以及容器刷新的时候去做一些事情,这时候就需要Spring自身提供的几种事件了。


这里没有说自定义事件就没有Spring原本为我们提供的事件更重要的意思,只是完全是在说使用场景而已。关于使用场景,大家可以自行把握。


我们之前文章提过,Spring中事件的超类是ApplicationEvent,它下面有个重要的子类是ApplicationContextEvent(context包下),还有一个很重要的子类是SpringApplicationEvent(springboot中)。你看我这里特地的区分出了两个子类的包,这个意思就是说ApplicationContextEvent是属于原本spring中的,而SpringApplicationEvent是属于SpringBoot里面的,这也代表着这两个子类的使用场景是可以不同的。


作者这里只介绍SpringBoot中的几种event,毕竟现在多数新的应用会基于它来构建


我们首先需要来了解的是SpringApplicationEvent,我们来看看源码中的文档注释是怎么说的:



翻译过来的意思是这个家伙是SpringApplication中关于ApplicationEvent的基类,我们知道SpringApplication其实指的就是一个SpringBoot应用,ApplicationEvent我们有说过,它是Spring中event的基类。也就是说,SpringApplicationEvent是SpringBoot应用中event的基类,我们一起来看看,SpringBoot中都有哪些类型的事件:



我们可以看到,一共有七种类型的事件,其实这其中类型也意味着SpringBoot应用进入到了不同的状态。我们一个一个来看看这些事件:


ApplicationStartingEvent


我们首先来到SpringApplication的run方法中看看,


public ConfigurableApplicationContext run(String... args) {
    // 暂且只关注这些代码
        // StopWatch 是一个用于记录任务过程中任务数量以及任务耗时的工具
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        //上面四句代码其实也就stopWatch.start做了点事情,其他的几乎可以说什么事情都没做
        // 设置headless模式
    configureHeadlessProperty();
        // SpringApplicationRunListeners 是用于在启动过程中发布各类事件的监听器,这里获取其实就是new了一下这个对象
    SpringApplicationRunListeners listeners = getRunListeners(args);
        // 发布启动事件(关注点)
    listeners.starting();
    .......略过


这段代码是一个SpringBoot应用在启动时候先执行的代码,逻辑非常简单,我们这里关注最后这句isteners.starting(),点进去看看,我们发现:


public void starting() {
    for (SpringApplicationRunListener listener : this.listeners) {
      // 调用下面的代码
        listener.starting();
    }
}
public void starting() {
  // 上面调用的是这里
    this.initialMulticaster.multicastEvent(
            new ApplicationStartingEvent(this.application, this.args));
}


我们很容易就可以发现,这里就是发布了一个ApplicationStartingEvent而已,这意味着从这里应用才开始进入“启动中”的这样的状态,我们如果想监听这个事件应该怎么做呢?注意此时的做法特殊一些,废话不多说,直接上代码:

监听器


// 注意这里不需要Component注解
public class TestListener3 implements ApplicationListener<ApplicationStartingEvent>{
    @Override
    public void onApplicationEvent(ApplicationStartingEvent event) {
        System.out.println("发现你了,呵呵");
    }
}


监听器的定义还是和之前一样,但是注入方式变了,我们需要这样做:


@SpringBootApplication
public class SpringbootdemoApplication {
  public static void main(String[] args) {
    SpringApplication springBootApplication = new SpringApplication(SpringbootdemoApplication.class);
        // 必须这样注入
    springBootApplication.addListeners(new TestListener3());
        // run方法参数必须传args,否则不能多profile切换
    springBootApplication.run(args);
  }
}


原因很简单了,因为此时ApplicationContext还没有初始化,因此你用了component注解也没什么卵用,因此需要我们自己人肉敲代码注入(如果你是要搞框架扩展的话需要利用SPI)。然后我们点击main方法启动,可以看到如下结果:



在启动日志的最开始的地方,就执行了我们自己的逻辑。


ApplicationEnvironmentPreparedEvent


我们继续来看run方法:


// 刚才发布启动事件的地方
    listeners.starting();
    try {
        // 这块其实就是把命令行的参数封装了下
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    // 这是个很重要的地方,也是我们的关注点
        ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);


我们来看看prepareEnvironment方法:


private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
    // 1、这步是根据当前的SpringBoot应用类型,去创建不同的Environment对象
        // 这里的应用类型,指的是你是个servlet应用还是一个webflux应用
    ConfigurableEnvironment environment = getOrCreateEnvironment();
        // 2、这步是配置环境变量
    configureEnvironment(environment, applicationArguments.getSourceArgs());
        // 3、发布环境准备好的事件
    listeners.environmentPrepared(environment);
        // 4、将获取到的environment对象绑定到当前springboot应用上
    bindToSpringApplication(environment);
        // 5、将envrionment转化成真正所需要的类型(如果有必要做这步的话)
    if (!this.isCustomEnvironment) {
      environment = new EnvironmentConverter(getClassLoader())
          .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
    }
        // 6、把environment中所有的读到打的变量放到一个list中包装一下,再次增加到environment对象中
        // 说实话,实在不知道这样做的目的是啥。。。
    ConfigurationPropertySources.attach(environment);
    return environment;
  }


第1步很简单,就是根据不同的 type选择new了不同的Environment对象,2、3、4步都是做了些事情的(3,4很重要),我们一个一个来看,我们来看看第2步的代码:


protected void configureEnvironment(ConfigurableEnvironment environment,
      String[] args) {
        // 增加了一个用作转化格式以及类型的对象,比如需要把配置中的string变成int,或者是时间的format
    if (this.addConversionService) {
      ConversionService conversionService = ApplicationConversionService
          .getSharedInstance();
      environment.setConversionService(
          (ConfigurableConversionService) conversionService);
    }
        // 配置属性,这里其实就是将启动参数和默认参数加入到environment对象中
        // 默认参数是需要我们new SpringApplication这个对象之后,去调用setDefaultProperties添加的
    configurePropertySources(environment, args);
        // 这里就是配置一下profile,主要是额外profile和activeprofile,额外profile和上面增加默认参数
        // 的方式类似,这一步对于我们来说不是很重要
    configureProfiles(environment, args);
}


第3步做的事情是发布ApplicationEnvironmentPreparedEvent事件,注意,这块意味着什么呢?我来解释一下,在这个事件刚刚发布之后,意味着所有的环境变量已经被spring读到,但是却还没有读到配置,也就是你的application.properties或者是application.yml还没有被读到,但是下一步就要去读了,这个监听器也同样需要我们人肉编码去注入的,我就不再次demo这样的小东西了,有兴趣的读者可以试试。 第4步就是要去读我们的配置,这是很重要的一步,这步中,将我们配置文件中的东西,以key-value的形式放到了environment对象中的propertySource中,这是整个SpringBoot应用第一次去拿到我们的配置,你看啊,我用词很严谨的,特地说了个第一次,你肯定很吃惊,难道还会有第二次?当然是的,不然配置中心咋玩?至于什么时候还可以去拉取,这个马上会说,别急,SB启动流程很长,我们慢慢来。


你之所以觉得看着很简单,是因为我在背后默默研究了很多,然后一字一句的想办法说清楚这些,求个赞,你的赞就是我最大的动力


ApplicationContextInitializedEvent 和 ApplicationPreparedEvent


我们顺着run方法继续往下看:


// 上面讲的地方
      ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
            // 配置忽略BeanInfo
      configureIgnoreBeanInfo(environment);
            // 打印banner
      Banner printedBanner = printBanner(environment);
            // 根据不同的应用类型创建不同的ApplicationContext对象
      context = createApplicationContext();
            // 获取异常报告器
      exceptionReporters = getSpringFactoriesInstances(
          SpringBootExceptionReporter.class,
          new Class[] { ConfigurableApplicationContext.class }, context);
            // 准备上下文,这是一个非常重要的点
      prepareContext(context, environment, listeners, applicationArguments,
          printedBanner);


上面的代码我们只需要关注最后一句prepareContext即可,这里面做了很多事情,我们来看看这里的代码:


private void prepareContext(ConfigurableApplicationContext context,ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
      ApplicationArguments applicationArguments, Banner printedBanner) {
        // 给创建好的ApplicationContext对象设置environment
    context.setEnvironment(environment);
        // 给applicationContext设置几个特殊的bean
    postProcessApplicationContext(context);
        // 1.这一步是非常重要的一步,调用所有的ApplicationContextInitializer的initialize方法
    applyInitializers(context);
        // 2.发布ApplicationContextInitializedEven事件
    listeners.contextPrepared(context);
        // 打印日志
    if (this.logStartupInfo) {
      logStartupInfo(context.getParent() == null);
      logStartupProfileInfo(context);
    }
    // 拿到已经创建好的BeanFactory实例(是在创建ApplicationContext实例时创建的)
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        // 注册两个特殊的bean
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
      beanFactory.registerSingleton("springBootBanner", printedBanner);
    }
        // 设置不允许覆盖BeanDefinition
    if (beanFactory instanceof DefaultListableBeanFactory) {
      ((DefaultListableBeanFactory) beanFactory)
          .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
    }
    // 3.获取所有的source(马上就会说这是个啥)
    Set<Object> sources = getAllSources();
        // 断言 source不能为空,,,因为若为空,接下来的流程是走不了的
    Assert.notEmpty(sources, "Sources must not be empty");
        // 4. 加载所有source对应的东西
    load(context, sources.toArray(new Object[0]));
        // 5. 发布ApplicationPreparedEvent事件
    listeners.contextLoaded(context);
  }


我们可以看到哈,这个prepareContext方法,还是做了不少事情的,其中重要的,我都用数字标了出来以方便我们一件一件来说,我们来看第1处applyInitializers这里,


protected void applyInitializers(ConfigurableApplicationContext context) {
      for (ApplicationContextInitializer initializer : getInitializers()) {
          // 拿initializer中的泛型,这个泛型代表了这个initializer初始化所对应的applicationContext的类型
          Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
                  initializer.getClass(), ApplicationContextInitializer.class);
          // 如果initializer初始化所对应的applicationContext的类型和context的类型不一样,则报错,因为无法做这个初始化动作
          Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
          // 执行初始化动作
          initializer.initialize(context);
      }
}


乍看之下好像感觉这段代码并没有什么营养,就是调用了ApplicationContextInitializer的initialize方法而已,但是这里其实可以做非常多非常多的事情,比如配置,前文已经说过,走到这里之前,已经把本地的配置加载完了,那么此时,便可以从别的地方拉取配置了,你是不是感觉这像是某种框架?没错,很多配置中心比如apollo就是在这里将配置中心的配置加载进来的。

第2处这里,主要就是发布ApplicationContextInitializedEvent这个事件,这时候,意味着ApplicationContext容器已经初始化完成,并且所有的配置已经加载好,其实嘛,严格来说,外部配置并不一定已经被拉下来了,也就是说其实在事件发布的时候,外部配置并不一定非得拉下来,为什么呢?我等会说这个的原因,我们先往下看。

第3处这里其实就是获取所有的source,什么是source呢?其实就是config class,具体的讲,就是你每次启动SpringBoot应用的启动类,当然你也可以去自定义其他类来做config class,SpringBoot对这个config class没有特殊的要求,所以你用啥来做config class都行,有没有意义就看你心情了。

第4处这里是个很重要的地方,我们一起看看这里的代码:


protected void load(ApplicationContext context, Object[] sources) {
    // just 打印下日志
    if (logger.isDebugEnabled()) {
      logger.debug(
          "Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
    }
        // 创建一个BeanDefinitionLoader实例
    BeanDefinitionLoader loader = createBeanDefinitionLoader(
        getBeanDefinitionRegistry(context), sources);
        // 设置bean名称生成器
    if (this.beanNameGenerator != null) {
      loader.setBeanNameGenerator(this.beanNameGenerator);
    }
        // 设置资源加载器
    if (this.resourceLoader != null) {
      loader.setResourceLoader(this.resourceLoader);
    }
        // 设置environment
    if (this.environment != null) {
      loader.setEnvironment(this.environment);
    }
        // 加载beanDefinition(并没有加载所有的beanDefinition)
    loader.load();
}


创建BeanDefinitionLoader实例代码如下:


// 就是简单的new了个对象
protected BeanDefinitionLoader createBeanDefinitionLoader(
      BeanDefinitionRegistry registry, Object[] sources) {
    return new BeanDefinitionLoader(registry, sources);
}
-----------------------高傲的分割线---------------------
// BeanDefinitionLoader构造方法:
BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
    // assert下必要条件
    Assert.notNull(registry, "Registry must not be null");
    Assert.notEmpty(sources, "Sources must not be empty");
    this.sources = sources;
        // 加载注解bean definition的
    this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
        // XmlBeanDefinitionReader 是专门从xml中加载bean definition的
    this.xmlReader = new XmlBeanDefinitionReader(registry);
    if (isGroovyPresent()) {
      this.groovyReader = new GroovyBeanDefinitionReader(registry);
    }
        // 默认的注解扫描器,也是加载beandefiniton的,只不过这个是扫描指定包的
    this.scanner = new ClassPathBeanDefinitionScanner(registry);
        // 设置需要排除的类,因为sources都是在调用BeanDefinitionLoader的load方法注入的,不需要再次注入到spring中
    this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));
}


我们可以看到,所谓SpringBoot中的BeanDefinitionLoader,其实就是结合了三种BeanDefinitionReader而已,除过不常用的Groovy的这个,其他两种就是从xml和注解中加载BeanDefinition的加载器。SpringBoot能从各种各样的配置中加载bean的原因就是这样而已。

我们继续看上面的上面的代码块中最后这里调用了BeanDefinitionLoader的load方法,我们来看看load方法是干嘛了:


public int load() {
    int count = 0;
    for (Object source : this.sources) {
          // 调用了下面的方法
      count += load(source);
    }
    return count;
}
--------------------------高傲的分割线-----------------------------
// 上边调用了这里
private int load(Object source) {
    // 用种姿势加载source
    Assert.notNull(source, "Source must not be null");
    if (source instanceof Class<?>) {
      return load((Class<?>) source);
    }
    if (source instanceof Resource) {
      return load((Resource) source);
    }
    if (source instanceof Package) {
      return load((Package) source);
    }
    if (source instanceof CharSequence) {
      return load((CharSequence) source);
    }
    throw new IllegalArgumentException("Invalid source type " + source.getClass());
}


好了,load方法说完了,各位应该还知道我们此时在哪个位置吧,此时是在run方法中的prepareContext的倒数第二个方法这里,现在来看最后一个方法,也就是发布事件这个,这里就是发布了一下ApplicationPreparedEvent而已,此时意味着ApplicationContext已经准备好了,可以去refresh了,refresh就是去加载所有的bean,这也是spring ioc的核心流程了,在这一步之前的以及这一步事件中,你都可以去拉取外部配置,去修改已经读到的配置,具体是覆盖还是增加看你自己的需求。这里如果你要做注册中心,那么注意下,这是你可以拉配置的最后时机了,因为下一步就要注入了,你配置都没拉下来的话那还玩个毛线。这里之前到读取到本地配置之后你都可以拉取外部配置。总之这里结束后,容器刷新的所有条件都应该已经准备好。

为了让你更好的理解,剩下的内容我放到下一篇,我们下周见

目录
相关文章
|
4天前
|
监控 Java 应用服务中间件
spring和springboot的区别
spring和springboot的区别
20 1
|
4天前
|
Java 关系型数据库 MySQL
【MySQL × SpringBoot 突发奇想】全面实现流程 · xlsx文件,Excel表格导入数据库的接口(下)
【MySQL × SpringBoot 突发奇想】全面实现流程 · xlsx文件,Excel表格导入数据库的接口
13 0
|
4天前
|
Java 关系型数据库 MySQL
【MySQL × SpringBoot 突发奇想】全面实现流程 · xlsx文件,Excel表格导入数据库的接口(上)
【MySQL × SpringBoot 突发奇想】全面实现流程 · xlsx文件,Excel表格导入数据库的接口
20 0
|
4天前
|
前端开发 关系型数据库 MySQL
【MySQL × SpringBoot 突发奇想】全面实现流程 · 数据库导出Excel表格文件的接口
【MySQL × SpringBoot 突发奇想】全面实现流程 · 数据库导出Excel表格文件的接口
26 0
|
4天前
|
移动开发 前端开发 NoSQL
ruoyi-nbcio从spring2.7.18升级springboot到3.1.7,java从java8升级到17(二)
ruoyi-nbcio从spring2.7.18升级springboot到3.1.7,java从java8升级到17(二)
51 0
|
4天前
|
XML Java 数据库连接
Spring框架与Spring Boot的区别和联系
Spring框架与Spring Boot的区别和联系
24 0
|
4天前
|
Java Spring 容器
深入理解Spring Boot启动流程及其实战应用
【5月更文挑战第9天】本文详细解析了Spring Boot启动流程的概念和关键步骤,并结合实战示例,展示了如何在实际开发中运用这些知识。
19 2
|
4天前
|
SQL Java 数据库连接
Springboot框架整合Spring JDBC操作数据
JDBC是Java数据库连接API,用于执行SQL并访问多种关系数据库。它包括一系列Java类和接口,用于建立数据库连接、创建数据库操作对象、定义SQL语句、执行操作并处理结果集。直接使用JDBC涉及七个步骤,包括加载驱动、建立连接、创建对象、定义SQL、执行操作、处理结果和关闭资源。Spring Boot的`spring-boot-starter-jdbc`简化了这些步骤,提供了一个在Spring生态中更便捷使用JDBC的封装。集成Spring JDBC需要添加相关依赖,配置数据库连接信息,并通过JdbcTemplate进行数据库操作,如插入、更新、删除和查询。
|
4天前
|
SQL Java 数据库连接
Springboot框架整合Spring Data JPA操作数据
Spring Data JPA是Spring基于ORM和JPA规范封装的框架,简化了数据库操作,提供增删改查等接口,并可通过方法名自动生成查询。集成到Spring Boot需添加相关依赖并配置数据库连接和JPA设置。基础用法包括定义实体类和Repository接口,通过Repository接口可直接进行数据操作。此外,JPA支持关键字查询,如通过`findByAuthor`自动转换为SQL的`WHERE author=?`查询。
|
4天前
|
缓存 Java 开发者
10个点介绍SpringBoot3工作流程与核心组件源码解析
Spring Boot 是Java开发中100%会使用到的框架,开发者不仅要熟练使用,对其中的核心源码也要了解,正所谓知其然知其所以然,V 哥建议小伙伴们在学习的过程中,一定要去研读一下源码,这有助于你在开发中游刃有余。欢迎一起交流学习心得,一起成长。