前言

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

会讲什么?

  1. Springboot的生命周期
  2. 监听器机制
  3. 探针
  4. 依据事情开发
  5. 自界说Starter

关于Springboot的安装原理,在Springboot根底中现已讲的十分明白了,咱们能够去我写的Springboot3全篇学习笔记里边看


监听器

什么是监听器?

能够简略了解为AOP思维(面向切面编程)对Springboot自身的完结。也便是说,将Springboot的发动到销毁的整个流程当做一个切面,在其生命周期每一步都搞一个告知办法,在项目发动时就会触发界说的告知办法,而这个咱们界说的这个包括告知办法的切面类,便是监听器


springboot自己的监听器是怎样界说的?

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

Springboot3中心原理

其间的一段代码

# 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)。
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文件。并指定好自己刚写的监听器
org.springframework.boot.SpringApplicationRunListener=监听器地址

留意运用.隔开地址而非/,下面是我的地址,作为格局参阅

org.springframework.boot.SpringApplicationRunListener=com.atguigu.pro02.config.MyListener

springboot全生命周期(要点)

下面我会经过对上面监听器的每一个切入点的触发机遇的具体解说,深化的协助咱们了解springboot的生命周期。

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

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

在这儿,汤师爷给咱们翻译翻译什么叫starting,所谓starting便是发动,能够了解为发动阶段,其实这样讲比较笼统,更准确的说法咱们需求翻译翻译这个传入的参数:

ConfigurableBootstrapContext—>可装备的引导上下文

也便是说咱们经过这个参数,能够装备引导的内容,

什么是引导?

咱们都知道,springboot的中心是IOC容器,那么谁来创立开始的容器

便是这个引导


源码解析

咱们发动项目时都是运转了主发动办法,假如咱们想要看项意图发动流程,当然也要从主发动类的run办法下手

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

咱们按住ctrl点击run,然后会发现办法内调用了另一个run办法,再次点击仍是履行了其他run,咱们持续往里边点

(留意:点进源码后假如ide右上角提示要下载源码,点击下载才干持续找)

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

Springboot3中心原理

终究会找到一个回来ConfigurableApplicationContext的run办法

public ConfigurableApplicationContext run(String... args) {

这个办法便是springboot的生命周期履行流程 咱们截取一部分源码:

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

这儿创立了一个bootstrapContext引导,然后界说了一个ConfigurableApplicationContext可装备容器,可是还没有创立它,也便是在容器初始化之前,getRunListeners(args);获取监听器,咱们点开看一下,它怎样获取的

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,必定是它去找了咱们界说的监听器

List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class,
       argumentResolver);

找这个getSpringFactoriesInstances办法

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

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

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

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


接着看终究一句源码

listeners.starting(bootstrapContext, this.mainApplicationClass)

这不便是咱们的监听器的第一个发动中办法嘛。

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


environmentPrepared的触发机遇

这一步是在springboot环境预备之后履行的,咱们再截取一段新的源码

ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);

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

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的触发机遇

翻译翻译:上下文预备完结,要了解这句话,咱们需求看源码

context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

上面的代码中,终于创立了容器,但咱们contextPrepared的履行方位其实是在prepareContext办法中,咱们点开它

截取部分源码:

//装备容器环境
context.setEnvironment(environment);
//做前置处理
postProcessApplicationContext(context);
addAotGeneratedInitializerIfNecessary(this.initializers);
//初始化
applyInitializers(context);
//履行监听器中的contextPrepared
listeners.contextPrepared(context);
//封闭发动引导
bootstrapContext.close(context);

在这一步中,它在环境装备好以及做好容器前置处理后,履行了咱们的上下文预备完结发动引导功遂身退 ,终究封闭,接下来容器来管事情了。

下面是关于容器装备的一些处理,包括处理循环依靠履行Spring容器的生命周期函数加载容器内bean的资源装备资源加载,假如咱们通晓Spring源码,天然能看懂,这儿不多赘述。

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现已算正式可用了。

//改写容器
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中

catch (Throwable ex) {
    if (ex instanceof AbandonedRunException) {
       throw ex;
    }
    handleRunFailure(context, ex, listeners);
    throw new IllegalStateException(ex);
}

每一个过错中都会履行handleRunFailure(context, ex, listeners);办法,在这个办法中,会履行咱们的failed办法

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现已成功发动完结了,下面是源码

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

SpringApplicationRunListener监听器总结

终究贴上Springboot中ApplicationRunListener监听器的全流程图

Springboot3中心原理


生命周期中其他可刺进的点与事情触发机制

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

由于所谓的事情触发机制实质上也是经过这些点知Springboot生命周期来工作的,假如不专门提这些其余的可刺进的点,就无法正确的看待事情触发自身。

Springboot中的其他可刺进的点

咱们先看一段源码:

this.bootstrapRegistryInitializers = new ArrayList<>(
       getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

1. BootstrapRegistryInitializer

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

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

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();下面是他的源码

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

剖析第二句:

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

这又是一个可刺进的点,咱们相同能够经过完结它来完结对生命周期的感知与操作 那么这个setInitializers到底做了什么呢?

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

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

那么它在哪里履行了呢?

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

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(....);办法傍边:

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);办法,在这个里边,点开源码:

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的界说就要简略的多,咱们能够直接在主发动类的主目标中增加它(搞一个函数式接口的完结类)

@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增加引导初始化的办法:

springApplication.addBootstrapRegistryInitializer();

直接在主发动类里边就能增加

3. 两种Runner

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

这一小节咱们加快速度,我先讲他们的触发机遇,然后告知咱们怎样用

咱们回到主生命周期源码

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(找到触发机遇),咱们打开他的源码

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就行了:

@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,也是初始化时刺进的终究的切面

getSpringFactoriesInstances(ApplicationListener.class));

咱们测验完结一下它,这个东西要求完结一个泛型ApplicationEvent(应用程序事情),内部会有一个办法:onApplicationEvent

这个办法会在有事情触发时主动被调用(这句话很要害)

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

spring.factories里边也要配一下

org.springframework.context.ApplicationListener=自己写的类的地址,(com最初)

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

Springboot3中心原理
它在每一个大监听器(ApplicationRunListener)告知的前面都有一个告知,并且还多了几个告知

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

探针

===========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个探针到底是在源码的哪一步发出的呢?咱们回到主源码

listeners.started(context, timeTakenToStartup);

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

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

他这儿还遍历履行了一个started办法,咱们运用ctrl+alt+b点击看这个办法,选择EventPublishingRunListener.java的完结

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

咱们能够看到他发布了2个事情。咱们看上面代码紧接着的下一串代码,


@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

@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

@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并完结办法。

//TODO  这儿不需求放入ioc,下面的发布者会用到它,会主动放进去
public class LoginSuccessEvent extends ApplicationEvent {
    //TODO 这儿会接纳一个资源,传什么咱们自己定
    public LoginSuccessEvent(Object source) {
        super(source);
        User user = (User)source;
        System.out.println( user.getUserName()+"登录啦~~");
    }
}

然后咱们创立一个发送任何事情的东西,需求完结ApplicationEventPublisherAware接口

@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,同时需求界说泛型作为监听的事情类型

@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注解,并将办法的参数改为指定的事情

@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目标也贴一下吧

@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
    private String userName;
    private String password;
}

Controller层的写法

//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);
}

跑一下看看

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

@RestController
@RequestMapping("robot")
public class robotController {
    @Autowired
    RobotService robotService;
    @GetMapping("hello")
    public String sayHello(){
        return robotService.sayHiService();
    }
}

上面是一段很一般的Controller层代码

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

Service层好像也没什么不同,要点在于这个名为user的Bean,咱们期望他与装备文件绑定,这样的话,咱们就能够经过装备文件来操控Controller层回来的语句了,咱们界说他的前缀为robot

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

到目前为止,这个项目正常运转是没有问题的,可是怎样让它变成了个Starter呢

首要咱们要删除掉运转项意图主办法,然后咱们新建另一个module,意图是为了测验它,

相同需求web场景,创立加上web的场景发动器

由于咱们不需求用到数据源,咱们在主发动类上排除数据源,否则会报错

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

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

正常来讲咱们的项目这样就能够了,由于咱们引进了机器人模块,所以机器人的一切类咱们都能够运用,就像jar包相同。但此刻有一个问题,便是咱们并没有将机器人的类创立目标并放入测验项意图IOC傍边,也便是说,当咱们的测验项目跑起来时,容器内部并不会有robot项意图bean目标,由于咱们的测验项目默许只会扫描自己主程序下面的包,并不会把机器人的包扫进IOC

其实咱们能够运用一个@Import注解,这个注解会把指定的类扫进IOC中。可是咱们总不能在测验类上加一个@Imoprt(),把一切的类都填进去吧,这样运用咱们Starter的人还不累死。

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

@Import({RobotService.class, User.class, robotController.class})
@Configuration
public class RobotAutoConfiguration {
}

这样的话,咱们只需求在测验的模块上引进这个RobotAutoConfiguration类,就相当于把这些类悉数引进了!下面是咱们测验类的主办法

@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里边引进这个依靠,就会有提示了

<!--        导入装备处理器,装备文件自界说的properties装备都会有提示-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

注解办法完结自界说Starter

幻想一下,作为一个用户,我并不知道需求引进什么类才干让Starter正常运用,咱们可不能够只运用一个注解,就完结Robot的主动安装呢?

咱们先看看Springboot自己是怎样做的

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

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

咱们测验自己搞一个注解,就叫@EnableRobot,咱们在机器人项目下面新建一个annotation包,新建这个注解,引进咱们的RobotAutoConfiguration.class

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import(RobotAutoConfiguration.class)
public @interface EnableRobot {
}

搞一下试试(由于咱们的User目标要在装备文件里边界说,咱们界说一下再运转)

@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的全类名就行了 ,下面是我自己的,咱们依据自己的状况更改

com.atguigu.robot.config.RobotAutoConfiguration

到这儿就算讲完了上面承诺的几条Springboot中心原理,假如咱们想看Springboot的主动安装原理加强版(包括Spring底层原理,直接一拳全打通),能够留言,人多了会更,横竖这一篇文章是写不了了,作者写到这儿现已快被卡死了,贴一下图。

Springboot3中心原理