Springboot启动过程中的五大扩展点

2022-04-14 00:00:00 逻辑 自定义 事件 扩展 配置文件

作者 | Love DN 
出品 | 脚本之家(ID:jb51net)

本文讨论的是基于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,对配置文件进行修改

一个例子
  1. 配置spring.factories,定义一个事件

    org.springframework.context.ApplicationListener=net.sy.config.listener.DatasourcePropertiesListener
  2. 声明事件监听器

    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干专业的事。至于什么事,业务自己定义即可。


相关文章