前言
本篇由Springboot3全篇学习笔记因内容篇幅过大分切出来。
会讲什么?
- Springboot的生命周期
- 监听器机制
- 探针
- 依据事情开发
- 自界说Starter
关于Springboot的安装原理,在Springboot根底中现已讲的十分明白了,咱们能够去我写的Springboot3全篇学习笔记里边看
监听器
什么是监听器?
能够简略了解为AOP思维(面向切面编程)对Springboot自身的完结。也便是说,将Springboot的发动到销毁的整个流程当做一个切面,在其生命周期每一步都搞一个告知办法,在项目发动时就会触发界说的告知办法,而这个咱们界说的这个包括告知办法的切面类,便是监听器。
springboot自己的监听器是怎样界说的?
咱们找到所导入的依靠中名为:spring-boot的jar包,在META-INF
下面有一个名为sping.factories
的文件
其间的一段代码
# 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
)。
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
文件。并指定好自己刚写的监听器
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):
终究会找到一个回来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);
}
前面的参数处理咱们不关心,看下面这句,由于它回来了一个SpringApplicationRunListener
的List
,必定是它去找了咱们界说的监听器
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监听器的全流程图
生命周期中其他可刺进的点与事情触发机制
为什么要把这些切面专门分出来讲?
由于所谓的事情触发机制实质上也是经过这些点知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容器创立之后,改写之前的前置处理中,正好在全生命周期监听器SpringApplicationRunListener
的contextPrepared()
办法的上一步。
其实说实话,它的方位导致了它的功用很为难,与上面的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,哈哈哈哈~~~~");
}
};
}
小总结
-
假如咱们想在项目刚发动时就干事,咱们能够运用:
BootstrapRegistryInitializer
-
假如咱们想在容器创立之后但内部还没有任何东西时干事,咱们能够运用:
ApplicationContextInitializer
-
假如咱们想在容器成功发动后干事:能够运用
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最初)
跑起来看一下,都触发了什么 这儿咱们自己去跑一下,我这边直接说结果
它在每一个大监听器(ApplicationRunListener
)告知的前面都有一个告知,并且还多了几个告知
- 在Servlet容器预备完结后多发了一个告知
- 他在大监听器的
started
办法之前除了自己的告知以外,还多了一个AvailabilityChangeEvent
告知 - 在大监听器的
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点累计登录积分
- 还需求给用户发一张优惠券,
- 还需求记载用户登录后的信息状况
咱们测验完结一下这个功用
常规完结
咱们需求一个登录的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,假如登录相关的功用再增加,就会越来越杂乱,并且代码之间的耦合也会越来越高。
依据事情开发的思路
咱们测验将登录看做一个事情而非一串动作,咱们把登录这件事发布出去,然后让对这件事关心的人做出反响。这是一种很高明的模式,减轻了发布者与响应者之间的耦合。
要完结事情的发布与接纳,咱们需求以下几个目标
-
能够被发布的事情自身
-
能发送事情的东西
-
能接纳事情的目标
依据事情开发实操
咱们先来创立一个能够被发送的事情自身(登陆成功事情),他需求承继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底层原理,直接一拳全打通),能够留言,人多了会更,横竖这一篇文章是写不了了,作者写到这儿现已快被卡死了,贴一下图。