[享學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高工、架構師 系列羣大家庭學習和交流。
往期精選

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章