[享学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以及其它相关依赖

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

  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依旧有很高的学习价值的,请勿忽视它哦。