前言
最开始为 loveqq 框架添加 nacos 配置中心启动器的时候,一个问题一直困扰着我。
那就是框架的启动需要配置文件,而配置文件在配置中心。
当应用启动后再取读取配置文件也是可以的,但问题是有些条件 bean,需要依赖配置项才能决定是否应该实例化。 所以当应用启动完成后再拉取配置文件,虽然配置读到了,但是 bean 的依赖关系却不是我所需要的。
于是,我就陷入了一个死循环:要启动就需要读取配置,而要配读取置就需要先启动!到底如何才能提前读取到配置文件呢?
最开始,我想搞一个 nacos 的 BeanPostProcessor,当监控到 PropertyContext 实例化的时候直接去拉取配置。 但是这样是不行的,因为 BeanPostProcessor 注册的时候,所有的 bean 定义已经加载完毕,条件注解已经判断,此时再读取配置已经晚了。
此时,我突然想到了 spring 的 @FeignClient 客户端的一些初始化源码逻辑,依稀记得每次创建 Feign 客户端,spring 都要创建一个 SpringApplication run 一下。
于是,我就想到了是否可以额外启动一个应用,仅用于读取配置文件,读取完毕后,再走主流程?
于是,一个构思慢慢的出现了,我创建了一个新模块:loveqq-boot-cloud-bootstrap,并新增了一个 @BootstrapConfiguration 注解。 该模块专门用于加载 @BootstrapConfiguration 注解的配置,加载后将 @BootstrapConfiguration 注解的 bean 注册到主流程的 BeanFactory。
引导上下文
引导上下文(BootstrapApplicationContext),继承了 DefaultConfigurableApplicationContext,目的是防止重入,导致重复引导。
Bootstrap 接口
专门抽象出了 Bootstrap 接口,方便第三方实现自己的引导逻辑,具体实现如下:
java
package com.kfyty.loveqq.framework.cloud.bootstrap;
/**
* 描述: {@link BeanFactory} 引导
* 加载 {@link BootstrapConfiguration} 标记的配置类,并复制到给定的应用上下文
*
* @author kfyty725
* @date 2023/9/10 22:00
* @email kfyty725@hotmail.com
*/
@Slf4j
public class BeanFactoryBootstrap implements Bootstrap {
/**
* 空 bean
*/
private static final Object EMPTY_BEAN = new Object();
@Override
public void bootstrap(ConfigurableApplicationContext applicationContext) throws Exception {
log.info("Bootstrap starting...");
try (ApplicationContext bootstrapContext = new BootstrapApplicationContextFactory().create(applicationContext.getCommandLineArgs(), BeanFactoryBootstrapApplication.class)) {
bootstrapContext.refresh();
this.mergeBootstrapConfiguration(bootstrapContext, applicationContext);
}
log.info("Bootstrap succeed completed.");
}
/**
* 合并 {@link BootstrapConfiguration} 标注的 bean
* 将引导上下文中的 {@link BootstrapConfiguration} 标注的 bean 合并到主应用上下文
*
* @param bootstrapContext 引导 BeanFactory
* @param applicationContext 主应用 BeanFactory
*/
protected void mergeBootstrapConfiguration(ApplicationContext bootstrapContext, ConfigurableApplicationContext applicationContext) {
for (Map.Entry<String, BeanDefinition> entry : bootstrapContext.getBeanDefinitionWithAnnotation(BootstrapConfiguration.class).entrySet()) {
this.registryBootstrapBean(entry.getKey(), entry.getValue(), bootstrapContext, applicationContext);
}
}
/**
* 注册引导配置类
* 被注册的引导配置类,在引导上下文中将被替换为 {@link this#EMPTY_BEAN},避免销毁引导上下文时被销毁
*
* @param beanName 引导配置 bean
* @param beanDefinition 引导配置 bean definition
* @param bootstrapContext 引导上下文
* @param applicationContext 应用上下文
*/
protected void registryBootstrapBean(String beanName, BeanDefinition beanDefinition, ApplicationContext bootstrapContext, ConfigurableApplicationContext applicationContext) {
// 注册到应用上下文
Object bean = bootstrapContext.getBean(beanName);
applicationContext.registerBeanDefinition(beanName, beanDefinition);
applicationContext.replaceBean(beanName, bean);
if (bean instanceof BeanPostProcessor) {
applicationContext.registerBeanPostProcessors(beanName, (BeanPostProcessor) bean);
}
// 替换引导上下文的中 bean,避免被销毁
bootstrapContext.replaceBean(beanName, EMPTY_BEAN);
// 使用应用上下文重新回调 aware 接口
this.invokeAware(beanName, bean, applicationContext);
// 注册嵌套的引导配置 bean
for (Method method : ReflectUtil.getMethods(beanDefinition.getBeanType())) {
Bean beanAnnotation = AnnotationUtil.findAnnotation(method, Bean.class);
if (beanAnnotation != null) {
String nestedBeanName = BeanUtil.getBeanName(method, beanAnnotation);
if (bootstrapContext.containsBeanDefinition(nestedBeanName)) {
this.registryBootstrapBean(nestedBeanName, bootstrapContext.getBeanDefinition(nestedBeanName), bootstrapContext, applicationContext);
}
}
}
}
/**
* 执行相关能力接口,注入新的上下文
*
* @param bean 引导 bean
* @param applicationContext 应用上下文
*/
protected void invokeAware(String beanName, Object bean, ConfigurableApplicationContext applicationContext) {
if (bean instanceof BeanFactoryAware aware) {
aware.setBeanFactory(applicationContext);
}
if (bean instanceof ApplicationContextAware aware) {
aware.setApplicationContext(applicationContext);
}
if (bean instanceof ConfigurableApplicationContextAware aware) {
aware.setConfigurableApplicationContext(applicationContext);
}
// 主应用还未启动,配置上下文还不存在,注册一个回调 bean 定义
if (bean instanceof PropertyContextAware) {
BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(PropertyContextAwareCallback.class)
.setBeanName(beanName.concat("_PropertyContextAwareCallback"))
.addConstructorArgs(PropertyContextAware.class, bean)
.getBeanDefinition();
applicationContext.registerBeanDefinition(beanDefinition.getBeanName(), beanDefinition, false);
}
}
@RequiredArgsConstructor
static class PropertyContextAwareCallback implements PropertyContextAware, Ordered {
/**
* bean
*/
private final PropertyContextAware bean;
@Override
public void setPropertyContext(GenericPropertiesContext propertiesContext) {
this.bean.setPropertyContext(propertiesContext);
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}
}
这样一来,就实现了通用的引导逻辑。
nacos 配置中心引导
于是 nacos 的配置中心启动器就有了解决方案 创建一个 NacosPropertyLoaderBeanPostProcessor 处理器,并将其标记为 BootstrapConfiguration。 这样在引导阶段,该 BeanPostProcessor 就会被注册到主应用里,将 PropertyContext 实例化的时候,就可以第一时间感知到,并拉取配置。
springcloud 实现
今天我突然想到 springcloud 应该也会遇到我这个问题,那么 springcloud 是如何实现的呢? 一同搜索,大家可以看这个:zhuanlan.zhihu.com/p/456499577
loveqq 在没有参考,并且不知道 springcloud 如何实现的情况下,和 springcloud 的实现可以说是不谋而合。
不过区别还是有的: springcloud 是基于自己的事件监听机制引导,loveqq 则是专门的引导接口。
springcloud 专门新增了 bootstrap.yaml 配置文件,loveqq 则不需要。
springcloud 做了不少默认配置及判断操作,loveqq 则不需要,因为 loveqq 在设计之初,就具有一定的自我配置,自我启动能力。比如 ApplicationContext 的创建也依赖自身的依赖注入功能。
这次就到这里了,感兴趣的可以 gitee/gitcode/github 看看。