[享学Netflix] 三十九、Ribbon核心API源码解析:ribbon-core(二)IClientConfig配置详解

任何一个傻瓜都会写让机器理解的代码,只有好的程序员才能写出人类可以理解的代码。

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

前言

配置对于一个程序到底有多重要自然不用多说,每个库均有它自己的配置管理方式,比如Spring有Enviroment抽象等。

本文即将介绍的是Ribbon中一个使用频繁,且非常重要的接口:IClientConfig,它负责Ribbon的配置管理,包括所有默认值的维护,以及提供提供其读写能力。


正文

Ribbon所有的配置均交由IClientConfig统一管理,并提供统一的入口、出口,其它核心组件如IClient、ILoadBalancer均会读取此配置来控制其行为。

弱弱说一句:Ribbon的配置管理依赖于Netflix Archaius库,学习它可跳转到这里:Netflix Archaius全文讲解


IClientConfig

定义各种API用于初始化IClient或者ILoadBalancer的客户端配置,以及方法执行。

public interface IClientConfig {

	// 如account、user...
	public String getClientName();
	// 默认值是ribbon
	public String getNameSpace();
	
	// 加载给定客户端/负载均衡器(名为clinetName)的属性。该方法重要,重要,重要
	// 内部会自动加载默认是,也就是loadDefaultValues()方法
	public void loadProperties(String clientName);
	// 加载配置的默认值们,放进全局的Configuration里面。和clientName无关,公用的
	public void loadDefaultValues();

	@Deprecated
	public void setProperty(IClientConfigKey key, Object value);
    @Deprecated
	public Object getProperty(IClientConfigKey key);
    @Deprecated
	public Object getProperty(IClientConfigKey key, Object defaultVal);
	// 请用这三个带有泛型的方法,代替上面的三个方法  不用强转更安全
	// 入参是IClientConfigKey,这在上文已经详解过
	public <T> IClientConfig set(IClientConfigKey<T> key, T value);
	public <T> T get(IClientConfigKey<T> key);
	public <T> T get(IClientConfigKey<T> key, T defaultValue);
	... // 省略getPropertyAsInteger getPropertyAsString getPropertyAsBoolean方法


	// 配置内是否包含某个key
	public boolean containsProperty(IClientConfigKey key);
	//返回此Client客户端配置使用的适用虚拟地址(“vip”)。
	//会依赖于VipAddressResolver进行解析
	public String resolveDeploymentContextbasedVipAddresses();
}

该接口有唯一实现类DefaultClientConfigImpl,它管理着常用属性key对应的默认值,以及实现所有的接口方法。


DefaultClientConfigImpl

ArchaiusConfigurationManager加载属性的默认客户端配置。也可以通过编程的方式实现IClientConfig的加载。

您可以在classpath文件中定义属性,也可以将其定义为系统属性。如果是前者,那么应该调用ConfigurationManager.loadPropertiesFromResources()API来加载文件到全局配置Configuration里。而绝大数的配置你并不会显示的配置出来,这些默认值便通过DefaultClientConfigImpl来管理:

public class DefaultClientConfigImpl implements IClientConfig {

	// ===================默认值们==================
	public static final Boolean DEFAULT_PRIORITIZE_VIP_ADDRESS_BASED_SERVERS = Boolean.TRUE;
	public static final String DEFAULT_NFLOADBALANCER_PING_CLASSNAME = "com.netflix.loadbalancer.DummyPing";
	// 默认在单台Server上不会重试
    public static final int DEFAULT_MAX_AUTO_RETRIES = 0;
    // 默认这台不行,只会重试下一台(若你有多台机器,其它的就不会被尝试到了)
    public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1;
    public static final int DEFAULT_READ_TIMEOUT = 5000;
    public static final int DEFAULT_CONNECT_TIMEOUT = 2000;
	...
	// 默认的nameSpace值
	public static final String DEFAULT_PROPERTY_NAME_SPACE = "ribbon";
	// all connections idle for 30 secs
	public static final int DEFAULT_CONNECTIONIDLE_TIME_IN_MSECS = 30000; 
}

以上为定义的public static默认值,各默认值和CommonClientConfigKey管理的通用key们一一对应。这种对应关系也是交由DefaultClientConfigImpl来做的,进而把k-v放进全局配置里:

DefaultClientConfigImpl:

	// properties装载全部的属性k-v,注意key是如ConnectTimeout,而非ribbon.ConnectTimeout这种
	protected volatile Map<String, Object> properties = new ConcurrentHashMap<>();
	// dynamicProperties:它相较于properties只会装载支持动态化的的属性k-v
	// 所以可以看到它的v是DynamicStringProperty具有动态性嘛
	private final Map<String, DynamicStringProperty> dynamicProperties = new ConcurrentHashMap<String, DynamicStringProperty>();
	// 该变量无任何作用,感觉像是忘记作者忘记删掉的一个成员变量
	protected Map<IClientConfigKey<?>, Object> typedProperties = new ConcurrentHashMap<>();

	private String clientName = null;
	// 用于解析vipAddress(下有解析)
	private VipAddressResolver resolver = null;
	// 是否允许动态属性,注意:这里值虽然是true
	// 但是参考下面的构造器:构造之处会把此值改为false
	private boolean enableDynamicProperties = true;
	// 默认的名称空间是ribbon,请不要改变它
	// 说明:所有的配置都是和NameSpace名称空间绑定的哦
	private String propertyNameSpace = DEFAULT_PROPERTY_NAME_SPACE;

	... // 省略所有的属性的get方法
    public void setClientName(String clientName){
        this.clientName  = clientName;
    }
    @Override
	public String getClientName() {
        return clientName;
    }

	// =======仅有的两个构造器,需要特别注意,特别注意=======
	// 构造的时候,会把enableDynamicProperties改为false,表示不允许属性值动态化
    public DefaultClientConfigImpl() {
        this.dynamicProperties.clear();
        this.enableDynamicProperties = false;
    }
    public DefaultClientConfigImpl(String nameSpace) {
    	this();
    	this.propertyNameSpace = nameSpace;
    }

当构造一个DefaultClientConfigImpl实例时,enableDynamicProperties会被置为false,也就是不会支持动态属性。实例建好了,但是还没有完成默认值的“装载”,下面继续看看。


属性装载

属性的[配置格式]

任何配置的加载均需要有格式,这在代码里也有所体现:

DefaultClientConfigImpl:

	// 默认值,全局通用的值的格式
    String getDefaultPropName(String propName) {
        return getNameSpace() + "." + propName;
    }
    // 指定clientName的格式
    public String getInstancePropName(String restClientName, String key) {
        return restClientName + "." + getNameSpace() + "." + key;
    }

	// 获取指定propName对应的key,这里体现了优先级
	// 若有clientName就拿指定clientName的,否则去拿全局的
    private String getConfigKey(String propName) {
        return (clientName == null) ? getDefaultPropName(propName) : getInstancePropName(clientName, propName);
    }

它规定了加载的格式为:

// 默认情况下nameSpace=ribbon,并且大概率没人会改它
<clientName>.<nameSpace>.<propertyName>=<value>

这种配置是和clientName绑定的:指定Client客户端专属,大多数时候我们需要全局公用的配置,也就是所谓兜底的默认值,如果某个属性缺少clientName,则将其解释为适用于所有客户端的属性:

<nameSpace>.<propertyName>=<value>

// 如下,它就是所有client共用的属性
ribbon.ReadTimeout=5000

loadDefaultValues()装载默认值

构建DefaultClientConfigImpl时并不会立马加载默认值到全局Configuration配置,而是需要显示被调用的。

DefaultClientConfigImpl:

	// 它是个接口方法
	@Override
    public void loadDefaultValues() {
        putDefaultIntegerProperty(CommonClientConfigKey.MaxHttpConnectionsPerHost, getDefaultMaxHttpConnectionsPerHost());
        putDefaultIntegerProperty(CommonClientConfigKey.MaxTotalHttpConnections, getDefaultMaxTotalHttpConnections());
		...
		putDefaultStringProperty(CommonClientConfigKey.VipAddressResolverClassName, getDefaultVipaddressResolverClassname()); 
		...
		putDefaultStringProperty(CommonClientConfigKey.ListOfServers, "");
	}


	// 1、先去Configuration里找,有没有配置好的全局属性值(比如配置在config.properties里的,或者系统属性里的)
	// 2、若没有就使用defaultValue,就是本类的常量值作为默认值喽
	// 3、放进去setPropertyInternal(propName, value);
    protected void putDefaultStringProperty(IClientConfigKey propName, String defaultValue) {
        String value = ConfigurationManager.getConfigInstance().getString(getDefaultPropName(propName), defaultValue);
        setPropertyInternal(propName, value);
    }
    protected void setPropertyInternal(final String propName, Object value) {
    	// 记录所有的k-v键值对到properties里面
    	// 需要注意的是它的key是propertyName,如ConnectTimeout,而并非是ribbon.ConnectTimeout这种
		String stringValue = (value == null) ? "" : String.valueOf(value);
        properties.put(propName, stringValue);
		// 此处非常关键:非常关键:非常关键
		// 若enableDynamicProperties=false,也就是没有开启动态属性的支持,就return了
		// 也就是不会注册回调函数从而支持动态属性
        if (!enableDynamicProperties) {
            return;
        }

		// ======若开启了动态属性的支持======
		
		// clientName.ribbon.xxx或者ribbon.xxx
		String configKey = getConfigKey(propName);
		// 看看Configuration里是否已经有配置该值
		DynamicStringProperty prop = DynamicPropertyFactory.getInstance().getStringProperty(configKey, null);
		// 动态属性回调:用于同步更新properties里的值
		prop.addCallback(() -> {
			// 注意:这个get方法具有动态性哦~~~~~~~
			String value = prop.get();
            if (value != null) {
                properties.put(propName, value);
            } else {
                properties.remove(propName);
            }
		});
		dynamicProperties.put(propName, prop);
    }

通过调用该方法,实现了把所有的默认k-v均装进了Map<String, Object> properties里面,并且还对属性们开启了动态监测(若enableDynamicProperties=true的话),使得其具有动态性。


loadProperties(clientName)装载指定Client值

在我们开发过程中,大多数时候我们是想得到指定Client它所有拥有的配置。该方法加载给定Client的属性。它首先加载所有属性的默认值,以及任何已经用Archaius ConfigurationManager定义的属性。

DefaultClientConfigImpl:

	// 它是个接口方法
    @Override
	public void loadProperties(String restClientName){
		// 重要:重要:重要:此处是唯一把它改为true的地方
		enableDynamicProperties = true;
		// 顺带也帮你把成员属性clientName赋上值
		setClientName(restClientName);
		// 加载默认值(注意:此时均具有动态性了)
		loadDefaultValues();
		// 此处巧用subset方法,得到一个新的Configuration 
		// 简答的说:就是把所有的以"account."开头的都拿过来(此处架设restClientName=account)
		Configuration props = ConfigurationManager.getConfigInstance().subset(restClientName);
		// 遍历,把Configuration里面的值都放进全局成员属性properties里
		for (Iterator<String> keys = props.getKeys(); keys.hasNext(); ){
			...
			// 如果是ribbon.xxx的话,就切割一下。保证后面部分才是真实的key
			if (prop.startsWith(getNameSpace())){
				prop = prop.substring(getNameSpace().length() + 1);
			}
			// 改方法上文有讲,此处重要的是getStringValue()方法
			setPropertyInternal(prop, getStringValue(props, key));
		}
	}

	// 这是为了解决默认情况下AbstractConfiguration的问题自动将逗号分隔的字符串转换为数组
	// 我们知道在AbstractConfiguration下若你配置的是1,2,3的话,那最终存进去的是数组形式[1,2,3]
	// 而本方法就是给它还原,具体逻辑比较简单,就不再详述了
	protected static String getStringValue(Configuration config, String key) {
		...
	}
  1. 只有调用了该方法,去加载指定client的配置事,enableDynamicProperties才会设为true,让其支持动态属性了
    1. 言外之意:纯默认值管理下,是不支持动态属性的
  2. getStringValue()方法能够保证你配置的value值原滋原味
    1. 比如你配置的是1,2,3,那么保证存储到properties里面的也是1,2,3,而非[1,2,3]
    2. 注意这里处理和putDefaultStringProperty()是有差异的,它依赖的是AbstractConfiguration#getString()这个API,而它只会返回collection.iterator().next(),也就是如果你是逗号分隔的话,只会返回第一个值(这个差异特别重要,所有默认值里千万不要配置逗号分隔的形式,否则就是bug了)
  3. . 请务必确保restClientName不能为null(它程序内没有判断,其实算个小bug),否则抛错

绝大多数情况下,我们并不需要主动调用loadDefaultValues(),而只需使用loadProperties(restClientName)方法即可完成配置的加载。


属性获取

属性完成装载后,获取就比较简单了。

DefaultClientConfigImpl:

	// 接口方法:获取所有的属性k-v
	// 说明:这里返回的是实例本身,并不是副本。所有你是可以向里面添加、删除值的
    @Override
	public Map<String, Object> getProperties() {
        return properties;
    }
    // 虽然它已经被标记为过期:使用get()方法代替,但因为使用的人还不少,所以看看
    @Override
	public Object getProperty(IClientConfigKey key){
        String propName = key.key();
        Object value = getProperty(propName);
        return value;
    }
    protected Object getProperty(String key) {
    	if (enableDynamicProperties) {
    		DynamicStringProperty dynamicProperty = dynamicProperties.get(key);
    		// 先找指定Client的属性
    		dynamicValue = DynamicProperty.getInstance(getConfigKey(key)).getString();
    			// 找不到再去找全局的
    			dynamicValue = DynamicProperty.getInstance(getDefaultPropName(key)).getString();
    	}
    	return properties.get(key);
    }

getProperty()方法的执行逻辑:

  1. 若开启了动态属性enableDynamicProperties=true,那就先去动态属性里找
    1. 先找指定Client自己的配置(clientName.ribbon.xxx)
    2. 没有找到就找全局的配置(ribbon.xxx)
  2. 木有找到,就去全局的properties里找
DefaultClientConfigImpl:

	@Override
    public <T> T get(IClientConfigKey<T> key) {
    	// 同样也依赖于上面说的getProperty()方法哦
    	Object obj = getProperty(key.key());
        if (obj == null) {
            return null;
        }
        Class<T> type = key.type();
    	... // 根据type类型做数据转换,略。这就是带泛型的好处
    }

	// =============当然还有些快捷方法==========
	    @Override
    public int getPropertyAsInteger(IClientConfigKey key, int defaultValue) { ... }
    @Override
    public String getPropertyAsString(IClientConfigKey key, String defaultValue) { ... }
    @Override
    public boolean getPropertyAsBoolean(IClientConfigKey key, boolean defaultValue) { ... }


	// =========判断方法=========
    @Override
	public boolean containsProperty(IClientConfigKey key){
        Object o = getProperty(key);
        return o != null ? true: false;
    }

实例构建

IClientConfig作为最常打交道的一个接口,所以Ribbon也很暖心的给我们提供了多种构建该实例的方式。

示例一:静态方法方式

我们知道IClientConfig的唯一实现类只有DefaultClientConfigImpl,所以它提供了几个静态方法让你便捷构建其实例:

DefaultClientConfigImpl:

	// 空的。因为load加载方法均还没调用,因此里面的properties属性都是空的
	// 此时若你getProperty,得到结果均是null
	public static DefaultClientConfigImpl getEmptyConfig() {
	    return new DefaultClientConfigImpl();
	}
	
	// 带有clientName的DefaultClientConfigImpl实例,推荐使用
	// 它内部调用了config.loadProperties(clientName),所以是个完整的config了
	public static DefaultClientConfigImpl getClientConfigWithDefaultValues(String clientName) {
		return getClientConfigWithDefaultValues(clientName, DEFAULT_PROPERTY_NAME_SPACE);
	}
	// clintName的名字为default,一般不建议使用
	public static DefaultClientConfigImpl getClientConfigWithDefaultValues() {
        return getClientConfigWithDefaultValues("default", DEFAULT_PROPERTY_NAME_SPACE);
    }
    // 自己指定clientName、自己指定nameSpace 一般也用不着
	public static DefaultClientConfigImpl getClientConfigWithDefaultValues(String clientName, String nameSpace) {
	    DefaultClientConfigImpl config = new DefaultClientConfigImpl(nameSpace);
	    config.loadProperties(clientName);
		return config;
	}

此方式是推荐使用的方式,特别是带ClientName的方法,得到的是一个有血有肉的,可以立马提供服务的实例。

示例二:Builder方式

IClientConfig接口内有一个内部类com.netflix.client.config.IClientConfig.Builder即使Builder模式。

IClientConfig:
	
	public static class Builder {
		
		private IClientConfig config;
		
		// 它构建出来的实例也是DefaultClientConfigImpl
        public static Builder newBuilder() {
            Builder builder = new Builder();
            builder.config = new DefaultClientConfigImpl();
            return builder;
        }
        // 推荐使用它而不是上面的空构造
        public static Builder newBuilder(String clientName) {
            Builder builder = new Builder();
            builder.config = new DefaultClientConfigImpl();
            builder.config.loadProperties(clientName);
            return builder;
        }
        // 当然你也可以指定具体的Class实现,只是我们从来不会这么干.....
        public static Builder newBuilder(Class<? extends IClientConfig> implClass, String clientName) {
            Builder builder = new Builder();
            try {
                builder.config = implClass.newInstance();
                builder.config.loadProperties(clientName);
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            }
            return builder;
        }
        ...
        // =======提供一些快捷设值方法=======
        public Builder withMaxAutoRetries(int value) {
            config.set(CommonClientConfigKey.MaxAutoRetries, value);
            return this;
        }
        ...
        public Builder withConnectTimeout(int value) {
            config.set(CommonClientConfigKey.ConnectTimeout, value);
            return this;
        }
		...
        public IClientConfig build() {
            return config;
        }
	}

该builder在稍微复杂点的构建过程中还是比较好用的。

示例三:new方式

不说了,就是使用构造器进行new。需要特别注意的是:刚new出来的IClientConfig实例,enableDynamicProperties是为false的哦。


代码示例

public void fun5() {
    // 静态方法构建
    IClientConfig config = DefaultClientConfigImpl.getClientConfigWithDefaultValues("YourBatman");
    System.out.println(config.getClientName());
    System.out.println(config.get(CommonClientConfigKey.ConnectTimeout));
    System.out.println("-----------------------");
    // Builder构建
    config = IClientConfig.Builder.newBuilder("YourBatman")
            .withConnectTimeout(8000)
            .withReadTimeout(10000)
            .build();
    System.out.println(config.getClientName());
    System.out.println(config.get(CommonClientConfigKey.ConnectTimeout));
    System.out.println("-----------------------");

    // new方式构建
    config = new DefaultClientConfigImpl();
    System.out.println("load前:");
    System.out.println(config.getClientName());
    System.out.println(config.get(CommonClientConfigKey.ConnectTimeout));
	
	config.loadProperties("YourBatman");
    System.out.println("load后:");
    System.out.println(config.getClientName());
    System.out.println(config.get(CommonClientConfigKey.ConnectTimeout));
}

运行程序,打印:

YourBatman
2000
-----------------------
YourBatman
8000
-----------------------
load前:
null
null
load后:
YourBatman
2000

总结

关于Ribbon的配置管理接口IClientConfig就介绍到这了,本篇内容比较重要,但是并不难,希望读者能够学有所获。
分隔线

声明

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

发布了377 篇原创文章 · 获赞 572 · 访问量 51万+
App 阅读领勋章
微信扫码 下载APP
阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie

分享到微信朋友圈

×

扫一扫,手机浏览