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底层原理,直接一拳全打通),可以留言,人多了会更,反正这一篇文章是写不了了,作者写到这里已经快被卡死了,贴一下图。

相关推荐
捂月8 分钟前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn34 分钟前
依赖注入注解
java·后端·spring
Estar.Lee1 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪1 小时前
Django:从入门到精通
后端·python·django
一个小坑货1 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet271 小时前
【Rust练习】22.HashMap
开发语言·后端·rust
uhakadotcom1 小时前
如何实现一个基于CLI终端的AI 聊天机器人?
后端
Iced_Sheep2 小时前
干掉 if else 之策略模式
后端·设计模式
XINGTECODE2 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶2 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露