Spring远程加载配置的实现方法详解

 更新时间:2023年03月27日 11:23:42   作者:T.Y.Bao  
这篇文章主要介绍了Spring远程加载配置的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧

前要

本文以携程的Apollo和阿里的Nacos为例。

pom中引入一下依赖:

        <dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
            <version>2.0.1</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>2021.1</version>
        </dependency>

不管是Apollo还是Nacos,实现从远程加载配置都是通过ConfigurableEnvironmentPropertySource完成的,步骤如下:

  • 远程拉取配置,生成PropertySource
  • ConfigurableEnvironment获取聚合类 MutablePropertySources propertySources = ConfigurableEnvironment#getPropertySources();
  • 将拉取的PropertySource添加到从ConfigurableEnvironment获取的聚合类MutablePropertySources#add...(PropertySource<?> propertySource)

至于这个过程是怎么触发和运行的,要看具体实现。

  • 在apollo-client中,使用BeanFactoryPostProcessor。
  • 在spring-cloud-starter-alibaba-nacos-config中,由于 cloud-nacos实现了spring cloud config规范(处于org.springframework.cloud.bootstrap.config包下),nacos实现该规范即可,即实现spring cloud 的PropertySourceLocator接口。

Apollo

关注PropertySourcesProcessor ,该类为一个BeanFactoryPostProcessor,同时为了获取ConfigurableEnvironment,该类实现了EnvironmentAware回调接口。该类何时被加入spring容器?是通过@EnableApolloConfig@Import注解的类ApolloConfigRegistrar来加入,常规套路。

public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware,
    ApplicationEventPublisherAware, PriorityOrdered {
	// aware回调接口设置
	private ConfigurableEnvironment environment;
	@Override
  	public void setEnvironment(Environment environment) {
    	//it is safe enough to cast as all known environment is derived from ConfigurableEnvironment
    	this.environment = (ConfigurableEnvironment) environment;
  	}
	@Override
  	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
  		// 获取配置
    	this.configUtil = ApolloInjector.getInstance(ConfigUtil.class);
    	// 从远程获取PropertySource
    	initializePropertySources();
    	// 为每个ConfigPropertySource注册ConfigChangeEvent监听器
    	// 监听器监听到ConfigChangeEvent后publish一个ApolloConfigChangeEvent
    	// 等于将apollo自定义的ConfigChangeEvent事件机制转化为了spring的ApolloConfigChangeEvent事件
    	initializeAutoUpdatePropertiesFeature(beanFactory);
  	}
	private void initializePropertySources() {
		// 聚合类,该类也是一个PropertySource,代理了一堆PropertySource
		// 该类中有一个 Set<PropertySource<?>> 字段
		CompositePropertySource composite = new ...;
		...
		// 从 远程 或 本地缓存 获取配置
		Config config = ConfigService.getConfig(namespace);
		// 适配Config到PropertySource,并加入聚合类		
		composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
		// 添加到ConfigurableEnvironment
		environment.getPropertySources().addFirst(composite);
	}
 	private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) {
    	if (!AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) {
      		return;
    	}
		// 定义监听器,监听器监听到ConfigChangeEvent后发布ApolloConfigChangeEvent
    	ConfigChangeListener configChangeEventPublisher = changeEvent ->
        	applicationEventPublisher.publishEvent(new ApolloConfigChangeEvent(changeEvent));
		// 注册监听器到每个PropertySource
    	List<ConfigPropertySource> configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();
    	for (ConfigPropertySource configPropertySource : configPropertySources) {
      		configPropertySource.addChangeListener(configChangeEventPublisher);
   		}
  	}
	...
}

从上面可知初始化时会从ConfigService远程拉取配置,并保存到内部缓存。而后续远程配置中心配置发生变化时本地会拉去最新配置并发布事件,PropertySource根据事件进行更新。

无论是开始从远程拉取配置初始化,还是后续远程配置更新,最终都是通过RemoteConfigRepository以http形式定时获取配置:

public class RemoteConfigRepository extends AbstractConfigRepository implements ConfigRepository{
  public RemoteConfigRepository(String namespace) {
  	...
  	// 定时拉取
	this.schedulePeriodicRefresh();
	// 长轮询
	this.scheduleLongPollingRefresh();
	...
  }
  private void schedulePeriodicRefresh() {
    // 定时线程池
    m_executorService.scheduleAtFixedRate(
        new Runnable() {
          @Override
          public void run() {
          	// 调用父抽象类trySync()
          	// trySync()调用模版方法sync()
            trySync();
          }
        }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),
        m_configUtil.getRefreshIntervalTimeUnit());
  }
  @Override
  protected synchronized void sync() {
  	// 事务
    Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");
    try {
      ApolloConfig previous = m_configCache.get();
      // http远程拉取配置
      ApolloConfig current = loadApolloConfig();
      // reference equals means HTTP 304
      if (previous != current) {
        logger.debug("Remote Config refreshed!");
        // 设置缓存
        m_configCache.set(current);
        // 发布事件,该方法在父抽象类中
        this.fireRepositoryChange(m_namespace, this.getConfig());
      }

      if (current != null) {
        Tracer.logEvent(String.format("Apollo.Client.Configs.%s", current.getNamespaceName()),
            current.getReleaseKey());
      }

      transaction.setStatus(Transaction.SUCCESS);
    } catch (Throwable ex) {
      transaction.setStatus(ex);
      throw ex;
    } finally {
      transaction.complete();
    }
    ...
  }

可以看到,在构造方法中,就执行了 3 个本地方法,其中就包括定时刷新和长轮询刷新。这两个功能在 apollo 的 github 文档中也有介绍:

  • 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
  • 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
  • 这是一个fallback机制,为了防止推送机制失效导致配置不更新。
  • 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified。
  • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。

所以,长连接是更新配置的主要手段,然后用定时任务辅助长连接,防止长连接失败。

org.springframework.cloud.bootstrap.config

nacos实现了spring cloud config规范,规范代码的maven坐标如下:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-context</artifactId>
      <version>...</version>
      <scope>compile</scope>
    </dependency>

这里介绍规范内容,nacos的实现略。

PropertySource

PropertySource用于存储k-v键值对,远程或本地的配置最终都转化为PropertySource,放入ConfigurableEnvironment中,通常EnumerablePropertySource中会代理一个PropertySource的list。

PropertySourceLocator

规范接口主要为PropertySourceLocator接口,该接口用于定位PropertySource,注释如下:

Strategy for locating (possibly remote) property sources for the Environment. Implementations should not fail unless they intend to prevent the application from starting.

public interface PropertySourceLocator {
	// 实现类实现该方法
	PropertySource<?> locate(Environment environment);
	default Collection<PropertySource<?>> locateCollection(Environment environment) {
		return locateCollection(this, environment);
	}
	static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator, Environment environment) {
		// 调用实现类
		PropertySource<?> propertySource = locator.locate(environment);
		if (propertySource == null) {
			return Collections.emptyList();
		}
		// 如果该PropertySource是代理了list的CompositePropertySource,提取全部
		if (CompositePropertySource.class.isInstance(propertySource)) {
			Collection<PropertySource<?>> sources = ((CompositePropertySource) propertySource).getPropertySources();
			List<PropertySource<?>> filteredSources = new ArrayList<>();
			for (PropertySource<?> p : sources) {
				if (p != null) {
					filteredSources.add(p);
				}
			}
			return filteredSources;
		}
		else {
			return Arrays.asList(propertySource);
		}
	}
}

PropertySourceBootstrapConfiguration

调用PropertySourceLocator接口将PropertySource加入ConfigurableEnvironment中。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration
		implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
	@Autowired(required = false)
	private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
	public void setPropertySourceLocators(Collection<PropertySourceLocator> propertySourceLocators) {
		this.propertySourceLocators = new ArrayList<>(propertySourceLocators);
	}
	@Override
	public void initialize(ConfigurableApplicationContext applicationContext) {
		List<PropertySource<?>> composite = new ArrayList<>();
		// 排序
		AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
		boolean empty = true;
		// applicationContext由回调接口提供
		ConfigurableEnvironment environment = applicationContext.getEnvironment();
		for (PropertySourceLocator locator : this.propertySourceLocators) {
			// 调用PropertySourceLocator
			Collection<PropertySource<?>> source = locator.locateCollection(environment);
			...
			for (PropertySource<?> p : source) {
				// 是否代理了PropertySource的list做分类
				if (p instanceof EnumerablePropertySource) {
					EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
					sourceList.add(new BootstrapPropertySource<>(enumerable));
				}
				else {
					sourceList.add(new SimpleBootstrapPropertySource(p));
				}
			}
			composite.addAll(sourceList);
			empty = false;
		}
		if (!empty) {
			// 获取 ConfigurableEnvironment中的MutablePropertySources
			MutablePropertySources propertySources = environment.getPropertySources();
			...
			// 执行插入到ConfigurableEnvironment的MutablePropertySources
			insertPropertySources(propertySources, composite);
			...
		}
	}
}

总结

可以看到从远程获取配置都是通过向ConfigurableEnvironment插入从远程获取的数据转化的PropertySource。而从远程获取就涉及到长轮询、本地缓存等内容,设计都比较一致。

到此这篇关于Spring远程加载配置的实现方法详解的文章就介绍到这了,更多相关Spring远程加载配置内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java Objects工具类原理及用法详解

    Java Objects工具类原理及用法详解

    这篇文章主要介绍了Java Objects工具类原理及用法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-08-08
  • java 查找字符串所在的位置代码

    java 查找字符串所在的位置代码

    这篇文章主要介绍了java 查找字符串所在的位置代码,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • java+mysql模拟实现银行系统

    java+mysql模拟实现银行系统

    这篇文章主要为大家详细介绍了java+mysql模拟实现银行系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • 学习JVM之java内存区域与异常

    学习JVM之java内存区域与异常

    关于JVM内存区域的知识对于初学者来说其实是很重要的,了解Java内存分配的原理,这对于以后JAVA的学习会有更深刻的理解。下面来看看详细介绍。
    2016-07-07
  • Java基础--反射机制

    Java基础--反射机制

    本文主要介绍了Java反射机制的相关知识。具有很好的参考价值,下面跟着小编一起来看下吧
    2017-03-03
  • ArrayList和LinkedList区别及使用场景代码解析

    ArrayList和LinkedList区别及使用场景代码解析

    这篇文章主要介绍了ArrayList和LinkedList区别及使用场景代码解析,小编觉得还是挺不错的,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • Mybatis中使用in()查询的方式详解

    Mybatis中使用in()查询的方式详解

    当参数有值,添加条件查询,附带一个字符串的in查询,下面这篇文章主要给大家介绍了关于Mybatis中使用in()查询的方式,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-08-08
  • MyBatis还是JPA?终于有答案了

    MyBatis还是JPA?终于有答案了

    这篇文章主要介绍了MyBatis还是JPA,中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • 单点登录的概念及SpringBoot实现单点登录的操作方法

    单点登录的概念及SpringBoot实现单点登录的操作方法

    在本文中,我们将使用Spring Boot构建一个基本的单点登录系统,我们将介绍如何使用Spring Security和JSON Web Tokens(JWTs)来实现单点登录功能,本文假设您已经熟悉Spring Boot和Spring Security,感兴趣的朋友一起看看吧
    2024-10-10
  • Java面向对象之类的继承介绍

    Java面向对象之类的继承介绍

    大家好,本篇文章主要讲的是Java面向对象之类的继承介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-02-02

最新评论