如果你想拥有不平凡的人生,那就请拿出不平凡的努力 代码下载地址:https://github.com/f641385712/netflix-learning
在当下日益复杂的互联网云环境中,对应用APP的灵活部署要求越来越高:同样的一份代码在不同环境、不同区域…需要有不同表现(逻辑不同、性能不同…),同时高可用方面的多机房、多云灾备亦体现出了部署的复杂性。
通过前几篇文章关于Netflix Archaius
的学习,相信你已经完全掌握了它是如何处理组合配置、如何让属性动态化的。在它的属性抽象com.netflix.config.Property
中,有一个非常重要的子类DynamicContextualProperty
--> 根据上下文不同,属性值也不一样,它便是今天本文的主要内容。
DynamicContextualProperty
里的Contextual
便是它的核心:上下文是个泛概念,它可以包括环境、区域、数据中心等等,但却又不限于此。它是Netflix Archaius
拿来应对多环境部署、复杂环境获取不同属性值的有效工具,本文将展开对它以及部署上下文DeploymentContext
的深入探讨和学习。
说明:多环境配置支持 + 动态化,想起来就很激动有木有~
对于多环境部署的,Archaius
主要使用两个核心API来给与支持:DynamicContextualProperty
和DeploymentContext
,接下来由浅入深,一步步了解其深意。
定义应用程序的部署上下文的接口。
public interface DeploymentContext {
// 提出一点:命名不规范,应用全大写
// 这里几乎覆盖了部署所有的参数需求,如环境、数据中心、分区等等都有
// 当然还可能不够的,下面会教你如何扩展
public enum ContextKey {
environment("@environment"), datacenter("@datacenter"), appId("@appId"),
serverId("@serverId"), stack("@stack"), region("@region"), zone("@zone");
...
}
// ========接口方法=========
// For example "test", "dev", "prod"。当然喽,任意字符串都行
public String getDeploymentEnvironment();
public void setDeploymentEnvironment(String env);
// 数据中心
public String getDeploymentDatacenter();
public void setDeploymentDatacenter(String deployedAt);
// 对应@appId
public String getApplicationId();
public void setApplicationId(String appId);
// 对应:@serverId。注意和@appId的区别。
// 一般来说 一个APP都是多实例部署嘛
public void setDeploymentServerId(String serverId);
public String getDeploymentServerId();
// 部署此应用程序的堆栈名称。
// 堆栈的名字可以用来影响应用程序的行为。
public void setDeploymentStack(String stack);
public String getDeploymentStack();
// 分区:如东北区、华南区、北美区等等 us-east-1/us-west-1...
public String getDeploymentRegion();
public void setDeploymentRegion(String region);
// ========通用方法======
// 其实你会发现,还有@zone没有给特定的方法,所以这里给了个通用方法
// 这样是为了避免后续枚举增加值,弄得不向下兼容了,所以给出一个通用方法喽
public String getValue(ContextKey key);
public void setValue(ContextKey key, String value);
}
注意:所有属性是可选的,如果未设置,可能返回null。
该接口看着方法很多,其实就一句话:内部一个Map,key是ContextKey
类型,value是String类型,来表示部署上下文参数。它的直接实现类是:SimpleDeploymentContext
。
太简单了,内部维护一个map(线程安全的Map)管理者上下文属性们。
public class SimpleDeploymentContext implements DeploymentContext {
private Map<DeploymentContext.ContextKey, String> map = new ConcurrentHashMap<>();
@Override
public String getDeploymentEnvironment() {
return map.get(ContextKey.environment);
}
@Override
public void setDeploymentEnvironment(String env) {
map.put(ContextKey.environment, env);
}
... // 其它方法类似,略
@Override
public String getValue(ContextKey key) {
return map.get(key);
}
@Override
public void setValue(ContextKey key, String value) {
map.put(key, value);
}
}
它是使用Map维护,是一种通用实现,但是却还没有和Configuration
挂上钩。它有个子类ConfigurationBasedDeploymentContext
,便是和Configuration
有关喽。
从名字就能看出来,它是基于Configuration / ConfigurationManager
来实现的。
public class ConfigurationBasedDeploymentContext extends SimpleDeploymentContext {
// 这些属性可以作为key,放在Configuration里,或者系统属性里均可
// 但是,但是:都标记为过期了,不建议这么做了
// 现在推荐使用枚举值来管理
@Deprecated
public static final String DEPLOYMENT_ENVIRONMENT_PROPERTY = "archaius.deployment.environment";
@Deprecated
public static final String DEPLOYMENT_DATACENTER_PROPERTY = "archaius.deployment.datacenter";
...
@Deprecated
public static final String DEPLOYMENT_REGION_PROPERTY = "archaius.deployment.region";
// 唯一构造器
public ConfigurationBasedDeploymentContext() {
AbstractConfiguration config = ConfigurationManager.getConfigInstance();
// 只有defaultConfigDisabled = true不允许默认初始化逻辑(只允许自定义)
// 但是你又没自定义的时候,Confiuration是可能为null的
if (config != null) {
String contextValue = getValueFromConfig(DEPLOYMENT_APPLICATION_ID_PROPERTY);
if (contextValue != null) {
// 会放置两份,key分别为@appId和DEPLOYMENT_APPLICATION_ID_PROPERTY
setApplicationId(contextValue);
}
... // 把所有的key都放进来(均会放置两份)
}
// 添加一个Configuration的监听器ConfigurationListener
config.addConfigurationListener(new ConfigurationListener() {
// 只监听成功后的。EVENT_ADD_PROPERTY和EVENT_SET_PROPERTY两种事件
// 修改还有一种是clear,此处是没有监听的
@Override
public void configurationChanged(ConfigurationEvent event) {
if (event.isBeforeUpdate() || (event.getType() != AbstractConfiguration.EVENT_ADD_PROPERTY && event.getType() != AbstractConfiguration.EVENT_SET_PROPERTY)) {
return;
}
// value不为null,才需要技术处理
String name = event.getPropertyName();
String value = event.getPropertyValue() == null ? null : String.valueOf(event.getPropertyValue());
if (value == null) {
return;
}
// 把改变之后的值,也设置进去
if (name.equals(DEPLOYMENT_ENVIRONMENT_PROPERTY)) {
ConfigurationBasedDeploymentContext.super.setDeploymentRegion(value);
setValueInConfig(ContextKey.environment.getKey(), value);
} { ... }
}
});
}
...
// 获取方法也做了增强
// 1、去Configuration或者System里找key为@environment的值
// 2、1若为null。继续同样地方找key为DEPLOYMENT_ENVIRONMENT_PROPERTY的
// 3、2若为null。super.getDeploymentEnvironment(),再去当前Map里早
@Override
public String getDeploymentEnvironment() {
String value = getValueFromConfig(DeploymentContext.ContextKey.environment.getKey());
if (value != null) {
return value;
} else {
value = getValueFromConfig(DEPLOYMENT_ENVIRONMENT_PROPERTY);
if (value != null) {
return value;
} else {
return super.getDeploymentEnvironment();
}
}
}
...
}
该类逻辑无非在父类基础上增加了对Configuration
和System
的寻找,因此若我们想要设置部署参数,是可以通过两者来做的,推荐你使用Configuration
。
另外,ConfigurationBasedDeploymentContext
是Archaius
默认使用的部署上下文实现,具体代码参考:ConfigurationManager
的static代码块部分。
它继承自PropertyWrapper<T>
,相较于其它子类来说,它是一个功能强大,理解难度颇高的一个使用类,也是和本文主题:复杂部署相关的API。
它具有多个可能的关联值,并根据运行时上下文确定该值,其中可以包括部署上下文、其他属性的值或用户输入的属性,它的Value值用一个JSON表示。
说明:它强依赖于Jackson模块完成操作。若你还不太懂Jackson如何使用,请务必参阅全网最全、最好的Jackson专栏,电梯直达:Jackson专栏
在实地介绍DynamicContextualProperty
之前,先看看DefaultContextualPredicate
的实现,它代表一种Predicate断言逻辑的实现,能告诉你什么叫匹配,什么叫不匹配。
// 这个泛型类型很复杂哦~~~~
public class DefaultContextualPredicate implements Predicate<Map<String, Collection<String>>> {
// 这个字段命名很奇特:它是一个Function,输入的是一个值,输出的被转为另一个值
private final Function<String, String> getValueFromKeyFunction;
public DefaultContextualPredicate(Function<String, String> getValueFromKeyFunction) {
this.getValueFromKeyFunction = getValueFromKeyFunction;
}
// 断言逻辑
@Override
public boolean apply(@Nullable Map<String, Collection<String>> input) {
if (null == input) {
throw new NullPointerException();
}
// 遍历input的每个entry,必须每一个都是true,最终才返回true
for (Map.Entry<String, Collection<String>> entry: input.entrySet()) {
String key = entry.getKey();
Collection<String> value = entry.getValue();
// 也就是说key经过Function处理后,得到的value值必须被原value包含才行
// 比如key处理好后值是"a",而原来的value值是["a","b"],那么就算匹配成功喽
if (!value.contains(getValueFromKeyFunction.apply(key))) {
return false;
}
}
// 必须每一个都是true,最终才返回true
return true;
}
}
从该Predicate
实现能总结出如下匹配逻辑:
Map<String, Collection<String>>
是个组合逻辑,每个entry都为true最终才为truekey
经过Function处理后的值必须被value所包含才算此entry为true@Test
public void fun1() {
// Function解释:过来的name一定叫"YourBatman",age一定是"18"岁
Predicate<Map<String, Collection<String>>> predicate = new DefaultContextualPredicate(key -> {
if(key.equals("name"))
return "YourBatman";
if(key.equals("age"))
return "18";
return null;
});
// 输入:名称必须是这三个中的一个,而年龄必须是16到20岁之间
Map<String, Collection<String>> input = new HashMap<>();
input.put("name", Arrays.asList("Peter","YourBatman","Tiger"));
input.put("age", Arrays.asList("16","17","18","19","20"));
System.out.println(predicate.test(input));
// 输入:名字必须精确的叫Peter
input = new HashMap<>();
input.put("name", Arrays.asList("Peter"));
input.put("age", Arrays.asList("16","17","18","19","20"));
System.out.println(predicate.test(input));
}
运行程序,第一个输出为true
,因为条件都符合。第二个false
,因为名字不符合。
为了方便使用,Archaius
内置了一个深度整合Configuration
实现,拿去直接用便可:
DefaultContextualPredicate:
public static final DefaultContextualPredicate PROPERTY_BASED = new DefaultContextualPredicate(new Function<String, String>() {
@Override
public String apply(@Nullable String input) {
return DynamicProperty.getInstance(input).getString();
}
});
简单解释PROPERTY_BASED
:key处理后输出什么,由Configuration
配置来决定,这样就完美和Configuration
集成在了一起。
// T表示最终getValue返回的实际类型,这里扔不确定,所以可以是任何值
// 因为上下文不同,所以有可能返回任何值
// 它继承自PropertyWrapper,所以它的属性值也是具有动态性的哦~~~~~~~
public class DynamicContextualProperty<T> extends PropertyWrapper<T> {
// 对value的包装,该类属性代表着上下文条件、匹配规则
public static class Value<T> {
// 条件们 匹配规则
private Map<String, Collection<String>> dimensions;
// 最后实际返回的值,是T类型。可以是任意类型,如String、int,设置可以是POJO
private T value;
// 注释
private String comment;
private boolean runtimeEval = false;
// 请注意:这里是if哦~~~
@JsonProperty("if")
public final Map<String, Collection<String>> getDimensions() {
return dimensions;
}
... // 省略其它get/set方法
}
// 判断逻辑你可以自定义:比如你可以自定义为只需要有一个为true就为true
// 但默认情况下使用的就是PROPERTY_BASED喽
private final Predicate<Map<String, Collection<String>>> predicate;
private static Predicate<Map<String, Collection<String>>> defaultPredicate = DefaultContextualPredicate.PROPERTY_BASED;
// JSON字符串,会被返解析为它~~~~
// 这是一个复杂的str -> POJO的反序列化,所以借助Jackson的ObjectMapper来完成的
volatile List<Value<T>> values;
private final ObjectMapper mapper = new ObjectMapper();
// 可以自定义判断逻辑predicate(一般使用默认的即可)
public DynamicContextualProperty(String propName, T defaultValue, Predicate<Map<String, Collection<String>>> predicate) { ... }
// 使用默认的判断逻辑(全都为true才为true,并且和Configuration集成)
public DynamicContextualProperty(String propName, T defaultValue) { ...}
... // 在构造器阶段:把属性值value(是个JSON串),转换为了List<Value<T>> values本地存储着~~~
// 当属性发生改变时,List<Value<T>> values也会跟着变化
@Override
protected final void propertyChanged() {
propertyChangedInternal();
propertyChanged(this.getValue());
}
...
}
以上都是初始化阶段完成的动作:
List<Value<T>> values
放着 Map<String, Collection<String>> dimensions
哦propertyChanged()
方法,所以每当属性变化时,便可重新给 List<Value<T>> values
赋值PROPERTY_BASED
:上下文环境属性从Configuration
里面获取到,从而进行判断准备好了这些能力后,下面就进入到作为一个Property
的核心方法:获取属性值value
DynamicContextualProperty:
@Override
public T getValue() {
if (values != null) {
for (Value<T> v: values) {
if (v.getDimensions() == null || v.getDimensions().isEmpty() || predicate.apply(v.getDimensions())) {
// 只有条件符合,才拿出其实际值
return v.getValue();
}
}
}
return defaultValue;
}
对getValue()
获取步骤做如下描述:
PROPERTY_BASED
匹配成功首先,在config.properties
“配置”好我们的条件(先用JSON美化表示,后写进properties文件里):
JSON美化表示:
[
{
"if":{
"@region":[
"ali"
],
"@environment":[
"prod"
]
},
"value":"YourBatman-ali-prod"
},
{
"if":{
"@region":[
"ten"
],
"@environment":[
"test"
]
},
"value":"YourBatman-ten-test"
},
{
"if":{
"@environment":[
"prod"
],
"@myDiyParam":[
"China"
]
},
"value":"YourBatman-myDiy-pro"
},
{
"value":"YourBatman"
}
]
# 应用名称:根据机房、环境来拼接生成
applicationName=[{"if":{"@region":["ali"],"@environment":["prod"]},"value":"YourBatman-ali-prod"},{"if":{"@region":["ten"],"@environment":["test"]},"value":"YourBatman-ten-test"},{"if":{"@environment":["prod"],"@myDiyParam":["China"]},"value":"YourBatman-myDiy-prod"},{"value":"YourBatman"}]
说明:一般情况下,
com.netflix.config.DeploymentContext.ContextKey
里面的这些key是默认支持的。此处的@myDiyParam
属性自定义变量名~~~(并不要求你一@开头,但遵守规范是个好习惯)
1、一个条件都木有的默认值生效:
@Test
public void fun2(){
DynamicPropertyFactory factory = DynamicPropertyFactory.getInstance();
DynamicContextualProperty<String> contextualProperty = factory.getContextualProperty("applicationName", "defaultName");
System.out.println(contextualProperty.getValue()); // YourBatman
}
2、阿里上的生产环境:
@Test
public void fun3(){
// 通过SimpleDeploymentContext手动设置部署环境参数
SimpleDeploymentContext deploymentContext = new SimpleDeploymentContext();
deploymentContext.setDeploymentRegion("ali");
deploymentContext.setDeploymentEnvironment("prod");
ConfigurationManager.setDeploymentContext(deploymentContext);
DynamicContextualProperty<String> contextualProperty = new DynamicContextualProperty<>("applicationName", "defaultName");
System.out.println(contextualProperty.getValue()); // YourBatman-ali-prod
}
3、腾讯上的测试环境:直接用属性key完成
@Test
public void fun4(){
// 调用一下,让Configuration完成初始化
AbstractConfiguration configInstance = ConfigurationManager.getConfigInstance();
configInstance.addProperty(DeploymentContext.ContextKey.region.getKey(),"ten");
configInstance.addProperty(DeploymentContext.ContextKey.environment.getKey(),"test");
// 效果同上。但推荐用上者
// System.setProperty(DeploymentContext.ContextKey.region.getKey(),"ten");
// System.setProperty(DeploymentContext.ContextKey.environment.getKey(),"test");
DynamicContextualProperty<String> contextualProperty = new DynamicContextualProperty<>("applicationName", "defaultName");
System.out.println(contextualProperty.getValue()); // YourBatman-ten-test
}
4、让自定义的@myDiyParam
条件生效:
@Test
public void fun5() {
System.setProperty(DeploymentContext.ContextKey.environment.getKey(), "prod");
System.setProperty("@myDiyParam", "China");
DynamicContextualProperty<String> contextualProperty = new DynamicContextualProperty<>("applicationName", "defaultName");
System.out.println(contextualProperty.getValue()); // YourBatman-myDiy-prod
}
让自定义的的属性生效也是极简单的有木有,不过据我经验,生产环境建议你不要乱弄,用枚举管理起来较好。
这个特性灵活性非常的强,这对于复杂的云计算环境:多环境、多区域、多机房等等部署,非常非常有用,能够极大的提升系统的弹性,给了架构师更多的想象空间。
如题:Netflix Archaius如何支持多环境、多区域、多数据中心部署?现在你应该能给出你的答案了~
在微服务、容器化技术、云源生越来越流行的今天,多环境部署是作为一名架构师、运维人员必备的技能,而Netflix Archaius
提供了非常灵活的支持,祝你轻松上云、安全上云。