Springboot3核心原理

前言

本篇由Springboot3全篇学习笔记因内容篇幅过大分切出来。

会讲什么?

  1. Springboot的生命周期
  2. 监听器机制
  3. 探针
  4. 基于事件开发
  5. 自定义Starter

关于Springboot的装配原理 ,在Springboot基础中已经讲的非常明白了,大家可以去我写的Springboot3全篇学习笔记里面看


监听器

什么是监听器?

可以简单理解为AOP思想(面向切面编程 )对Springboot本身的实现。也就是说,将Springboot的启动到销毁的整个流程当做一个切面 ,在其生命周期每一步都搞一个通知方法 ,在项目启动时就会触发定义的通知方法 ,而这个我们定义的这个包含通知方法的切面类,就是监听器


springboot自己的监听器是怎么定义的?

我们找到所导入的依赖中名为:spring-boot的jar包,在META-INF下面有一个名为sping.factories的文件

其中的一段代码

ini 复制代码
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener

我们可以看到,在这里,springboot自己就定义了一大堆的监听器

从这里我们可以知道的是,springboot的listener在定义时并不是简单的写一段代码就行了 ,而是要在META-INF下面的sping.factories文件中指定。


如何写自己的监听器?

  1. 我们在创建一个名为MyListener的类,实现一个名为SpringApplicationRunListener的接口,然后点击类名ctrl+o 实现该接口的方法(从startingfailed)。
java 复制代码
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

import java.time.Duration;

public class MyListener implements SpringApplicationRunListener {
    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        SpringApplicationRunListener.super.starting(bootstrapContext);
    }

    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        SpringApplicationRunListener.super.environmentPrepared(bootstrapContext, environment);
    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        SpringApplicationRunListener.super.contextPrepared(context);
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        SpringApplicationRunListener.super.contextLoaded(context);
    }

    @Override
    public void started(ConfigurableApplicationContext context, Duration timeTaken) {
        SpringApplicationRunListener.super.started(context, timeTaken);
    }

    @Override
    public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
        SpringApplicationRunListener.super.ready(context, timeTaken);
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        SpringApplicationRunListener.super.failed(context, exception);
    }
}

提一点:关于为什么要实现这个接口,也就是说为什么用这个监听器而非其他的监听器呢?因为这个监听器是最强大全面的监听器,它监听springboot的全流程,且能够进行操作

  1. 在项目resources文件夹下创建META-INF文件夹,并创建spring.factories文件。并指定好自己刚写的监听器
ini 复制代码
org.springframework.boot.SpringApplicationRunListener=监听器地址

注意使用.隔开地址而非/,下面是我的地址,作为格式参考

ini 复制代码
org.springframework.boot.SpringApplicationRunListener=com.atguigu.pro02.config.MyListener

springboot全生命周期(重点)

下面我会通过对上面监听器的每一个切入点的触发时机的详细讲解,深入的帮助大家理解springboot的生命周期。

starting的触发时机与项目引导阶段

java 复制代码
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
    SpringApplicationRunListener.super.starting(bootstrapContext);
}

在这里,汤师爷给大家翻译翻译什么叫starting,所谓starting就是启动,可以理解为启动阶段,其实这样讲比较笼统,更准确的说法我们需要翻译翻译这个传入的参数:

ConfigurableBootstrapContext--->可配置的引导上下文

也就是说我们通过这个参数,可以配置引导的内容,

什么是引导?

我们都知道,springboot的核心是IOC容器,那么谁来创建最初的容器

就是这个引导


源码解析

我们启动项目时都是运行了主启动方法,如果我们想要看项目的启动流程,当然也要从主启动类的run方法入手

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

我们按住ctrl点击run,然后会发现方法内调用了另一个run方法,再次点击还是执行了别的run,我们继续往里面点

(注意:点进源码后如果ide右上角提示要下载源码,点击下载才能继续找)

截图中的右上角(我的下载过之后就会变成Reader More):

最终会找到一个返回ConfigurableApplicationContext的run方法

typescript 复制代码
public ConfigurableApplicationContext run(String... args) {

这个方法就是springboot的生命周期执行流程 我们截取一部分源码:

ini 复制代码
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);

这里创建了一个bootstrapContext引导 ,然后定义了一个ConfigurableApplicationContext可配置容器,但是还没有创建它,也就是在容器初始化之前,getRunListeners(args);获取监听器,我们点开看一下,它怎么获取的

ini 复制代码
private SpringApplicationRunListeners getRunListeners(String[] args) {
    ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this);
    argumentResolver = argumentResolver.and(String[].class, args);
    List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class,
          argumentResolver);
    SpringApplicationHook hook = applicationHook.get();
    SpringApplicationRunListener hookListener = (hook != null) ? hook.getRunListener(this) : null;
    if (hookListener != null) {
       listeners = new ArrayList<>(listeners);
       listeners.add(hookListener);
    }
    return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup);
}

前面的参数处理我们不关心,看下面这句,因为它返回了一个SpringApplicationRunListenerList,一定是它去找了我们定义的监听器

ini 复制代码
List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class,
       argumentResolver);

找这个getSpringFactoriesInstances方法

typescript 复制代码
private <T> List<T> getSpringFactoriesInstances(Class<T> type, ArgumentResolver argumentResolver) {
    return SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader()).load(type, argumentResolver);
}

翻译翻译: forDefaultResourceLocation,从默认的资源地址,什么叫默认的资源地址?点进去

less 复制代码
public static SpringFactoriesLoader forDefaultResourceLocation(@Nullable ClassLoader classLoader) {
    return forResourceLocation("META-INF/spring.factories", classLoader);
}

哦,原来是META-INF/spring.factories呀,多谢黄老爷。


接着看最后一句源码

kotlin 复制代码
listeners.starting(bootstrapContext, this.mainApplicationClass)

这不就是我们的监听器的第一个启动中方法嘛。

破案了,starting方法在容器启动之前被执行,会将引导参数传进来。


environmentPrepared的触发时机

这一步是在springboot环境准备之后执行的,我们再截取一段新的源码

ini 复制代码
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);

这一步明显是准备环境的,我们点进去看看

scss 复制代码
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
       DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach(environment);
    //执行监听器
    listeners.environmentPrepared(bootstrapContext, environment);
    DefaultPropertiesPropertySource.moveToEnd(environment);
    Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
          "Environment prefix cannot be set via properties.");
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
       EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
       environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}

果然,在这一步源码中就有一句执行了我们的environmentPrepared方法。在此之前,它创建并配置了Springboot启动所需要的环境。在准备好之后执行了该方法,并将环境参数传了进来

通过这个environment参数,我们能获取非常多的系统参数。如果我们希望给用户定制的springboot程序做定期收费,我们就可以通过参数来配置程序是否可运行


contextPrepared的触发时机

翻译翻译:上下文准备完成,要理解这句话,我们需要看源码

ini 复制代码
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

上面的代码中,终于创建了容器,但我们contextPrepared的执行位置其实是在prepareContext方法中,我们点开它

截取部分源码:

scss 复制代码
//配置容器环境
context.setEnvironment(environment);
//做前置处理
postProcessApplicationContext(context);
addAotGeneratedInitializerIfNecessary(this.initializers);
//初始化
applyInitializers(context);
//执行监听器中的contextPrepared
listeners.contextPrepared(context);
//关闭启动引导
bootstrapContext.close(context);

在这一步中,它在环境配置好以及做好容器前置处理后,执行了我们的上下文准备完成启动引导功成身退,最后关闭,接下来容器来管事情了。

下面是关于容器配置的一些处理,包括解决循环依赖执行Spring容器的生命周期函数加载容器内bean的资源配置资源加载,如果大家精通Spring源码,自然能看懂,这里不多赘述。

scss 复制代码
if (this.logStartupInfo) {
    logStartupInfo(context.getParent() == null);
    logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
    beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
    autowireCapableBeanFactory.setAllowCircularReferences(this.allowCircularReferences);
    if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
       listableBeanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
    }
}
if (this.lazyInitialization) {
    context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
if (!AotDetector.useGeneratedArtifacts()) {
    // Load the sources
    Set<Object> sources = getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    load(context, sources.toArray(new Object[0]));
}

contextLoaded的触发时机

在上面的源码结尾,会执行该方法 ,表示容器加载完成,但容器此时并未刷新 ,也就是说,bean对象并未实际创建


started的触发时机

紧接上句源码结束,Spring就刷新了容器,下面的源码告诉我们,started在容器已经创建完成,并且bean对象已经全部创建后执行,其实到这一步,Springboot已经算正式可用了。

scss 复制代码
//刷新容器
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
    new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
//执行监听器的started方法
listeners.started(context, timeTakenToStartup);

failed的触发时机

这个其实是Springboot启动失败时才会执行的监听步骤 看源码,在源码的catch中

php 复制代码
catch (Throwable ex) {
    if (ex instanceof AbandonedRunException) {
       throw ex;
    }
    handleRunFailure(context, ex, listeners);
    throw new IllegalStateException(ex);
}

每一个错误中都会执行handleRunFailure(context, ex, listeners);方法,在这个方法中,会执行我们的failed方法

scss 复制代码
private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
       SpringApplicationRunListeners listeners) {
    try {
       try {
          handleExitCode(context, exception);
          if (listeners != null) {
          //执行监听器中的启动失败监听
             listeners.failed(context, exception);
          }
       }
       finally {
          reportFailure(getExceptionReporters(context), exception);
          if (context != null) {
             context.close();
             shutdownHook.deregisterFailedApplicationContext(context);
          }
       }
    }
    catch (Exception ex) {
       logger.warn("Unable to close ApplicationContext", ex);
    }
    ReflectionUtils.rethrowRuntimeException(exception);
}

ready的触发时机

顾名思义:ready就是准备好了的意思。这段代码其实是Springboot内部会有一个自检的步骤,确认启动完毕后,会执行该代码,表示Springboot已经成功启动完成了,下面是源码

ini 复制代码
try {
    if (context.isRunning()) {
       Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
       listeners.ready(context, timeTakenToReady);
    }
}

SpringApplicationRunListener监听器总结

最后贴上Springboot中ApplicationRunListener监听器的全流程图


生命周期中其他可插入的点与事件触发机制

为什么要把这些切面专门分出来讲?

因为所谓的事件触发机制 本质上也是通过这些点知Springboot生命周期来工作的,如果不专门提这些其余的可插入的点,就无法正确的看待事件触发本身。

Springboot中的其他可插入的点

大家先看一段源码:

scss 复制代码
this.bootstrapRegistryInitializers = new ArrayList<>(
       getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

1. BootstrapRegistryInitializer

上面这段代码其实是从SpringApplication(上文中核心源码的所在类)的构造器 中截取的,也就是说,这这包含了BootstrapRegistryInitializer.class实现类的bootstrapRegistryInitializers(引导注册初始化),本质是Arraylist,在最开始就已经被赋值了!

那么这个引导在哪一步被执行了呢?

ini 复制代码
public ConfigurableApplicationContext run(String... args) {
    if (this.registerShutdownHook) {
       SpringApplication.shutdownHook.enableShutdowHookAddition();
    }
    long startTime = System.nanoTime();
    //注意这个create方法
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);

可以看到,我们上一章中讲到的SpringApplicationRunListeners还在排在后面,前面有一堆代码,其中有一个createBootstrapContext();下面是他的源码

csharp 复制代码
private DefaultBootstrapContext createBootstrapContext() {
    DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
    this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
    return bootstrapContext;
}

initialize,翻译翻译:初始化

也就是说,这个可插入的点早在引导被初始化时,就已经被执行了远远早于ioc的创建与其他可插入的点 ,该类完成了对引导的初始化工作,但是它接收的是一个List,也就是说,Spring自己实现了这个接口,利用它完成了初始化 。但是我们也可以通过自己实现这个类,在引导初始化阶段完成自己的工作


ApplicationListener总结

好了,我们找出了除SpringApplicationRunListeners以外第一个可插入的点BootstrapRegistryInitializer.class,他只有一个方法,而且执行的很早,可以拿它来做什么呢?

我们可以用它来做初始化秘钥的校验,假设你想设计一个收费的项目,如果客户的项目过期,则通过这一步校验来阻止项目的启动

2. ApplicationContextInitializer

分析第二句:

scss 复制代码
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

这又是一个可插入的点,我们一样可以通过实现它来完成对生命周期的感知与操作 那么这个setInitializers到底做了什么呢?

typescript 复制代码
public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) {
    this.initializers = new ArrayList<>(initializers);
}

就是给属性initializers赋值,把接收到的合集中的参数放进去。

那么它在哪里执行了呢?

让我们回到主生命周期流程代码中: 既然这个东西的名字是ApplicationContextInitializer容器初始化,那么它必然在容器的初始化附近,

ini 复制代码
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);

他藏在容器的前置处理prepareContext(....);方法当中:

scss 复制代码
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
       ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
       ApplicationArguments applicationArguments, Banner printedBanner) {
    context.setEnvironment(environment);
    postProcessApplicationContext(context);
    addAotGeneratedInitializerIfNecessary(this.initializers);
    applyInitializers(context);
    listeners.contextPrepared(context);
    bootstrapContext.close(context);

这里有一个applyInitializers(context);方法,在这个里面,点开源码:

scss 复制代码
protected void applyInitializers(ConfigurableApplicationContext context) {
    for (ApplicationContextInitializer initializer : getInitializers()) {
       Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
             ApplicationContextInitializer.class);
       Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
       initializer.initialize(context);
    }
}

上面的代码中循环遍历了initializer,并最终使用了他的initialize方法。到这里我们知道了它的执行位置。


ApplicationContextInitializer总结

让我们回到整个生命周期代码中,来看一下它执行位置在哪里

它在IOC容器创建之后,刷新之前 的前置处理中,正好在全生命周期监听器SpringApplicationRunListenercontextPrepared()方法的上一步。

其实说实话,它的位置导致了它的功能很尴尬,与上面的contextPrepared()几乎是一样的,因为他们的参数也一样。

但是需要说的是,虽然功能几乎相同,但是我们最初的全生命周期监听器SpringApplicationRunListener(下面我会称它为大监听器),的定义方法必须是实现相应接口并在指定的配置文件中定义。

ApplicationContextInitializer的定义就要简单的多,我们可以直接在主启动类的主对象中添加它(搞一个函数式接口的实现类)

typescript 复制代码
@SpringBootApplication()
//@EnableConfigurationProperties(pig.class)
@MapperScan({"com.atguigu.pro02.mapper"})
public class Pro02Application {


    public static void main(String[] args) {
       SpringApplication springApplication = new SpringApplication(Pro02Application.class);
       springApplication.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
          @Override
          public void initialize(ConfigurableApplicationContext applicationContext) {
             System.out.println("源码分析不是有手就行吗?");
          }
       });
       springApplication.run(args);

    }

上面的代码其实可以优化,这里为了大家看的清晰,就不搞了。

其实对于上面的那个初始化引导,也可以直接这么搞, 主启动类有一个addBootstrapRegistryInitializer添加引导初始化的方法:

ini 复制代码
springApplication.addBootstrapRegistryInitializer();

直接在主启动类里面就能添加

3. 两种Runner

其实上面还有一个ApplicationListener,但我们暂时不讲,他里面牵涉到一个事件机制和探针。我们先来看2种Runner:

这一小节我们加快速度,我先讲他们的触发时机 ,然后告诉大家怎么用

我们回到主生命周期源码

scss 复制代码
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
    new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
//注意callRunner
callRunners(context, applicationArguments);

我们可以看到,在容器启动完成后调用了callRunners (找到触发时机),我们打开他的源码

scss 复制代码
private void callRunners(ApplicationContext context, ApplicationArguments args) {
    context.getBeanProvider(Runner.class).orderedStream().forEach((runner) -> {
       if (runner instanceof ApplicationRunner applicationRunner) {
          callRunner(applicationRunner, args);
       }
       if (runner instanceof CommandLineRunner commandLineRunner) {
          callRunner(commandLineRunner, args);
       }
    });
}

这里根据类型Runner.class从ioc中取出来对象,跑了一个for循环,判断是不是ApplicationRunner或者CommandLineRunner类的实例,是的话就执行。

好的,我们既然知道了他是从容器中取出来的,那我们直接在容器里面加这两种Runner就行了:

java 复制代码
@Bean
public ApplicationRunner myApplicationRunner(){
    return new ApplicationRunner() {
       @Override
       public void run(ApplicationArguments args) throws Exception {
          System.out.println("我的ApplicationRunner,哈哈哈哈~~~~");
       }
    };
}

@Bean
public CommandLineRunner myCommandLineRunner(){
    return new CommandLineRunner() {
       @Override
       public void run(String... args) throws Exception {
          System.out.println("我的CommandLineRunner,哈哈哈哈~~~~");
       }
    };
}

小总结

  1. 如果我们想在项目刚启动时就做事,我们可以使用:BootstrapRegistryInitializer

  2. 如果我们想在容器创建之后但内部还没有任何东西时做事,我们可以使用:ApplicationContextInitializer

  3. 如果我们想在容器成功启动后做事:可以使用2种Runner


事件触发机制与探针

我们来讲这个极为关键的最后一个Listener:ApplicationListener,也是初始化时插入的最后的切面

scss 复制代码
getSpringFactoriesInstances(ApplicationListener.class));

我们尝试实现一下它,这个东西要求实现一个泛型ApplicationEvent(应用程序事件 ),内部会有一个方法:onApplicationEvent

这个方法会在有事件触发时自动被调用(这句话很关键)

csharp 复制代码
public class MyApplicationListener implements ApplicationListener<ApplicationEvent> {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println("==========="+event+"事件触发==============");
    }
}

spring.factories里面也要配一下

ini 复制代码
org.springframework.context.ApplicationListener=自己写的类的地址,(com开头)

跑起来看一下,都触发了什么 这里大家自己去跑一下,我这边直接说结果

它在每一个大监听器(ApplicationRunListener)通知的前面都有一个通知,而且还多了几个通知

  1. 在Servlet容器准备完成后多发了一个通知
  2. 他在大监听器的started方法之前除了自己的通知以外,还多了一个 AvailabilityChangeEvent通知
  3. 在大监听器的ready方法之前除了自己的通知以外,也还多了一个 AvailabilityChangeEvent通知

探针

ini 复制代码
===========org.springframework.boot.context.event.ApplicationStartedEvent[source=org.springframework.boot.SpringApplication@55562aa9]事件触发==============
===========org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@7728643a, started on Thu Nov 09 13:04:56 CST 2023]事件触发==============
=============started ====== 
我的ApplicationRunner,哈哈哈哈~~~~
我的CommandLineRunner,哈哈哈哈~~~~
===========org.springframework.boot.context.event.ApplicationReadyEvent[source=org.springframework.boot.SpringApplication@55562aa9]事件触发==============
===========org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@7728643a, started on Thu Nov 09 13:04:56 CST 2023]事件触发==============
=============ready ====== 

对于上面的Servlet容器准备完成后多的那个通知我们不管,它爱通知就通知,也无所谓。重点在于这两个AvailabilityChangeEvent的通知需要专门讲一下,他是为了K8s预留的通知,告诉外界,该应用程序处于就绪状态,如果第一个started的通知出去而第二个ready之前的通知没出去,就表示我们的springboot项目可能有问题,只有2个都通知了,才能说明程序正常启动,而这两个东西,就是我们所说的探针

那么这2个探针到底是在源码的哪一步发出的呢?我们回到主源码

ini 复制代码
listeners.started(context, timeTakenToStartup);

他在大监听器的started里面触发的

scss 复制代码
void started(ConfigurableApplicationContext context, Duration timeTaken) {
    doWithListeners("spring.boot.application.started", (listener) -> listener.started(context, timeTaken));
}

他这里还遍历执行了一个started 方法,我们使用ctrl+alt+b点击看这个方法,选择EventPublishingRunListener.java的实现

typescript 复制代码
@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
    context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context, timeTaken));
    AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
}

我们可以看到他发布了2个事件。我们看上面代码紧接着的下一串代码,

typescript 复制代码
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
    context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, timeTaken));
    AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
}

不用想,这肯定也就是ready方法底层。


事件驱动开发

设想一个场景,一个用户登录了,我们需要给用户做以下3个功能

  1. 增加1点累计登录积分
  2. 还需要给用户发一张优惠券,
  3. 还需要记录用户登录后的信息状态

我们尝试实现一下这个功能

常规实现

我们需要一个登录的Controller和3个Service

less 复制代码
@RestController
@RequestMapping("login")
public class loginController {

    @Autowired
    sysService sysService;

    @Autowired
    accountService accountService;

    @Autowired
    couponService couponService;

    @GetMapping("/{userName}/{password}")
    public void getMapping(@PathVariable String userName,@PathVariable String password){
        sysService.Login(userName,password);
        accountService.Login(userName);
        couponService.Login(userName);
    }
}

下面是3个Service

arduino 复制代码
@Service
public class sysService {
    private Logger logger = LoggerFactory.getLogger(sysService.class);
    public void Login(String userName ,String password){
        logger.info("用户:{}登录成功,密码为:{}",userName,password);
    }
}

@Service
public class accountService {
    private Logger logger = LoggerFactory.getLogger(accountService.class);
    public void Login(String userName){
        logger.info("用户:{}登录成功,积分+1",userName);
    }
}


@Service
public class couponService {
    private Logger logger = LoggerFactory.getLogger(couponService.class);
    public void Login(String userName){
        logger.info("用户:{}登录成功,优惠券下发一张",userName);
    }
}

常规实现的缺点

在这里我们就会发现一个很严重的问题,就是我们需要引入大量的Service,如果登录相关的功能再添加,就会越来越复杂,而且代码之间的耦合也会越来越高


基于事件开发的思路

我们尝试将登录看做一个事件 而非一串动作,我们把登录这件事发布出去,然后让对这件事关心的人做出反应。这是一种很高明的模式,减轻了发布者与响应者之间的耦合。

要完成事件的发布与接收,我们需要以下几个对象

  1. 可以被发布的事件本身

  2. 能发送事件的工具

  3. 能接收事件的对象

基于事件开发实操

我们先来创建一个可以被发送的事件本身(登陆成功事件 ),他需要继承ApplicationEvent并实现方法。

scala 复制代码
//TODO  这里不需要放入ioc,下面的发布者会用到它,会自动放进去
public class LoginSuccessEvent extends ApplicationEvent {
    
    //TODO 这里会接收一个资源,传什么大家自己定
    public LoginSuccessEvent(Object source) {
        super(source);
        User user = (User)source;
        System.out.println( user.getUserName()+"登录啦~~");
    }
    
}

然后我们创建一个发送任何事件的工具,需要实现ApplicationEventPublisherAware接口

typescript 复制代码
@Component
public class EventPublisher implements ApplicationEventPublisherAware {

    //TODO 定义一个发布者,类型与接口要实现的方法参数的类型要一致
    ApplicationEventPublisher publisher;

    //TODO 定义一个名为:发布 的方法,借助Springboot给我们赋值的这个发布者,把事件发布出去
    public void publish(ApplicationEvent event){
        publisher.publishEvent(event);
    }

    //TODO  把接口实现方法传进来的这个发布者赋值给我们自己的发布者
    //TODO  这个方法的参数在Springboot启动时会自动传进来,我们不用自己传
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher=applicationEventPublisher;
    }
}

我们现在来创建一个对登陆成功事件感兴趣的人,它同样需要实现一个接口:ApplicationListener,同时需要定义泛型作为监听的事件类型

java 复制代码
@Service
public class accountService implements ApplicationListener<LoginSuccessEvent> {
    private Logger logger = LoggerFactory.getLogger(accountService.class);

    @Override
    public void onApplicationEvent(LoginSuccessEvent event) {
        User user = (User)event.getSource();
        logger.info("用户:{}登录成功,积分+1",user.getUserName());
    }
}

我们还有另一种写法,直接在方法上添加@EventListener注解,并将方法的参数改为指定的事件

java 复制代码
@Service
public class couponService {
    private Logger logger = LoggerFactory.getLogger(couponService.class);
    //TODO定义事件触发优先级
    @Order(1)
    @EventListener
    public void onLoginSuccess(LoginSuccessEvent event){
    //TODO 抓取资源并强转为User类型
        User user = (User) event.getSource();
        logger.info("用户:{}登录成功,优惠券下发一张",user);
    }
}

最后,这个User对象也贴一下吧

less 复制代码
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
    private String userName;
    private String password;
}

Controller层的写法

less 复制代码
//TODO 把发布者注入进来
@Autowired
EventPublisher eventPublisher;

@GetMapping("/{userName}/{password}")
public void getMapping(@PathVariable String userName,@PathVariable String password){
    //TODO 创建事件所需对象
    User user = new User(userName,password);
    //TODO 传入user对象当做source构建出时间对象
    LoginSuccessEvent loginSuccessEvent = new LoginSuccessEvent(user);
    //TODO 把事件对象发布出去
    eventPublisher.publish(loginSuccessEvent);
}

跑一下看看

less 复制代码
jack登录啦~~
===========com.atguigu.pro02.event.LoginSuccessEvent[source=User(userName=jack, password=10010)]事件触发==============
2023-11-09 16:08:52 578 [http-nio-9000-exec-1] INFO com.atguigu.pro02.service.accountService - 用户:jack登录成功,积分+1
2023-11-09 16:08:52 578 [http-nio-9000-exec-1] INFO com.atguigu.pro02.service.couponService - 用户:jack登录成功,优惠券下发一张
2023-11-09 16:08:52 578 [http-nio-9000-exec-1] INFO com.atguigu.pro02.service.sysService - 用户:jack登录成功,密码为:10010
===========ServletRequestHandledEvent: url=[/login/jack/10010]; client=[0:0:0:0:0:0:0:1]; method=[GET]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[31ms]; status=[OK]事件触发==============

关于事件被触发的先后顺序:

我们可以使用@Order注解来定义,数字越小越靠前 ,但是要注意,这只在同一种写法下生效!继承接口写法 默认要比@EventListener注解慢触发


自定义Starter

如何将自己的代码整合成一个Starter?

我们现在计划创造一个名为robot的机器人Starter ,当我们引入该Starter时,会自动增加一个Controller,访问/robot/hello,会返回一个hello语句

我们先以一个正常web服务的方式完成它:

创建一个项目,删除src,然后新建module(一定要是这种结构)

首先我们肯定要用到Web服务 ,所以我们需要导入web场景,可能涉及到Bean的创建,我们再引入lomback

less 复制代码
@RestController
@RequestMapping("robot")
public class robotController {

    @Autowired
    RobotService robotService;

    @GetMapping("hello")
    public String sayHello(){

        return robotService.sayHiService();
    }
}

上面是一段很普通的Controller层代码

typescript 复制代码
@Service
public class RobotService {
    @Autowired
    User user;
    public String sayHiService(){
     return user.getUname()+",你好!您的编号是:"+user.getUserId();
    };
}

Service层似乎也没什么不同,重点在于这个名为user的Bean,我们希望他与配置文件绑定,这样的话,我们就可以通过配置文件来控制Controller层返回的语句了 ,我们定义他的前缀为robot

less 复制代码
@ConfigurationProperties(prefix = "robot")
@Component
@Data
public class User {
    private  String uname;
    private  String userId;
}

到目前为止,这个项目正常运行是没有问题的,但是如何让它变成了个Starter呢

首先我们要删除掉运行项目的主方法,然后我们新建另一个module,目的是为了测试它,

同样需要web场景,创建加上web的场景启动器

因为我们不需要用到数据源,我们在主启动类上排除数据源,否则会报错

python 复制代码
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

然后在pom.xml文件中引入我们的机器人模块。

正常来讲我们的项目这样就可以了,因为我们引入了机器人模块,所以机器人的所有类我们都可以使用,就像jar包一样 。但此时有一个问题,就是我们并没有将机器人的类创建对象并放入测试项目的IOC当中 ,也就是说,当我们的测试项目跑起来时,容器内部并不会有robot项目的bean对象,因为我们的测试项目默认只会扫描自己主程序下面的包,并不会把机器人的包扫进IOC

其实我们可以使用一个@Import注解,这个注解会把指定的类扫进IOC中。但是我们总不能在测试类上加一个@Imoprt(),把所有的类都填进去吧,这样使用我们Starter的人还不累死。

所以我们分两步完成,在我们的机器人模块内我们新建一个RobotAutoConfiguration

less 复制代码
@Import({RobotService.class, User.class, robotController.class})
@Configuration
public class RobotAutoConfiguration {
}

这样的话,我们只需要在测试的模块上引入这个RobotAutoConfiguration类,就相当于把这些类全部引入了!下面是我们测试类的主方法

less 复制代码
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@Import(RobotAutoConfiguration.class)
public class TestApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

}

这样就达成了我们目的,使用者只需要一个@Import(RobotAutoConfiguration.class)就能正常使用我们的Starter了。

另外提一个小问题,当我们使用Springboot自己的配置文件去配置相关信息时,都会有相关的提示 ,我们的却没有,这是因为他们都在Starter中引入了一个依赖,我们也在我们的Robot的Starter里面引入这个依赖,就会有提示了

xml 复制代码
<!--        导入配置处理器,配置文件自定义的properties配置都会有提示-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

注解方式完成自定义Starter

想象一下,作为一个用户,我并不知道需要引入什么类才能让Starter正常使用,我们可不可以只使用一个注解,就完成Robot的自动装配呢?

我们先看看Springboot自己是怎么做的

less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

上面是@EnableWebMvc的注解源码,前面三个子注解,定义了这个主机放在哪个位置,而第四个注解定义了这个注解是干什么的。

我们尝试自己搞一个注解,就叫@EnableRobot,我们在机器人项目下面新建一个annotation包,新建这个注解,引入我们的RobotAutoConfiguration.class

less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import(RobotAutoConfiguration.class)
public @interface EnableRobot {
}

搞一下试试(因为我们的User对象要在配置文件里面定义,大家定义一下再运行)

less 复制代码
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableRobot
public class TestApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

}

如何全自动注入?

我们的Web场景为什么不需要任何注解就能运行呢?学过Springboot基础的都知道,这是因为Springboot自己的相关文件里面定义了这些类,当Springboot运行时会自动扫描这些文件,自己去装配

那我们直接定义一个跟Springboot自己的配置文件相同路径的文件,把我们的RobotAutoConfiguration搞进去,让Springboot帮我们配置不就好了吗?

我们在Robot模块的Resources文件夹下新建META-INF文件夹,然后新建spring文件夹,最后新建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,注意,这些目录一定要一级一级的去创建,不能一口气创建多层!!! 在后在文件中写入我们的RobotAutoConfiguration的全类名就行了 ,下面是我自己的,大家根据自己的情况更改

arduino 复制代码
com.atguigu.robot.config.RobotAutoConfiguration

到这里就算讲完了上面承诺的几条Springboot核心原理,如果大家想看Springboot的自动装配原理加强版(包含Spring底层原理,直接一拳全打通),可以留言,人多了会更,反正这一篇文章是写不了了,作者写到这里已经快被卡死了,贴一下图。

相关推荐
摇滚侠3 小时前
Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
spring boot·笔记·后端
程序员小凯5 小时前
Spring Boot测试框架详解
java·spring boot·后端
你的人类朋友6 小时前
什么是断言?
前端·后端·安全
程序员小凯7 小时前
Spring Boot缓存机制详解
spring boot·后端·缓存
i学长的猫7 小时前
Ruby on Rails 从0 开始入门到进阶到高级 - 10分钟速通版
后端·ruby on rails·ruby
用户21411832636028 小时前
别再为 Claude 付费!Codex + 免费模型 + cc-switch,多场景 AI 编程全搞定
后端
茯苓gao8 小时前
Django网站开发记录(一)配置Mniconda,Python虚拟环境,配置Django
后端·python·django
Cherry Zack8 小时前
Django视图进阶:快捷函数、装饰器与请求响应
后端·python·django
爱读源码的大都督9 小时前
为什么有了HTTP,还需要gPRC?
java·后端·架构
码事漫谈9 小时前
致软件新手的第一个项目指南:阶段、文档与破局之道
后端