菜鳥學源碼之Nacos v1.1.3源碼學習-Client模塊(2):NacosConfigService

上一篇博客我們基於Nacos源碼中的example模塊裏的app類學習了NacosNamingService相關的內容:
https://blog.csdn.net/crystonesc/article/details/100530292
接下來我們繼續以ConfigExample爲入口,學習下Client模塊中關於配置相關的類。首先把ConfigExample代碼貼出來:

public class ConfigExample {
    public static void main(String[] args) throws NacosException, InterruptedException {
        String serverAddr = "192.168.171.6";
        String dataId = "test";
        String group = "DEFAULT_GROUP";
        Properties properties = new Properties();
        properties.put("serverAddr", serverAddr);
        //從工廠方法獲取ConfigService
        ConfigService configService = NacosFactory.createConfigService(properties);
        String content = configService.getConfig(dataId, group, 5000);
        System.out.println("first:"+content);
        //添加配置監聽
        configService.addListener(dataId, group, new Listener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                System.out.println("receive:" + configInfo);
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        });

        //發佈配置
        boolean isPublishOk = configService.publishConfig(dataId, group, "content");
        System.out.println(isPublishOk);

        //獲取配置
        Thread.sleep(3000);
        content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);

        //刪除配置
        boolean isRemoveOk = configService.removeConfig(dataId, group);
        System.out.println(isRemoveOk);
        Thread.sleep(3000);

        //再次獲取配置
        content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);
        Thread.sleep(300000);
    }
}

1.首先需要配置Properties,主要設置服務器地址、dataId、group,其中dataId在Nacos中指的是配置集,由一些相關或非相關的配置組成,對應帶應用程序裏面就是一個配置文件。group則是分組名稱,不同的分組名稱可以有相同的dataId,這裏默認指定的是DEFAULT_GROUP。
2.從NacosFactory獲取ConfigService,這裏使用的方式和獲取NamingService類似,NacosFactory提供了獲取ConfigService和NamingService的方法,但是具體的獲取辦法則是通過NamingFactory和ConfigFactory來實現,如下方代碼所示,這裏截取了NacosFactory中的部分方法。

    public static ConfigService createConfigService(Properties properties) throws NacosException {
        return ConfigFactory.createConfigService(properties);
    }
    public static ConfigService createConfigService(String serverAddr) throws NacosException {
        return ConfigFactory.createConfigService(serverAddr);
    }

    public static NamingService createNamingService(String serverAddr) throws NacosException {
        return NamingFactory.createNamingService(serverAddr);
    }

    public static NamingService createNamingService(Properties properties) throws NacosException {
        return NamingFactory.createNamingService(properties);
    }

3.我們這裏具體看下ConfigFactory的createConfigService方法,如下所示,其通過類名創建了ConfigService的子類,NacosConfigService,並通過properties初始化了ConfigService。接下來我們繼續看下NacosConfigService的初始化方法.

public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            ConfigService vendorImpl = (ConfigService)constructor.newInstance(properties);
            return vendorImpl;
        } catch (Throwable var4) {
            throw new NacosException(-400, var4);
        }
    }

4.NacosConfigService是在client模塊當中,類名爲:com.alibaba.nacos.client.config.NacosConfigService,我們先來看看其構造函數,首先獲取用戶是否配置了編碼,如果未配置則採用默認的UTF-8,然後通過initNamespace方法初始化namespace,其初始化方法類似於NamingService中namespace的初始化方法,這裏不再贅述。接下來實例化了MetricsHttpAgent類,該類持有ServerHttpAgent類的實例,ServerHttpAgent主要實現與Nacos服務器之間的通信,包括配置發佈、刪除等等,而MetricsHttpAgent則是對ServerHttpAgent的一次包裝,實現了promethues的監控指標的上報。在早期Nacos的版本中,未有MetricsHttpAgent,代碼在這裏直接實例化了ServerHttpAgent。接着調用了agent的start方法,用於獲取服務器端IP地址,如果服務器端進行了擴縮容,這裏可以實時通知客戶端。實際上start方法類是由ServerListManager來進行服務器地址的管控,後面會詳細分析。最後實例化了ClientWorker來實現longpolling的配置更新任務,如果客戶端訂閱了某個配置,就會有ClientWorker來獲取最新的配置更新,並通知響應的監聽類。
完成構造函數的分析,我們可以繼續看下NacosConfigService還提供了哪些方法。

public NacosConfigService(Properties properties) throws NacosException {
        //獲取編碼配置
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
        if (StringUtils.isBlank(encodeTmp)) {
            encode = Constants.ENCODE;
        } else {
            encode = encodeTmp.trim();
        }
        //初始化Namespace
        initNamespace(properties);
        //包裝了ServerHttpAgent實現監控和Nacos服務器的操作,例如發佈、刪除配置等
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        //start方法適用於獲取Nacos服務器列表的
        agent.start();
        //實現longpolling的配置更新任務
        worker = new ClientWorker(agent, configFilterChainManager, properties);
    }

5.NacosConfigService提供的其它方法,下面在方法上面備註了下方法的大體作用,這裏不針對每個方法進行詳細敘述,而是通過流程貫穿中來帶出每個方法。

 //初始化Namespace
    private void initNamespace(Properties properties) {
        String namespaceTmp = null;

        String isUseCloudNamespaceParsing =
            properties.getProperty(PropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING,
                System.getProperty(SystemPropertyKeyConst.IS_USE_CLOUD_NAMESPACE_PARSING,
                    String.valueOf(Constants.DEFAULT_USE_CLOUD_NAMESPACE_PARSING)));

        if (Boolean.valueOf(isUseCloudNamespaceParsing)) {
            namespaceTmp = TemplateUtils.stringBlankAndThenExecute(namespaceTmp, new Callable<String>() {
                @Override
                public String call() {
                    return TenantUtil.getUserTenantForAcm();
                }
            });

            namespaceTmp = TemplateUtils.stringBlankAndThenExecute(namespaceTmp, new Callable<String>() {
                @Override
                public String call() {
                    String namespace = System.getenv(PropertyKeyConst.SystemEnv.ALIBABA_ALIWARE_NAMESPACE);
                    return StringUtils.isNotBlank(namespace) ? namespace : EMPTY;
                }
            });
        }

        if (StringUtils.isBlank(namespaceTmp)) {
            namespaceTmp = properties.getProperty(PropertyKeyConst.NAMESPACE);
        }
        namespace = StringUtils.isNotBlank(namespaceTmp) ? namespaceTmp.trim() : EMPTY;
        properties.put(PropertyKeyConst.NAMESPACE, namespace);
    }

    //獲取配置
    @Override
    public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
        return getConfigInner(namespace, dataId, group, timeoutMs);
    }

    //獲取配置並註冊監聽器
    @Override
    public String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener) throws NacosException {
        String content = getConfig(dataId, group, timeoutMs);
        worker.addTenantListenersWithContent(dataId, group, content, Arrays.asList(listener));
        return content;
    }

    //向Worker註冊監聽器
    @Override
    public void addListener(String dataId, String group, Listener listener) throws NacosException {
        worker.addTenantListeners(dataId, group, Arrays.asList(listener));
    }

    //發佈配置
    @Override
    public boolean publishConfig(String dataId, String group, String content) throws NacosException {
        return publishConfigInner(namespace, dataId, group, null, null, null, content);
    }

    //移除發佈的配置
    @Override
    public boolean removeConfig(String dataId, String group) throws NacosException {
        return removeConfigInner(namespace, dataId, group, null);
    }

    //移除Worker的監聽器
    @Override
    public void removeListener(String dataId, String group, Listener listener) {
        worker.removeTenantListener(dataId, group, listener);
    }

    //獲取配置
    private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
        group = null2defaultGroup(group);
        //校驗dataId和group
        ParamUtils.checkKeyParam(dataId, group);
        //Config響應體
        ConfigResponse cr = new ConfigResponse();

        cr.setDataId(dataId);
        cr.setTenant(tenant);
        cr.setGroup(group);

        // 優先使用本地配置
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        if (content != null) {
            LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
                dataId, group, tenant, ContentUtils.truncateContent(content));
            cr.setContent(content);
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();
            return content;
        }

        //通過worker去獲取配置
        try {
            content = worker.getServerConfig(dataId, group, tenant, timeoutMs);

            cr.setContent(content);

            //進行過濾
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();

            return content;
        } catch (NacosException ioe) {
            if (NacosException.NO_RIGHT == ioe.getErrCode()) {
                throw ioe;
            }
            LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",
                agent.getName(), dataId, group, tenant, ioe.toString());
        }

        //獲取快照?
        LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
            dataId, group, tenant, ContentUtils.truncateContent(content));
        content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
        cr.setContent(content);
        configFilterChainManager.doFilter(null, cr);
        content = cr.getContent();
        return content;
    }

    private String null2defaultGroup(String group) {
        return (null == group) ? Constants.DEFAULT_GROUP : group.trim();
    }

    //移除配置服務
    private boolean removeConfigInner(String tenant, String dataId, String group, String tag) throws NacosException {
        group = null2defaultGroup(group);
        ParamUtils.checkKeyParam(dataId, group);
        String url = Constants.CONFIG_CONTROLLER_PATH;
        List<String> params = new ArrayList<String>();
        params.add("dataId");
        params.add(dataId);
        params.add("group");
        params.add(group);
        if (StringUtils.isNotEmpty(tenant)) {
            params.add("tenant");
            params.add(tenant);
        }
        if (StringUtils.isNotEmpty(tag)) {
            params.add("tag");
            params.add(tag);
        }
        HttpResult result = null;
        try {
            result = agent.httpDelete(url, null, params, encode, POST_TIMEOUT);
        } catch (IOException ioe) {
            LOGGER.warn("[remove] error, " + dataId + ", " + group + ", " + tenant + ", msg: " + ioe.toString());
            return false;
        }

        if (HttpURLConnection.HTTP_OK == result.code) {
            LOGGER.info("[{}] [remove] ok, dataId={}, group={}, tenant={}", agent.getName(), dataId, group, tenant);
            return true;
        } else if (HttpURLConnection.HTTP_FORBIDDEN == result.code) {
            LOGGER.warn("[{}] [remove] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(), dataId,
                group, tenant, result.code, result.content);
            throw new NacosException(result.code, result.content);
        } else {
            LOGGER.warn("[{}] [remove] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(), dataId,
                group, tenant, result.code, result.content);
            return false;
        }
    }

    //發佈配置服務內部方法
    private boolean publishConfigInner(String tenant, String dataId, String group, String tag, String appName,
                                       String betaIps, String content) throws NacosException {
        //檢查是否設置了分組
        group = null2defaultGroup(group);
        //檢查dataId,group,content的合法性
        ParamUtils.checkParam(dataId, group, content);

        ConfigRequest cr = new ConfigRequest();
        cr.setDataId(dataId);
        cr.setTenant(tenant);
        cr.setGroup(group);
        cr.setContent(content);
        //對content的內容進行過濾,過濾的後的內容重新寫入cr的content
        configFilterChainManager.doFilter(cr, null);
        content = cr.getContent();

        String url = Constants.CONFIG_CONTROLLER_PATH;
        List<String> params = new ArrayList<String>();
        params.add("dataId");
        params.add(dataId);
        params.add("group");
        params.add(group);
        params.add("content");
        params.add(content);
        if (StringUtils.isNotEmpty(tenant)) {
            params.add("tenant");
            params.add(tenant);
        }
        if (StringUtils.isNotEmpty(appName)) {
            params.add("appName");
            params.add(appName);
        }
        if (StringUtils.isNotEmpty(tag)) {
            params.add("tag");
            params.add(tag);
        }

        List<String> headers = new ArrayList<String>();
        if (StringUtils.isNotEmpty(betaIps)) {
            headers.add("betaIps");
            headers.add(betaIps);
        }

        //調用agent的httpPost請求服務器進行配置發佈
        HttpResult result = null;
        try {
            result = agent.httpPost(url, headers, params, encode, POST_TIMEOUT);
        } catch (IOException ioe) {
            LOGGER.warn("[{}] [publish-single] exception, dataId={}, group={}, msg={}", agent.getName(), dataId,
                group, ioe.toString());
            return false;
        }

        if (HttpURLConnection.HTTP_OK == result.code) {
            LOGGER.info("[{}] [publish-single] ok, dataId={}, group={}, tenant={}, config={}", agent.getName(), dataId,
                group, tenant, ContentUtils.truncateContent(content));
            return true;
        } else if (HttpURLConnection.HTTP_FORBIDDEN == result.code) {
            LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(),
                dataId, group, tenant, result.code, result.content);
            throw new NacosException(result.code, result.content);
        } else {
            LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(),
                dataId, group, tenant, result.code, result.content);
            return false;
        }

    }

    @Override
    public String getServerStatus() {
        if (worker.isHealthServer()) {
            return "UP";
        } else {
            return "DOWN";
        }
    }

6.現在回到最開始的ConfigExample中,configService.getConfig(dataId, group, 5000),這裏表示獲取dataId和groupe的配置,同時超時爲5秒。這裏會調用NacosConfigService中getConfig方法,getConfig方法中,會通過調用對象內部的getConfigInner方法來執行實際的操作,getConfigInner會帶上NacosConfigService中配置的namespace,這樣用戶就不用每次都指定namespace了。

//獲取配置
    @Override
    public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
        return getConfigInner(namespace, dataId, group, timeoutMs);
    }

7.getConfigInner方法內容如下,首先其會做一些參數校驗,並構建通過ConfigResponse構建響應體,響應的內容包括dataId,tenant,group以及content(配置內容),tenant則就是用戶的namespace,接着Nacos會優先從本地獲取配置,獲取的方法是LocalConfigInfoProcessor.getFailover,這樣的做法是爲了在無法和服務器通信的情況下獲取本地配置數據,若本地沒有配置文件,則通過ClientWorker的getServerConfig方法去服務器側獲取配置。

//獲取配置
    private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
        group = null2defaultGroup(group);
        //校驗dataId和group
        ParamUtils.checkKeyParam(dataId, group);
        //Config響應體
        ConfigResponse cr = new ConfigResponse();

        cr.setDataId(dataId);
        cr.setTenant(tenant);
        cr.setGroup(group);

        // 優先使用本地配置
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        if (content != null) {
            LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
                dataId, group, tenant, ContentUtils.truncateContent(content));
            cr.setContent(content);
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();
            return content;
        }

        //通過worker去獲取配置
        try {
            content = worker.getServerConfig(dataId, group, tenant, timeoutMs);

            cr.setContent(content);

            //進行過濾
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();

            return content;
        } catch (NacosException ioe) {
            if (NacosException.NO_RIGHT == ioe.getErrCode()) {
                throw ioe;
            }
            LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",
                agent.getName(), dataId, group, tenant, ioe.toString());
        }

        //獲取快照
        LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
            dataId, group, tenant, ContentUtils.truncateContent(content));
        content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
        cr.setContent(content);
        configFilterChainManager.doFilter(null, cr);
        content = cr.getContent();
        return content;
    }

8.接下來我們再繼續查看ConfigExample後面的內容,這裏通過configService添加了一個監聽器,用於在配置發生改變後進行響應的操作。下面我們看下configService.addListener都做了什麼。

  //添加配置監聽
 configService.addListener(dataId, group, new Listener() {
     @Override
     public void receiveConfigInfo(String configInfo) {
         System.out.println("receive:" + configInfo);
     }

     @Override
     public Executor getExecutor() {
         return null;
     }
 });

9.NacosConfigService的addListener方法直接調用了ClientWorker中的addTenantListeners方法,我們再來看下addTenantListeners方法,這個方法主要完成兩件事,第一件事是向ClientWorker中的CacheData添加信息,其次是將Listener添加到cache當中,下面我們稍微分析下CacheData這個數據結構.

//向Worker註冊監聽器
    @Override
    public void addListener(String dataId, String group, Listener listener) throws NacosException {
        worker.addTenantListeners(dataId, group, Arrays.asList(listener));
    }

//添加租戶的監聽者
    public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners) throws NacosException {
        group = null2defaultGroup(group);
        String tenant = agent.getTenant();
        CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
        for (Listener listener : listeners) {
            cache.addListener(listener);
        }
    }

10.CacheData數據結構:
CacheData主要包含name,dataId,group,tenant,listeners,configFilterChainManager,md5屬性,可見一個CacheData主要對應的是一個配置集(配置文件),其中listeners這裏主要使用ManagerListenerWrap,這個CacheData的內部類,主要爲listeners附加了MD5的屬性,當配置更新後,Nacos會把配置最新的MD5值覆蓋上一次的MD5值,同時當發現Listener的MD5值與CacheData本生MD5值不同使,則會觸發配置更新的操作,下面使上述邏輯的代碼.

//查看Listeners的MD5值與CacheData的MD5值是否相同,不同的化,觸發通知
    void checkListenerMd5() {
        for (ManagerListenerWrap wrap : listeners) {
            if (!md5.equals(wrap.lastCallMd5)) {
                safeNotifyListener(dataId, group, content, md5, wrap);
            }
        }
    }

11.好了,我們繼續查看ConfigExample後面的內容,這裏使發佈配置操作,直接調用NacosConfigService的publish方法,如下面方法所示,最後在publishConfigInner方法中通過MetricHttpAgent完成方法的發佈。

//發佈配置
      boolean isPublishOk = configService.publishConfig(dataId, group, "content");
       System.out.println(isPublishOk);


//發佈配置
    @Override
    public boolean publishConfig(String dataId, String group, String content) throws NacosException {
        return publishConfigInner(namespace, dataId, group, null, null, null, content);
    }

//發佈配置服務內部方法
    private boolean publishConfigInner(String tenant, String dataId, String group, String tag, String appName,
                                       String betaIps, String content) throws NacosException {
        //檢查是否設置了分組
        group = null2defaultGroup(group);
        //檢查dataId,group,content的合法性
        ParamUtils.checkParam(dataId, group, content);

        ConfigRequest cr = new ConfigRequest();
        cr.setDataId(dataId);
        cr.setTenant(tenant);
        cr.setGroup(group);
        cr.setContent(content);
        //對content的內容進行過濾,過濾的後的內容重新寫入cr的content
        configFilterChainManager.doFilter(cr, null);
        content = cr.getContent();

        String url = Constants.CONFIG_CONTROLLER_PATH;
        List<String> params = new ArrayList<String>();
        params.add("dataId");
        params.add(dataId);
        params.add("group");
        params.add(group);
        params.add("content");
        params.add(content);
        if (StringUtils.isNotEmpty(tenant)) {
            params.add("tenant");
            params.add(tenant);
        }
        if (StringUtils.isNotEmpty(appName)) {
            params.add("appName");
            params.add(appName);
        }
        if (StringUtils.isNotEmpty(tag)) {
            params.add("tag");
            params.add(tag);
        }

        List<String> headers = new ArrayList<String>();
        if (StringUtils.isNotEmpty(betaIps)) {
            headers.add("betaIps");
            headers.add(betaIps);
        }

        //調用agent的httpPost請求服務器進行配置發佈
        HttpResult result = null;
        try {
            result = agent.httpPost(url, headers, params, encode, POST_TIMEOUT);
        } catch (IOException ioe) {
            LOGGER.warn("[{}] [publish-single] exception, dataId={}, group={}, msg={}", agent.getName(), dataId,
                group, ioe.toString());
            return false;
        }

        if (HttpURLConnection.HTTP_OK == result.code) {
            LOGGER.info("[{}] [publish-single] ok, dataId={}, group={}, tenant={}, config={}", agent.getName(), dataId,
                group, tenant, ContentUtils.truncateContent(content));
            return true;
        } else if (HttpURLConnection.HTTP_FORBIDDEN == result.code) {
            LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(),
                dataId, group, tenant, result.code, result.content);
            throw new NacosException(result.code, result.content);
        } else {
            LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}", agent.getName(),
                dataId, group, tenant, result.code, result.content);
            return false;
        }

    }

12.最後,ConfigExample中還對配置進行了獲取和刪除,其分析方法和發佈配置類似,這裏不再贅述。

小結:
本篇內容主要通過ConfigExample爲切入,分析Client模塊中關於配置發佈、管理相關的類和方法,我們瞭解到NacosConfigService以及MetricHttpAgent和ClientWorker的作用和其中一些方法,後面一篇我們將繼續分析Nacos的配置管理,來研究下ClientWorker是怎麼實現配置更新的.

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