Eureka詳解系列(三)--探索Eureka強大的配置體系

子月生發表於2021-02-04

簡介

通過前面的兩篇部落格,我們知道了:什麼是 Eureka?為什麼使用 Eureka?如何適用 Eureka?今天,我們開始來研究 Eureka 的原始碼,先從配置部分的原始碼開始看,其他部分後面再補充。

補充一點,我更多地會從設計層面分析原始碼,而不會順序地剖析每個過程的程式碼。一方面是因為篇幅有限,另一方面是因為我認為這樣做更有意義一些。

專案環境

os:win 10

jdk:1.8.0_231

eureka:1.10.11

maven:3.6.3

從一個例子開始

ConcurrentCompositeConfiguration這個類是 Eureka 配置體系的核心。在這個例子中,我們使用它對 property 進行增刪改查,並註冊了自定義監聽器來監聽 property 的改變

    @Test
    public void test01() {
        // 建立配置物件
        final ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration(); 
        // 註冊監聽器監聽property的改變
        config.addConfigurationListener(new ConfigurationListener() {
            
            public void configurationChanged(ConfigurationEvent event) {
                // 增加property
                if(AbstractConfiguration.EVENT_ADD_PROPERTY == event.getType() 
                        && !event.isBeforeUpdate()) {
                    System.err.println("add property:" + event.getPropertyName() + "=" + event.getPropertyValue());
                    return;
                }
                // 刪除property
                if(AbstractConfiguration.EVENT_CLEAR_PROPERTY == event.getType()) {
                    System.err.println("clear property:" + event.getPropertyName());
                    return;
                }
                // 更新property
                if(AbstractConfiguration.EVENT_SET_PROPERTY == event.getType() 
                        && event.isBeforeUpdate()
                        && !config.getString(event.getPropertyName()).equals(event.getPropertyValue())) {
                    System.err.println("update property:" 
                    + event.getPropertyName() 
                    + ":" 
                    + config.getString(event.getPropertyName())
                    + "==>"
                    + event.getPropertyValue()
                    );
                    return;
                }
            }
        });
        // 新增property
        config.addProperty("author", "zzs");
        // 獲取property
        System.err.println(config.getString("author"));
        // 更改property
        config.setProperty("author", "zzf");
        // 刪除property
        config.clearProperty("author");
    }
//    執行以上方法,控制檯列印內容:
//    add property:author=zzs
//    zzs
//    update property:author:zzs==>zzf
//    clear property:author

可以看到,當我們更改了 property 時,監聽器中的方法被觸發了,利用這一點,我們可以實現動態配置。

後面就會發現,Eureka 底層使用ConcurrentCompositeConfiguration來對配置引數進行增刪改查,並基於事件監聽的機制來支援動態配置

另一個有意思的地方

我們再來看看一個 UML 圖。上面例子中說到ConcurrentCompositeConfiguration的兩個功能,是通過實現Configuration和繼承EventSource來獲得的,這一點沒什麼特別的,之所以深究它,是因為我發現了其他有趣的地方。

zzs_eureka_15

我們主要來關注下它的三個成員屬性(它們都是AbstractConfiguration型別):

  1. configList:持有的配置物件集合。這個集合的配置物件存在優先順序,舉個例子,如果我新增了 Configuration1 和 Configuration2,當我們getProperty(String)時,會優先從 Configuration1 獲取,實在找不到才會去 Configuration2 獲取。
  2. overrideProperties最高優先順序的配置物件。當我們getProperty(String)時,會先從這裡獲取,實在沒有才會去 configList 裡找。
  3. containerConfiguration保底的配置物件。一般是 configList 的最後一個(注意,不一定是最後一個1),我們往ConcurrentCompositeConfiguration裡增刪改 property,實際操作的就是這個物件。

為了更好理解它們的作用,我寫了個測試例子。

    @Test
    public void test02() {
        // 建立配置物件
        ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration(); 
        // 新增配置1
        ConcurrentMapConfiguration config1 = new ConcurrentMapConfiguration();
        config1.addProperty("author", "zzs");
        config.addConfiguration(config1, "CONFIG_01");
        
        // 新增配置2
        ConcurrentMapConfiguration config2 = new ConcurrentMapConfiguration();
        config2.addProperty("author", "zzf");
        config.addConfiguration(config2, "CONFIG_02");
        
        // 在預設的containerConfiguration中新增property
        config.addProperty("author", "zhw");
        
        // ============以下測試configList的優先順序============
        System.err.println(config.getString("author"));
        // 刪除config1中的property
        config1.clearProperty("author");
        System.err.println(config.getString("author"));
        // 刪除config2中的property
        config2.clearProperty("author");
        System.err.println(config.getString("author"));
        
        // ============以下測試overrideProperties的優先順序============
        // 新增overrideProperties的property
        config.setOverrideProperty("author", "lt");
        System.err.println(config.getString("author"));
    }
//    執行以上方法,控制檯列印內容:
//    zzs
//    zzf
//    zhw
//    lt

這裡補充一點,當我們建立ConcurrentCompositeConfiguration時,就會生成一個 containerConfiguration,預設情況下,它會一直在集合最後面,每次新增新的配置物件,都是往 containerConfiguration 前面插入。

誰來載入配置

通過上面的例子可以知道,ConcurrentCompositeConfiguration並不會主動地去載入配置,所以,Eureka 需要自己往ConcurrentCompositeConfiguration裡新增配置,而完成這件事的是另外一個類--ConfigurationManager

zzs_eureka_16

ConfigurationManager作為一個單例物件使用,用來初始化配置物件,以及提供載入配置檔案的方法(後面的DefaultEurekaClientConfigDefaultEurekaServerConfig會來呼叫這些方法)。

下面我們看看配置物件的初始化。在ConfigurationManager被載入時就會初始化配置物件,進入到它的靜態程式碼塊就可以找到。我擷取的是最關鍵部分的程式碼。

    private static AbstractConfiguration createDefaultConfigInstance() {
        ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration();  
        try {
            // 載入指定url的配置
            // 通過archaius.configurationSource.additionalUrls啟動引數設定url,多個逗號隔開
            DynamicURLConfiguration defaultURLConfig = new DynamicURLConfiguration();
            config.addConfiguration(defaultURLConfig, URL_CONFIG_NAME);
        } catch (Throwable e) {
            logger.warn("Failed to create default dynamic configuration", e);
        }
        if (!Boolean.getBoolean(DISABLE_DEFAULT_SYS_CONFIG)) {
            // 載入System.getProperties()的配置
            // 通過archaius.dynamicProperty.disableSystemConfig啟動引數可以控制是否新增
            SystemConfiguration sysConfig = new SystemConfiguration();
            config.addConfiguration(sysConfig, SYS_CONFIG_NAME);
        }
        if (!Boolean.getBoolean(DISABLE_DEFAULT_ENV_CONFIG)) {
            // 載入System.getenv()的配置
            // 通過archaius.dynamicProperty.disableEnvironmentConfig啟動引數可以控制是否新增
            EnvironmentConfiguration envConfig = new EnvironmentConfiguration();
            config.addConfiguration(envConfig, ENV_CONFIG_NAME);
        }
        // 這個是自定義的保底配置
        ConcurrentCompositeConfiguration appOverrideConfig = new ConcurrentCompositeConfiguration();
        config.addConfiguration(appOverrideConfig, APPLICATION_PROPERTIES);
        config.setContainerConfigurationIndex(config.getIndexOfConfiguration(appOverrideConfig));// 這裡可以更改保底配置
        return config;
    }

可以看到,Eureka 支援通過 url 來指定配置檔案,只要指定啟動引數就行,這一點將有利於我們更靈活地對專案進行配置。預設情況下,它還會去載入所有的系統引數和環境引數。

另外,當我們設定以下啟動引數,就可以通過 JMX 的方式來更改配置。

-Darchaius.dynamicPropertyFactory.registerConfigWithJMX=true

配置物件初始化後,ConfigurationManager提供了方法供我們載入配置檔案(本地或遠端),如下。

// 這兩個的區別在於:前者會生成一個新的配置新增到configList;後者直接將property都加入到appOverrideConfig
public static void loadCascadedPropertiesFromResources(String configName) throws IOException;
public static void loadAppOverrideProperties(String appConfigName);

怎麼拿到最新的引數

動態配置的內容直接看原始碼不大好理解,我們先通過一個再簡單不過的例子開始來一步步實現我們自己的動態配置。在下面的方法中,我更改了 property,但是拿不到更新的值。原因嘛,我相信大家都知道。

    @Test
    public void test03() {
        // 獲取配置物件
        AbstractConfiguration config = ConfigurationManager.getConfigInstance();
        // 新增一個property
        config.addProperty("author", "zzs");
        
        String author = config.getString("author", "");
        
        System.err.println(author);
        
        // 更改property
        config.setProperty("author", "zzf");
        
        System.err.println(author);
    }
//    執行以上方法,控制檯列印內容:
//    zzs
//    zzs

為了拿到更新的值,我把程式碼改成這樣。我不定義變數來存放 property 的值,每次都重新獲取。顯然,這樣做可以成功。

    @Test
    public void test04() {
        // 獲取配置物件
        AbstractConfiguration config = ConfigurationManager.getConfigInstance();
        // 新增一個property
        config.addProperty("author", "zzs");
        
        System.err.println(config.getString("author", ""));
        
        // 更改property
        config.setProperty("author", "zzf");
        
        System.err.println(config.getString("author", ""));
    }
//    執行以上方法,控制檯列印內容:
//    zzs
//    zzf

但是上面的做法有個問題,我們都知道從ConcurrentCompositeConfiguration中獲取 property 是比較麻煩的,因為我需要去遍歷 configList,以及進行引數的轉換等。每次都這樣拿,不大合理。

於是,我增加了快取來減少這部分的開銷,當然,property 更改時我必須重新整理快取。

    @Test
    public void test05() {
        // 快取
        Map<String, String> cache = new ConcurrentHashMap<String, String>();
        // 獲取配置物件
        AbstractConfiguration config = ConfigurationManager.getConfigInstance();
        // 新增一個property
        config.addProperty("author", "zzs");
        
        String value = cache.computeIfAbsent("author", x -> config.getString(x, ""));
        System.err.println(value);
        
        // 新增監聽器監聽property的更改
        config.addConfigurationListener(new ConfigurationListener() {
            public void configurationChanged(ConfigurationEvent event) {
                // 刪除property
                if(AbstractConfiguration.EVENT_CLEAR_PROPERTY == event.getType()) {
                    cache.remove(event.getPropertyName());
                    return;
                }
                // 更新property
                if(AbstractConfiguration.EVENT_SET_PROPERTY == event.getType() 
                        && !event.isBeforeUpdate()) {
                    cache.put(event.getPropertyName(), String.valueOf(event.getPropertyValue()));
                    return;
                }
            }
        });
        
        // 更改property
        config.setProperty("author", "zzf");
        
        System.err.println(cache.get("author"));
    }
//    執行以上方法,控制檯列印內容:
//    zzs
//    zzf

通過上面的例子,我們實現了動態配置。

現在我們再來看看 Eureka 是怎麼實現的。這裡用到了DynamicPropertyFactoryDynamicStringProperty兩個類,通過它們,也實現了動態配置。

    @Test
    public void test06() {
        // 獲取配置物件
        AbstractConfiguration config = ConfigurationManager.getConfigInstance();
        // 新增一個property
        config.addProperty("author", "zzs");
        
        // 通過DynamicPropertyFactory獲取property
        DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance();
        DynamicStringProperty stringProperty = dynamicPropertyFactory.getStringProperty("author", "");
        
        System.err.println(stringProperty.get());
        
        // 更改property
        config.setProperty("author", "zzf");
        
        System.err.println(stringProperty.get());
    }
//    執行以上方法,控制檯列印內容:
//    zzs
//    zzf

至於原理,其實和我們上面的例子是差不多的。通過 UML 圖可以知道,DynamicProperty中就放了一張快取表,每次獲取 property 時,會優先從這裡拿。

zzs_eureka_17

既然有快取,就應該有監聽器,沒錯,在DynamicProperty.initialize(DynamicPropertySupport)方法中就可以看到。

    static synchronized void initialize(DynamicPropertySupport config) {
        dynamicPropertySupportImpl = config;
        // 註冊監聽器
        config.addConfigurationListener(new DynamicPropertyListener());
        updateAllProperties();
    }

Eureka有那幾類配置

在上面的分析中,我們用ConfigurationManager來初始化配置物件,並使用DynamicPropertyFactory來實現動態配置,這些東西構成了 Eureka 的配置體系的基礎,比較通用。基礎之上,是 Eureka 更具體的一些配置物件。

在 Eureka 裡,配置分成了三種(理解這一點非常重要):

  1. EurekaInstanceConfig:當前例項身份的配置資訊,即我是誰?
  2. EurekaServerConfig:一些影響當前Eureka Server和客戶端或對等節點互動行為的配置資訊,即怎麼互動?
  3. EurekaClientConfig:一些影響當前例項和Eureka Server互動行為的配置資訊,即和誰互動?怎麼互動?

這三個物件都持有了DynamicPropertyFactory的引用,所以支援動態配置,另外,它們還是用ConfigurationManager來載入自己想要的配置檔案。例如,EurekaInstanceConfigEurekaClientConfig負責載入eureka-client.properties,而EurekaServerConfig則負責載入eureka-server.properties

zzs_eureka_18

以上基本講完 Eureka 配置體系的原始碼,可以看到,這是一套非常優秀的配置體系,實際專案中可以參考借鑑。

最後,感謝閱讀。

參考資料

相關原始碼請移步:https://github.com/ZhangZiSheng001/eureka-demo

本文為原創文章,轉載請附上原文出處連結:https://www.cnblogs.com/ZhangZiSheng001/p/14374005.html

相關文章