[享学Archaius] 十七、Spring的Environment是如何整合进Archaius从而实现全局共享的?

效率永远是公司最看重的,而不是时间的付出。

–> 返回Netflix OSS套件专栏汇总 <–
代码下载地址:https://github.com/f641385712/netflix-learning

前言

上篇文章主要介绍了ArchaiusSpring Cloud的整合工程spring-cloud-starter-netflix-archaius的内容,本文将继续,会将站在实用的角度,深度分析ArchaiusAutoConfiguration该自动配置类到底做了哪些事,以及最后给出具体代码示例来体会一把Spring环境抽象EnvironmentArchaius整合的效果。

你可带着疑问阅读本文:为何Ribbon、Hystrix明明是使用Archaius管理自己的配置,而在Spring Cloud下把相应配置项写在application.properties / 配置中心里竟然都好使,何解?


正文

上篇文章介绍的spring-cloud-netflix-archaius工程里的3个类可认为均属于独立的API,和SB/SC整合并无关系,接下来便进入到整合的重点:ArchaiusAutoConfiguration自动配置类,当然喽这需要前文独立API内容的支持。


ArchaiusAutoConfiguration 自动配置类

说到Spring Boot的自动配置,相信是没有人不熟悉的。ArchaiusAutoConfiguration就是这样的一个自动配置类,它被配置在当前工程spring.factories文件里,容器启动时生效:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration

初始化配置

// 说明:ConfigurationBuilder是Apache Commons Configuration1.x的核心API
// ConcurrentCompositeConfiguration是archaius1.x(0.x)的核心API
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ ConcurrentCompositeConfiguration.class, ConfigurationBuilder.class })
// 优先级最高。毕竟一切都得配置先行嘛
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) 
public class ArchaiusAutoConfiguration {

	private static final AtomicBoolean initialized = new AtomicBoolean(false);
	@Autowired
	private ConfigurableEnvironment env;
	@Autowired(required = false)
	private List<AbstractConfiguration> externalConfigurations = new ArrayList<>();

	private static DynamicURLConfiguration defaultURLConfig;
}
  • AtomicBoolean initialized:是否已经初始化,避免此配置被加载两次而执行两次初始化逻辑:所以此属性是个static属性哦~
  • ConfigurableEnvironment env:Spring容器的环境抽象
  • List<AbstractConfiguration> externalConfigurations:你自己扔进容器里的AbstractConfiguration配置实例,这里会被注入进来
    • 但是并没有任何的使用到的地方,因为它下面又自己用context.getBeansOfType(AbstractConfiguration.class)去容器内获取了(下文有体现),所以其实这行代码是无用的
    • 因为下面是通过static方法定义的,因此必须是自己从容器里获取(由于之前版本并非实用static,而是使用的注入的属性值,现在改为了static方式,估计代码作者忘删了)
  • DynamicURLConfiguration defaultURLConfig:static静态属性。也就是最终它会把DynamicURLConfiguration放到全局属性里去(画外音:你的config.properties文件依旧有效,但是请注意优先级)

初始化逻辑从ConfigurableEnvironmentConfiguration这个配置Bean的实例化开始:

ArchaiusAutoConfiguration:

	// 这是核心初始化流程:会把容器内的配置都放在一起,并且做一些初始化的动作
	// 把用户自定义的externalConfigurations都放进来(自己获取)
	@Bean
	public static ConfigurableEnvironmentConfiguration configurableEnvironmentConfiguration(ConfigurableEnvironment env, ApplicationContext context) {
		// externalConfigurations的获取。其实效果同@Autowired,只是这是static方法不能直接使用而已~
		Map<String, AbstractConfiguration> abstractConfigurationMap = context.getBeansOfType(AbstractConfiguration.class);
		List<AbstractConfiguration> externalConfigurations = new ArrayList<>(abstractConfigurationMap.values());

		// 组合配置
		ConfigurableEnvironmentConfiguration envConfig = new ConfigurableEnvironmentConfiguration(env);
		configureArchaius(envConfig, env, externalConfigurations);
		return envConfig;
	}


	protected static void configureArchaius(ConfigurableEnvironmentConfiguration envConfig, ConfigurableEnvironment env, List<AbstractConfiguration> externalConfigurations) {
		// initialized是静态变量:它能保证全局只会初始化一次
		if (initialized.compareAndSet(false, true)) {
			String appName = env.getProperty("spring.application.name");
			if (appName == null) {
				appName = "application";
				log.warn("No spring.application.name found, defaulting to 'application'");
			}
			// 部署上下文使用的APPId为应用名
			System.setProperty(DeploymentContext.ContextKey.appId.getKey(), appName);


			// 组合配置,我们知道`ConfigurationManager`它最终也是生成的一个组合配置
			// 这里因为要加入Spring的Environment 所以重新组装一次
			ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration();

			
			// 1、自定义配置们放进来
			if (externalConfigurations != null) {
				for (AbstractConfiguration externalConfig : externalConfigurations) {
					config.addConfiguration(externalConfig);
				}
			}
			// 2、Spring的Environment环境放进来
			config.addConfiguration(envConfig, ConfigurableEnvironmentConfiguration.class.getSimpleName());

			// 3、加载`Archaius`自己的配置们放进来  所以config.properties等均还是有效的(且天生支持动态修改)
			defaultURLConfig = new DynamicURLConfiguration();
			try {
				config.addConfiguration(defaultURLConfig, URL_CONFIG_NAME);
			} catch (Throwable ex) {
				log.error("Cannot create config from " + defaultURLConfig, ex);
			}

			// 默认均会条件进来
			if (!Boolean.getBoolean(DISABLE_DEFAULT_SYS_CONFIG)) {
				SystemConfiguration sysConfig = new SystemConfiguration();
				config.addConfiguration(sysConfig, SYS_CONFIG_NAME);
			}
			if (!Boolean.getBoolean(DISABLE_DEFAULT_ENV_CONFIG)) {
				EnvironmentConfiguration environmentConfiguration = new EnvironmentConfiguration();
				config.addConfiguration(environmentConfiguration, ENV_CONFIG_NAME);
			}
			ConcurrentCompositeConfiguration appOverrideConfig = new ConcurrentCompositeConfiguration();
			config.addConfiguration(appOverrideConfig, APPLICATION_PROPERTIES);
			config.setContainerConfigurationIndex(config.getIndexOfConfiguration(appOverrideConfig));

			// 简单的说就是调用:ConfigurationManager.install(config);
			// 把该组合配置放进全局配置里
			addArchaiusConfiguration(config);
		} else { // 全局配置已经安装过 无需再安装
			log.warn("Netflix ConfigurationManager has already been installed, unable to re-install");
		}
	}

此配置初始化步骤看似复杂,但其实总结起来蛮简单的:将Spring环境、用户自定义的配置externalConfigurationsArchaius自己管理的配置组合起来,放进全局配置里。步骤可总结如下:

  1. 部署上下文DeploymentContext应用名称默认使用Spring Boot的应用名称:spring.application.name(若没指定默认名为application
  2. 使用组合配置类ConcurrentCompositeConfiguration作为最终的全局配置。最先把用户自定义的放进去(优先级最高)
  3. 然后是Spring的ConfigurableEnvironmentConfiguration环境抽象(优先级第二)
  4. 然后是Archaius管理的DynamicURLConfiguration(优先级第三,如config.properties里的配置)
  5. 最后若没显示禁用的话,会把SystemConfiguration、EnvironmentConfiguration放进去

不妥之处

初始化流程结束后,完成了全局Configuration配置的内容填充,并且最后把ConfigurableEnvironmentConfiguration这个Bean放进了容器里,其实这是不妥的,原因如下:

  1. ConfigurableEnvironmentConfiguration仅是Environment的一个包装器,因此它有且仅包含Spring环境内的配置,是不完整的
  2. 若配置属性是Archaius专属管理的,如config.properties里的或者ConfigurationManager.getConfigInstance().addProperty("name", "YourBatman")添加进来的,通过它都是获取不到的。

因此指出把它放进容器内的不妥之处(或者说是使用时候需要特别注意的地方),最好不要直接使用它是最安全的。


代码示例

config.properties内容:

my.name = YourBatman
my.age = 100

application.yaml内容:

my:
  age: 18

启动Spring Boot容器测试:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);

        // 读取配置
        AbstractConfiguration config = ConfigurationManager.getConfigInstance();
        System.out.println(config.getString("my.name"));
        System.out.println(config.getString("my.age"));

        DynamicStringProperty name = DynamicPropertyFactory.getInstance().getStringProperty("my.name", "defualtName");
        DynamicIntProperty age = DynamicPropertyFactory.getInstance().getIntProperty("my.age", 0);
        System.out.println(name);
        System.out.println(age);

        context.close();
    }

}

控制台输出:

YourBatman
18
DynamicProperty: {name=my.name, current value=YourBatman}
DynamicProperty: {name=my.age, current value=18}

可以看到application.yaml的优先级是更高的,也就是Spring的env比Archaius管理的配置是具有更高优先级的,从全局配置里也可看出,如下截图:

在这里插入图片描述

现在应该能够解答你的疑惑:为何Ribbon、Hystrix明明是使用Archaius管理着配置的,为何你把配置写在application.properties/配置中心里依旧好使了吧(并且还支持动态刷新哦),其核心便是“替换”了全局配置。


全局配置如何感知到Spring环境属性的变更

在使用开发中,我们的配置大都写在application.properties/yaml里,或者在配置中心里(而并不会放在conifg.properties里),总之最终都会被放进Spring的Environment里,那么问题就来了:全局配置如何感知到Spring环境属性的变更,从而保持同步性呢

这时候Spring Cloud就出马了,利用org.springframework.cloud.context.environment.EnvironmentChangeEvent这个事件就能很好的完成这项工作,这也就是为何工程名是spring-cloud-xxx的唯一原因吧:

ArchaiusAutoConfiguration:

	// 它是个ApplicationListener,监听EnvironmentChangeEvent事件
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnProperty(value = "archaius.propagate.environmentChangedEvent", matchIfMissing = true)
	@ConditionalOnClass(EnvironmentChangeEvent.class)
	protected static class PropagateEventsConfiguration implements ApplicationListener<EnvironmentChangeEvent> {

		@Autowired
		private Environment env;

		@Override
		public void onApplicationEvent(EnvironmentChangeEvent event) {
			// 拿到全局配置
			AbstractConfiguration manager = ConfigurationManager.getConfigInstance();

			// 从事件里拿到所有改变了的Key们,一个一个的处理
			for (String key : event.getKeys()) {
				// 拿到注册在Configuration上的所有事件门,一个一个的触发他们的configurationChanged方法
				// 事件类型是:AbstractConfiguration.EVENT_SET_PROPERTY;
				for (ConfigurationListener listener : manager.getConfigurationListeners()) {
					Object source = event.getSource();
					// TODO: Handle add vs set vs delete?
					int type = AbstractConfiguration.EVENT_SET_PROPERTY;
					// 改变后的value从env里获取(可能为null哦~)
					// 当然一般建议动态修改,而非删除,请务必注意喽
					String value = this.env.getProperty(key);
					boolean beforeUpdate = false;
					listener.configurationChanged(new ConfigurationEvent(source, type, key, value, beforeUpdate));
				}
			}
		}
	}

逻辑很简单:向容器内放一个ApplicationListener监听器监听着EnvironmentChangeEvent事件,对所有发生变更的key们,逐个触发其AbstractConfiguration.EVENT_SET_PROPERTY事件从而借助Archaius自己的同步机制更新全局配置属性。

你可以通过archaius.propagate.environmentChangedEvent=false来显示的关闭这个行为,但很显然一般你并不需要这么做。


关于Archaius2.x

其实Archaius1.x(或者说0.x)现在基本处于一个停更(只维护)状态,一般来说软件到这个状态,生命的尽头就快到了,更何况整个Netflix大都均已经进入维护阶段了呢。做为这么优秀的一个配置管理库,实际上Archaius是在继续(独立)发展的,那就是它的2.x版本:

<dependency>
    <groupId>com.netflix.archaius</groupId>
    <artifactId>archaius2-core</artifactId>
    <version>2.3.16</version>
</dependency>

它采用了全新的API设计(不向下兼容),并且采用API + 实现分离的方式,并不强依赖于Commons Configuration来实现,可扩展性更强了。Archaius2.x虽然优点众多,但是,但是,但是:由于不管是现在的Netflix OSS,还是Spring Cloud Netflix xxx(哪怕到了最新版本)依赖的均还是Archaius1.x版本(0.7.7版本),这也是为何本系列只讲1.x分支的原因,毕竟还是要以实用为主。

不过话说回来:我对Archaius2.x的印象还是不错的,如果你仅仅是需要一个配置库作为你的基础配置管理支撑,选择Archaius2.x是个非常好的选择,在功能性、可扩展性上它都大大优于1.x。但是现在我们大都处于Spring体系的温室里,因此请各位自行衡量投入产出比~


总结

关于Netflix ArchaiusSpring Cloud的整合,以及整个Archaius系列就全部介绍完了。配置对于一个程序的重要程度不言而喻,多么过分的强调都不为过,这也是为何我觉得现在称呼某些架构师为配置工程师是更合适的,因此本系列花重笔墨专题介绍了Archaius以及Apache Commons Configuration的使用和原理,它作为基础中的基础,在Netflix其它专题系列如Hystrix、Ribbon等中还会经常见面的~

最后再强调一点:即便到了Spring Boot2.2.x这么高的版本,它依赖的依旧还都是Archaius 1.x以及Commons Configuration1.x版本。所以很多时候切忌不要为了追新而追新,而是要围绕其背后的生态来学习才最具价值。
分隔线

声明

展开阅读全文
©️2020 CSDN 皮肤主题: 代码科技 设计师: Amelia_0503 返回首页
实付59.90元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值