本文讨论的是基于springboot的web项目,在启动时通过事件监听机制,如何对外提供扩展点,用来各业务自定义处理逻辑。此扩展点区别于Ioc容器生成时对外的扩展点,不要混淆。通过此文完全可以掌握在springboot启动时的主逻辑,且可以通过自定义事件监听器来个性化自己的业务逻辑。本文源码分析基于springboot2.6.3版本。
先从大家熟悉的SpringApplication.run(XXX.class);开始,我们先将这五个扩展点的源码处标记起来,下文会详细讲解。
理论
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// 声明一个SpringApplicationRunListeners,将给属性listeners赋值,从spring.factories中读取
// # Run Listeners
// org.springframework.boot.SpringApplicationRunListener=\
// org.springframework.boot.context.event.EventPublishingRunListener
SpringApplicationRunListeners listeners = getRunListeners(args);
// 个扩展点
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 第二个扩展点,这里为自定义配置文件提供扩展点
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 根据不同的this.webApplicationType = WebApplicationType.deduceFromClasspath();来创建不同的context
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 第三个扩展点,在context创建之初,也对应第四阶段,在context load后,此时ApplicationContext
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);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
// 第五个扩展点
listeners.ready(context, timeTakenToReady);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
上面的源码是我们熟知的springboot启动流程,springboot将此启动划分成了五大阶段,上面有标注,在讨论五大阶段前,我们先说下几个重要的接口和实现类。
SpringApplicationRunListener:这个接口,接口内部定义了在springboot启动时的几个阶段
1. starting(),对应个扩展点
2. environmentPrepared(ConfigurableEnvironment environment),对应第二个扩展点
3. contextPrepared(ConfigurableApplicationContext context),对应第三个扩展点
4. contextLoaded(ConfigurableApplicationContext context),对应第三个扩展点
5. started(ConfigurableApplicationContext context),对应第四个扩展点
6. running(ConfigurableApplicationContext context),第五个扩展点
7. failed(ConfigurableApplicationContext context, Throwable exception),这个一般是异常后处理逻辑,狭义上讲,不算业务扩展点
springboot默认有个实现类:EventPublishingRunListener,通过spring.factories
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
知道了EventPublishingRunListener的存在,那接下来就是讨论此Listener如何和Springboot启动关联起来,这里还需要一个利器:SpringApplicationRunListeners一个字母之差‘s’,可以理解SpringApplicationRunListeners是SpringApplicationRunListener的集合,大家请看结构
class SpringApplicationRunListeners {
private final Log log;
// 在run()方法时,会读取spring.factories中的所有SpringApplicationRunListener,添加到list中
private final List<SpringApplicationRunListener> listeners;
...
}
SpringApplicationRunListeners的方法和接口SpringApplicationRunListener的大体相同,springboot就是通过这个类,在上面的五大阶段分别留下了扩展点,我们拿第二个扩展点举例,看下代码如何实现
# SpringApplicationRunListeners
void environmentPrepared(ConfigurableEnvironment environment) {
for (SpringApplicationRunListener listener : this.listeners) {
// 调用所有的SpringApplicationRunListener的environmentPrepared方法
listener.environmentPrepared(environment);
}
}
# SpringApplicationRunListener - EventPublishingRunListener的实现方式
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
this.initialMulticaster
.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}
可以看到,EventPublishingRunListener中有个时间发布器,将对应的第二阶段的事件发布,这个就是读取配置文件,放到environment中
接下来我们就利用这个扩展点,看下如何在springboot读取配置文件时,如何设置hook,对配置文件进行修改
一个例子
配置spring.factories,定义一个事件
org.springframework.context.ApplicationListener=net.sy.config.listener.DatasourcePropertiesListener
声明事件监听器
public class DatasourcePropertiesListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
private static final Logger log = LoggerFactory.getLogger(DatasourcePropertiesListener.class);
public DatasourcePropertiesListener() {
}
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
ConfigurableEnvironment environment = event.getEnvironment();
// 获取所有的activeProfiles
String[] activeProfiles = environment.getActiveProfiles();
String[] var4 = activeProfiles;
int var5 = activeProfiles.length;
for(int var6 = ; var6 < var5; ++var6) {
String profile = var4[var6];
try {
Resource dynamic = null;
for(int i = ; i < PropertiesHooker.SUF_FIX.length; ++i) {
String sourceFile = "application-" + profile + PropertiesHooker.SUF_FIX[i];
dynamic = PropertiesUtil.externalConfigFile(sourceFile);
if (dynamic != null && dynamic.exists()) {
break;
}
}
if (dynamic != null && dynamic.exists()) {
// 这里就是根据不同的profile获取不同的自定义的Hooker
PropertiesHooker hooker = PropertiesHookFactory.factory(profile, environment);
// 核心逻辑,根据不同的hooker,动态增,删,改配置文件的属性值
List<Map<String, ?>> mapList = hooker.hook(dynamic);
Properties result = new Properties();
mapList.forEach((m) -> {
result.putAll(m);
});
// 将hook完的配置项追加到environment中
environment.getPropertySources().addFirst(new PropertiesPropertySource("dynamicProperty-" + profile, result));
} else {
log.warn("不存在profile:{}配置文件", profile);
}
} catch (Exception var12) {
log.error("profile:{}配置文件读取异常", profile, var12);
}
}
}
}
public int getOrder() {
return 2147483647;
}
}
// 以下是hooker的定义
public interface PropertiesHooker {
String PRE_FIX = "application";
String[] SUF_FIX = new String[]{".yml", ".yaml", ".properties"};
List<Map<String, ?>> hook(Resource resource);
}
// 有看管会问这些hooker有什么作用呢,举个例子,你不想在配置文件中写一些密码的明文;一些配置项想不受配置文件的限制,等等,用途很多
总结
通过上述讲解,大家对springboot提供的事件扩展点应该有一定的认识,总结起来一句话:启动时专业(Event)的EventListener干专业的事。至于什么事,业务自己定义即可。