『Naocs 2.x』(七) Nacos 配置中心是如何与 Spring Boot 结合的

1,349 阅读3分钟

前言

之前写了部分 Nacos 注册中心的源码分析文章,现在继续接着看 Nacos 配置中心的源码了。 本次目标有两个:

  • 了解 Spring Boot 项目启动时,如何从 Nacos 配置中心获取初始化配置。
  • Nacos 配置中心配置变更时,Spring Boot 项目如何更新。

当然实践至上,当弄明白上述两个问题后,也需要写一个丐版注册中心来加强认知。

丐版注册中心需要实现两个功能:

  • Spring Boot 项目启动时,可从丐版注册中心获取初始配置。
  • 丐版注册中心配置变更时,Spring Boot 项目能更新配置。

Spring Boot 初始化器

我们在《 『深入学习 Spring Boot』(二) 系统初始化器 》中曾学习到:

官方描述:系统初始化器是Spring容器刷新之前执行的一个回调函数。

作用是向Spring Boot容器中注册属性,需要实现ApplicationContextInitializer接口。

Spring Cloud 配置中心就是依靠ApplicationContextInitializer去调用远程接口,获取配置信息。

org.springframework.cloud.bootstrap.config包下,有此类:

public class PropertySourceBootstrapConfiguration implements
        ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
 
  @Autowired(required = false)
    private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
 
   // ......
  @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        List<PropertySource<?>> composite = new ArrayList<>();
        AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
        boolean empty = true;
    // 从应用上下文中,取出环境类
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        for (PropertySourceLocator locator : this.propertySourceLocators) {
      // 执行 locateCollection 方法,获取具体配置。
      // 注意本方法执行后,配置已经被从远程加载进来了。
            Collection<PropertySource<?>> source = locator.locateCollection(environment);
            // ....
            List<PropertySource<?>> sourceList = new ArrayList<>();
            for (PropertySource<?> p : source) {
                if (p instanceof EnumerablePropertySource) {
                    EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
                    sourceList.add(new BootstrapPropertySource<>(enumerable));
                }
                // ....
            }
            composite.addAll(sourceList);
            empty = false;
        }
        if (!empty) {
            MutablePropertySources propertySources = environment.getPropertySources();
            // ....
      // 把 composite(远程的属性) 设置到 环境类中
            insertPropertySources(propertySources, composite);
            // ....
        }
    }
 
  // .....
}

这里还涉及到了,Spring 上下文中的Environment类。

如果觉得陌生,可先阅读 《『深入学习 Spring Boot』(十) Environment 》、《『深入学习 Spring Boot』(十一) profiles 》我的两篇博客。

此类就是 Spring Boot 与 Nacos 之间的 God Class了。

PropertySourceLocator 属性加载器

这是一个接口定义,其实现类将真正读取属性。

public interface PropertySourceLocator {
 
    PropertySource<?> locate(Environment environment);
 
    default Collection<PropertySource<?>> locateCollection(Environment environment) {
        return locateCollection(this, environment);
    }
  // 注意,上文 PropertySourceBootstrapConfiguration # initialize,调用了此方法。
  // 也就是调用了 locate 方法。
    static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator,
            Environment environment) {
        PropertySource<?> propertySource = locator.locate(environment);
    // ....
    }
 
}

查看此接口的关系:

image-20211107225427546

好巧不巧,有个 Nacos 相关的实现。这下子可以串起来了。

Spring Boot 项目启动时,将执行 PropertySourceBootstrapConfiguration # initialize(),在其中获取到 PropertySourceLocator的实现类遍历执行locate()方法。

那么我们可以有一个猜想了,NacosPropertySourceLocatorlocate()中,请求配置中心获取到配置,设置到Environment中,Spring Boot 不就可以正常启动了。

NacosPropertySourceLocator

public class NacosPropertySourceLocator implements PropertySourceLocator {
 
  @Override
    public PropertySource<?> locate(Environment env) {
        // ....
 
        CompositePropertySource composite = new CompositePropertySource(
                NACOS_PROPERTY_SOURCE_NAME);
 
    // 1.加载共享配置
        loadSharedConfiguration(composite);
    // 2.加载扩展配置
        loadExtConfiguration(composite);
    // 3.加载应用配置
        loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
 
        return composite;
    }
 
  // 1、2、3底层都是通过调用 loadNacosDataIfPresent
    private void loadNacosDataIfPresent(final CompositePropertySource composite,
        final String dataId, final String group, String fileExtension,
        boolean isRefreshable) {
    // ....
    NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
            fileExtension, isRefreshable);
    this.addFirstPropertySource(composite, propertySource, false);
}
 
  // 继续调用 loadNacosPropertySource()
  private NacosPropertySource loadNacosPropertySource(final String dataId,
            final String group, String fileExtension, boolean isRefreshable) {
    // 不自动刷新,直接从本地缓存拿
        if (NacosContextRefresher.getRefreshCount() != 0) {
            if (!isRefreshable) {
                return NacosPropertySourceRepository.getNacosPropertySource(dataId,
                        group);
            }
        }
    // 这里是发请求的方法
        return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
                isRefreshable);
    }
}

NacosPropertySourceBuilder

public class NacosPropertySourceBuilder {
 
    NacosPropertySource build(String dataId, String group, String fileExtension,
            boolean isRefreshable) {
        Map<String, Object> p = loadNacosData(dataId, group, fileExtension);
        NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId,
                p, new Date(), isRefreshable);
        NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
        return nacosPropertySource;
    }
 
    private Map<String, Object> loadNacosData(String dataId, String group,
            String fileExtension) {
        String data = null;
        try {
      // 获取配置。重点。
            data = configService.getConfig(dataId, group, timeout);
            // 格式化配置
            Map<String, Object> dataMap = NacosDataParserHandler.getInstance()
                    .parseNacosData(data, fileExtension);
            return dataMap == null ? EMPTY_MAP : dataMap;
        }
        return EMPTY_MAP;
    }
}

NacosConfigService

public class NacosConfigService implements ConfigService {
 
    @Override
    public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
        return getConfigInner(namespace, dataId, group, timeoutMs);
    }
 
  private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
        group = blank2defaultGroup(group);
        ParamUtils.checkKeyParam(dataId, group);
        ConfigResponse cr = new ConfigResponse();
        cr.setDataId(dataId);
        cr.setTenant(tenant);
        cr.setGroup(group);
 
            // ...
 
        try {
            // 从远程获取配置信息。content 字符串的内容就是具体的配置内容。
            ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false);
            cr.setContent(response.getContent());
            cr.setEncryptedDataKey(response.getEncryptedDataKey());
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();
            return content;
        } catch (NacosException ioe) {
            // ...
        }
        // ...
    }
}

image-20211108224422948

从远程获取到配置项后,就可以保存到上下文环境中,进行应用程序的启动了。

小结

Nacos 配置中心是如何与 Spring Cloud 结合的?

  1. Spring Boot 有初始化器机制,专门用于初始化属性的。Spring Cloud 实现了一个初始化器,并在其中调用PropertySourceLocator#locate()
  2. Nacos 有一PropertySourceLocator的实现类,并在locate()方法中进行远程调用,获取具体配置项。