[享学Netflix] 十五、Netflix Archaius和Spring Cloud的集成:spring-cloud-starter-netflix-archaius

学技术不仅仅是要学技术本身,还有其思想,更重要是它的发展历史脉络。因为熟悉了这些,你便会从哲学的角度去思考问题,从而对其它技术也能触类旁通。

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning

前言

截止到上篇文章,其实关于Netflix Archaius的内容都已经讲述完了,理论上你现在应该可以没有障碍的使用它了。

本来本文我是没有打算去写的,因为掌握了核心后,去集成任何技术都是不算太难的一件事。但是,但是,但是,奈何Spring Boot/Cloud如此之火,现在一门技术如果不谈和它的整合,都不好意思说自己出道了。

基于此,本文就接着介绍下Netflix Archaius它和Spring Cloud的整合工程:spring-cloud-starter-netflix-archaius


正文

在阅读接下来内容,请务必确保你已经了解了Netflix Archaius的核心知识,以及Spring Cloud的基础支持:特别是Spring Cloud Context以及它的Commons抽象~。

说明:关于Netflix Archaius核心知识,你可以从 上篇文章(也就是十四、十三…)去了解。


spring-cloud-starter-netflix-archaius

首先需要明确:整合方面是Spring Cloud官方去整合Netflix Archaius,所以它属于一个官方支持的项目。那么一切就从它的官方工程说起:

GAV如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-archaius</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

1.4.0.RELEASE(2017.11)至今,当前最新版本为2.2.1.RELEASE,该Jar携带如下内容:

在这里插入图片描述
我本人有个疑问:为毛它会把spring-cloud-netflix-ribbon带进来,却又其实并没有任何地方使用到它,毕竟archaius属于更为底层的基础不可能使用上层API。
反倒而其实spring-cloud-netflix-ribbon它自己是携带spring-cloud-netflix-archaius的,所以这个starter的依赖管理得着实让我费解,若有可解释得通的朋友,欢迎你留言相告~

另外顺便还解答一个小疑问:工程名为何不叫spring-boot-starter-netflix-archaius,而叫spring-cloud-xxx呢?我找到了此唯一原因:它使用到了org.springframework.cloud.context.environment.EnvironmentChangeEvent这个Spring Cloud的标准事件,从而成功和Spring Cloud整合,所以它必须构建在Spring Cloud下,而非Spring Boot。


spring-cloud-starter-netflix-archaius VS spring-cloud-netflix-archaius

他俩的GAV如下,非常相似有木有。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-archaius</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-netflix-archaius</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

这个问题出自于不止一个小伙伴的提问,因此这里我也顺道解释一波。从表象上看,我相信你借助IDEA就能看出来:spring-cloud-starter-netflix-archaius管理着spring-cloud-netflix-archaius以及其它相关依赖

为了授之以渔,此处我额外用两点帮你区分这一类问题而不止是这一个问题:

  1. Spring Boot官方推荐你自定义的stater至少有两个模块
    1. autoconfigure模块:包含自动配置的代码
    2. starter模块:提供对autoconfigure模块的依赖,以及一些其它的依赖
  2. starter模块一般无代码,是个空jar。它唯一的目的是提供这个库所必须的依赖(就是管理依赖用的)

官方自己的starter均遵循此规律来实现,譬如:

  • spring-boot-starterspring-boot
  • spring-boot-starter-actuatorspring-boot-actuator
  • spring-boot-starter-aopspring-aop + aspectjweaver...

所以,spring-cloud-starter-netflix-archaius它包含有spring-cloud-netflix-archaius以及其它依赖,作为starter它只是帮你管理着那些必须依赖而已,而实际干事的是spring-cloud-netflix-archaius模块内的Java文件。


spring-cloud-netflix-archaius

它依赖于archaius-core 0.7.6实现的动态配置管理,其它依赖项均交给spring-cloud-starter-netflix-archaius管理着。

该Jar的内容并不多,有且仅有4个类:

在这里插入图片描述


ConfigurableEnvironmentConfiguration

它是对Spring环境抽象org.springframework.core.env.ConfigurableEnvironment的一个包装,适配为org.apache.commons.configuration.AbstractConfiguration,从而便可和Archaius无缝整合。

public class ConfigurableEnvironmentConfiguration extends AbstractConfiguration {

	// 管理着Spring的所有属性们
	private final ConfigurableEnvironment environment;
	public ConfigurableEnvironmentConfiguration(ConfigurableEnvironment environment) {
		this.environment = environment;
	}
	...
	@Override
	public boolean isEmpty() {
		return !getKeys().hasNext();
	}
	@Override
	public boolean containsKey(String key) {
		return this.environment.containsProperty(key);
	}
	@Override
	public Object getProperty(String key) {
		return this.environment.getProperty(key);
	}

	// 拿到所有的属性源PropertySource出来,注意这里需要处理CompositePropertySource这种哟
	@Override
	public Iterator<String> getKeys() {
		List<String> result = new ArrayList<>();
		for (Map.Entry<String, PropertySource<?>> entry : getPropertySources().entrySet()) {
			PropertySource<?> source = entry.getValue();
			if (source instanceof EnumerablePropertySource) {
				EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source;
				for (String name : enumerable.getPropertyNames()) {
					result.add(name);
				}
			}
		}
		return result.iterator();
	}

	...
}

该类的唯一作用:把Spring的环境抽象ConfigurableEnvironment适配为一个Configuration,从而可以加入到全局配置里面去。

说明:很多小伙伴看到它最终会被ArchaiusAutoConfiguration配置为一个Bean放到容器内,其实那是没有必要的,问下会解释缘由。


ArchaiusDelegatingProxyUtils

工具类。代理了ApplicationContext#getBean()等方法~

public final class ArchaiusDelegatingProxyUtils {

	// Application作为属性的key名称
	public static String APPLICATION_CONTEXT = ApplicationContext.class.getName();

	// 简单的说:就是去ApplicationContext里面去getBean
	// 前提是:ApplicationContext实例必须在全局的Configuration里面了~
	public static <T> T getNamedInstance(Class<T> type, String name) {
		ApplicationContext context = (ApplicationContext) ConfigurationManager.getConfigInstance().getProperty(APPLICATION_CONTEXT);
		return context != null && context.containsBean(name) ? context.getBean(name, type) : null;
	}
	// name = prefix + type.getSimpleName();
	public static <T> T getInstanceWithPrefix(Class<T> type, String prefix) {
		String name = prefix + type.getSimpleName();
		return getNamedInstance(type, name);
	}

	// 调用此方法:可以把ApplicationContext实例,放进Configuration里面
	public static void addApplicationContext(ConfigurableApplicationContext context) {
		AbstractConfiguration config = ConfigurationManager.getConfigInstance();
		config.clearProperty(APPLICATION_CONTEXT);
		config.setProperty(APPLICATION_CONTEXT, context);
	}
}

注意:以上三个工具方法,默认情况下没有被任何地方用到,所以当你需要自行扩展的时候,可以使用。


ArchaiusEndpoint

用于访问Archaius的配置的端点:有且仅有一个方法,并无写方法

@Endpoint(id = "archaius")
public class ArchaiusEndpoint {

	// 只有一个读方法而已
	@ReadOperation
	public Map<String, Object> invoke() {
		Map<String, Object> map = new LinkedHashMap<>();
		AbstractConfiguration config = ConfigurationManager.getConfigInstance();
		if (config instanceof ConcurrentCompositeConfiguration) {
			ConcurrentCompositeConfiguration composite = (ConcurrentCompositeConfiguration) config;
			for (Configuration item : composite.getConfigurations()) {
				append(map, item);
			}
		} else {
			append(map, config);
		}
		return map;
	}

	// 核心在append方法,往Map里放值
	// 需要注意的是:前面三种属性源都是不会添加进去的哦,就连ConfigurableEnvironmentConfiguration都不给你访问
	private void append(Map<String, Object> map, Configuration config) {
		if (config instanceof ConfigurableEnvironmentConfiguration)
			return;
		if (config instanceof SystemConfiguration)
			return;
		if (config instanceof EnvironmentConfiguration)
			return;
		for (Iterator<String> iter = config.getKeys(); iter.hasNext();) {
			String key = iter.next();
			map.put(key, config.getProperty(key));
		}
	}

}

访问示例:http://localhost:8080/actuator/archaius,默认情况下返回值是{}(因为你并没有手动给它设过值嘛),下面给给添加几个值:

public static void main(String[] args) {
    AbstractConfiguration config = ConfigurationManager.getConfigInstance();
    config.addProperty("name", "YourBatman");
    config.addProperty("age", 18);

    ConfigurableApplicationContext context = SpringApplication.run(CloudFeignApplication.class, args);
    // 这算是一个Bug,当把Context放进去后,如果访问`archaius`端点,会报错:序列化异常,所以此处展示注释掉喽
    // ArchaiusDelegatingProxyUtils.addApplicationContext(context);
}

运行程序,再次访问http://localhost:8080/actuator/archaius,控制台输出:

{"name":"YourBatman","age":18}

完美~

说明:此端点会有序列化的过程,所以若你存在不能被序列化的属性,此端点就会抛错哦。比如文上说的ApplicationContext就不能被正常序列化~


ArchaiusAutoConfiguration

以上3个类均是独立的API,有且仅有本来和Spring Boot/Cloud打了交道。它被配置在当前工程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);
	// Spring容器的环境
	@Autowired
	private ConfigurableEnvironment env;
	// 由此可知:你若要扩展属性Configuration,直接往容器里扔一个Bean即可~~~非常方便
	@Autowired(required = false)
	private List<AbstractConfiguration> externalConfigurations = new ArrayList<>();

	// 用于加载Archaius的那些默认行为,比如类路径下的config.properties
	private static DynamicURLConfiguration defaultURLConfig;


	// 这是核心初始化流程:会把容器内的配置都放在一起,并且做一些初始化的动作
	// 把用户自定义的externalConfigurations都放进来
	@Bean
	public static ConfigurableEnvironmentConfiguration configurableEnvironmentConfiguration(ConfigurableEnvironment env, ApplicationContext context) {
		ConfigurableEnvironmentConfiguration envConfig = new ConfigurableEnvironmentConfiguration(env);
		configureArchaius(envConfig, env, externalConfigurations);
		return envConfig;
	}

	protected static void configureArchaius(...) {
		// 保证只初始化一次
		if (initialized.compareAndSet(false, true)) {
			
			// 把应用名称,appName放进系统属性(全局配置)
			String appName = env.getProperty("spring.application.name");
			if (appName == null) {
				appName = "application";
				log.warn("No spring.application.name found, defaulting to 'application'");
			}
			System.setProperty(DeploymentContext.ContextKey.appId.getKey(), appName);


			// 请注意:它是一个组合属性,将会组合下面的一些配置们
			ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration();
			
			... // 把用户额外配置的属性都放进来
			config.addConfiguration(externalConfig);
			// 把Spring Env属性放进来
			config.addConfiguration(envConfig, ConfigurableEnvironmentConfiguration.class.getSimpleName());
			
			// 把形如config.properties这种配置加进来(默认是empty哦)
			defaultURLConfig = new DynamicURLConfiguration();
			config.addConfiguration(defaultURLConfig, URL_CONFIG_NAME);

			// 如果系统属性、系统环境没有被禁止
			// 那就把new SystemConfiguration()、 new EnvironmentConfiguration()加进来
			if (!Boolean.getBoolean(DISABLE_DEFAULT_SYS_CONFIG))
			if (!Boolean.getBoolean(DISABLE_DEFAULT_ENV_CONFIG))
			
			// 把组合好的配置,安装到全局的Configuration里面去
			// 需要注意的是:这里install是指定了外部config的
			// 所以即使你内部`ConfigurationManager#createDefaultConfigInstance`过,最终也会被这个给替换掉
			// 但其里面的内容都会被copy过来,不会丢失哦~~~~
			// 所以这个install是专门用于设置外部config进来的~~~~
			... ConfigurationManager.install(config);
			// addArchaiusConfiguration(config);
		}
	}

	...
		@Bean
		@ConditionalOnEnabledEndpoint
		protected ArchaiusEndpoint archaiusEndpoint() {
			return new ArchaiusEndpoint();
		}
	...
}

以上初始化步骤看似复杂,但其实只做了一件事:将Spring环境配置、用户自定义的配置externalConfigurations,以及你已经放进去的一些属性们全部放进全局Configuration里

它的初始化步骤可总结如下:

  1. 把应用名appName放进去(应用名由spring.application.name来定,否则默认值是application)
  2. 用户自定义的的Configuration Bean放进去(因为它最先放进去,所以优先级最高)
  3. 把Spring的ConfigurableEnvironmentConfiguration放进去(优先级第二)
  4. Archaius自己的DynamicURLConfiguration放进去
  5. SystemConfiguration、EnvironmentConfiguration放进去(若没禁止的话)

最终,全局Configuration属性的值的截图参考如下:

在这里插入图片描述
初始化结束后,不仅仅完成了全局Configuration的初始化,并且然后还把ConfigurableEnvironmentConfiguration放进了容器(其实我觉得它是完全没必要放进容器里的,因为它里面的属性并不全,我们也不会直接用它:只有Spring容器内的,向你通过ConfigurationManager.getConfigInstance().addProperty("name", "YourBatman")这种方式放进去的话它是获取不到的哦)。


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

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

这时候Spring Cloud就出马了,利用org.springframework.cloud.context.environment.EnvironmentChangeEvent这个事件就能很好的完成这个工作:

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));
				}
			}
		}

	}

逻辑很简单:对所有发生变更的key们,逐个触发其AbstractConfiguration.EVENT_SET_PROPERTY事件从而同步更新全局配置属性。

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


使用示例

使用示例在Spring Cloud配置中心篇章里会回溯到此,请出门参阅。


关于Archaius2.x

其实Archaius1.x(或者说0.x)现在基本处于一个停更(只维护)状态,一般来说软件到这种时候,生命的尽头就快到了。

说明:Archaius1.x的最新版本是0.7.7(和0.7.6差不多)

而实际上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虽然优点众多,但是,但是,但是:由于不管是现在的Hystrix,还是Spring Cloud Netflix xxx(哪怕到了最新版本)依赖的均是Archaius1.x版本(0.7.7版本),所以本系列只讲1.x,而不讲2.x。

还是一样的,万变不离其宗,有兴趣的小伙伴可自行研究Archaius2.x如何使用?


总结

关于Netflix Archaius和Spring Cloud的集成部分就说到这了,至此全部关于Archaius的内容就介绍完了,它作为基础中的基础,后面章节将会使用到它,所以还会频繁见面哦~

另外提示一点:你可以看到,即便到了Spring Boot2.2.x这么高的版本,它依赖的依旧还都是Archaius 1.x版本以及Commons Configuration1.x版本。所以说,即使Commons Configuration2.x已成为主流被推荐使用,但是1.x依旧有很高的学习价值的,请勿忽视它哦。

分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了310 篇原创文章 · 获赞 474 · 访问量 40万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/104486647