[享学Netflix] 十五、Archaius和Spring Cloud的集成
- 2020 年 3 月 18 日
- 笔记
学技术不仅仅是要学技术本身,还有其思想,更重要是它的发展历史脉络。因为熟悉了这些,你便会从哲学的角度去思考问题,从而对其它技术也能触类旁通。 代码下载地址: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
以及其它相关依赖。
为了授之以渔,此处我额外用两点帮你区分这一类问题而不止是这一个问题:
Spring Boot
官方推荐你自定义的stater至少有两个模块autoconfigure
模块:包含自动配置的代码starter
模块:提供对autoconfigure模块的依赖,以及一些其它的依赖
starter
模块一般无代码,是个空jar。它唯一的目的是提供这个库所必须的依赖(就是管理依赖用的)
官方自己的starter均遵循此规律来实现,譬如:
spring-boot-starter
和spring-boot
spring-boot-starter-actuator
和spring-boot-actuator
spring-boot-starter-aop
和spring-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里。
它的初始化步骤可总结如下:
- 把应用名
appName
放进去(应用名由spring.application.name
来定,否则默认值是application) - 把用户自定义的的
Configuration
Bean放进去(因为它最先放进去,所以优先级最高) - 把Spring的
ConfigurableEnvironmentConfiguration
放进去(优先级第二) - 把
Archaius
自己的DynamicURLConfiguration
放进去 - 把
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依旧有很高的学习价值的,请勿忽视它哦。