深入理解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的核心流程了,在这一步之前的以及这一步事件中,你都可以去拉取外部配置,去修改已经读到的配置,具体是覆盖还是增加看你自己的需求。这里如果你要做注册中心,那么注意下,这是你可以拉配置的最后时机了,因为下一步就要注入了,你配置都没拉下来的话那还玩个毛线。这里之前到读取到本地配置之后你都可以拉取外部配置。总之这里结束后,容器刷新的所有条件都应该已经准备好。

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

目录
打赏
0
0
0
0
54
分享
相关文章
|
19天前
|
Spring IOC—基于注解配置和管理Bean 万字详解(通俗易懂)
Spring 第三节 IOC——基于注解配置和管理Bean 万字详解!
109 26
【SpringFramework】Spring IoC-基于XML的实现
本文主要讲解SpringFramework中IoC和DI相关概念,及基于XML的实现方式。
117 69
SpringBoot是如何简化Spring开发的,以及SpringBoot的特性以及源码分析
Spring Boot 通过简化配置、自动配置和嵌入式服务器等特性,大大简化了 Spring 应用的开发过程。它通过提供一系列 `starter` 依赖和开箱即用的默认配置,使开发者能够更专注于业务逻辑而非繁琐的配置。Spring Boot 的自动配置机制和强大的 Actuator 功能进一步提升了开发效率和应用的可维护性。通过对其源码的分析,可以更深入地理解其内部工作机制,从而更好地利用其特性进行开发。
47 6
Spring Boot 3 集成 Spring Security + JWT
本文详细介绍了如何使用Spring Boot 3和Spring Security集成JWT,实现前后端分离的安全认证概述了从入门到引入数据库,再到使用JWT的完整流程。列举了项目中用到的关键依赖,如MyBatis-Plus、Hutool等。简要提及了系统配置表、部门表、字典表等表结构。使用Hutool-jwt工具类进行JWT校验。配置忽略路径、禁用CSRF、添加JWT校验过滤器等。实现登录接口,返回token等信息。
407 12
【SpringFramework】Spring IoC-基于注解的实现
本文主要记录基于Spring注解实现IoC容器和DI相关知识。
63 21
Spring Boot 3 集成Spring AOP实现系统日志记录
本文介绍了如何在Spring Boot 3中集成Spring AOP实现系统日志记录功能。通过定义`SysLog`注解和配置相应的AOP切面,可以在方法执行前后自动记录日志信息,包括操作的开始时间、结束时间、请求参数、返回结果、异常信息等,并将这些信息保存到数据库中。此外,还使用了`ThreadLocal`变量来存储每个线程独立的日志数据,确保线程安全。文中还展示了项目实战中的部分代码片段,以及基于Spring Boot 3 + Vue 3构建的快速开发框架的简介与内置功能列表。此框架结合了当前主流技术栈,提供了用户管理、权限控制、接口文档自动生成等多项实用特性。
86 8
SpringBoot项目打包成war包
通过上述步骤,我们成功地将一个Spring Boot应用打包成WAR文件,并部署到外部的Tomcat服务器中。这种方式适用于需要与传统Servlet容器集成的场景。
21 8
Spring Boot 两种部署到服务器的方式
本文介绍了Spring Boot项目的两种部署方式:jar包和war包。Jar包方式使用内置Tomcat,只需配置JDK 1.8及以上环境,通过`nohup java -jar`命令后台运行,并开放服务器端口即可访问。War包则需将项目打包后放入外部Tomcat的webapps目录,修改启动类继承`SpringBootServletInitializer`并调整pom.xml中的打包类型为war,最后启动Tomcat访问应用。两者各有优劣,jar包更简单便捷,而war包适合传统部署场景。需要注意的是,war包部署时,内置Tomcat的端口配置不会生效。
258 17
Spring Boot 两种部署到服务器的方式
springboot自动配置原理
Spring Boot 自动配置原理:通过 `@EnableAutoConfiguration` 开启自动配置,扫描 `META-INF/spring.factories` 下的配置类,省去手动编写配置文件。使用 `@ConditionalXXX` 注解判断配置类是否生效,导入对应的 starter 后自动配置生效。通过 `@EnableConfigurationProperties` 加载配置属性,默认值与配置文件中的值结合使用。总结来说,Spring Boot 通过这些机制简化了开发配置流程,提升了开发效率。
66 17
springboot自动配置原理
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等