前言
本篇由Springboot3全篇学习笔记因内容篇幅过大分切出来。
会讲什么?
- Springboot的生命周期
- 监听器机制
- 探针
- 基于事件开发
- 自定义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
文件中指定。
如何写自己的监听器?
- 我们在创建一个名为MyListener的类,实现一个名为
SpringApplicationRunListener
的接口,然后点击类名ctrl+o 实现该接口的方法(从starting
到failed
)。
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的全流程,且能够进行操作。
- 在项目
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);
}
前面的参数处理我们不关心,看下面这句,因为它返回了一个SpringApplicationRunListener
的List
,一定是它去找了我们定义的监听器
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容器创建之后,刷新之前 的前置处理中,正好在全生命周期监听器SpringApplicationRunListener
的contextPrepared()
方法的上一步。
其实说实话,它的位置导致了它的功能很尴尬,与上面的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,哈哈哈哈~~~~");
}
};
}
小总结
-
如果我们想在项目刚启动时就做事,我们可以使用:
BootstrapRegistryInitializer
-
如果我们想在容器创建之后但内部还没有任何东西时做事,我们可以使用:
ApplicationContextInitializer
-
如果我们想在容器成功启动后做事:可以使用
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
)通知的前面都有一个通知,而且还多了几个通知
- 在Servlet容器准备完成后多发了一个通知
- 他在大监听器的
started
方法之前除了自己的通知以外,还多了一个AvailabilityChangeEvent
通知 - 在大监听器的
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点累计登录积分
- 还需要给用户发一张优惠券,
- 还需要记录用户登录后的信息状态
我们尝试实现一下这个功能
常规实现
我们需要一个登录的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,如果登录相关的功能再添加,就会越来越复杂,而且代码之间的耦合也会越来越高。
基于事件开发的思路
我们尝试将登录看做一个事件 而非一串动作,我们把登录这件事发布出去,然后让对这件事关心的人做出反应。这是一种很高明的模式,减轻了发布者与响应者之间的耦合。
要完成事件的发布与接收,我们需要以下几个对象
-
可以被发布的事件本身
-
能发送事件的工具
-
能接收事件的对象
基于事件开发实操
我们先来创建一个可以被发送的事件本身(登陆成功事件 ),他需要继承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底层原理,直接一拳全打通),可以留言,人多了会更,反正这一篇文章是写不了了,作者写到这里已经快被卡死了,贴一下图。