菜鸟学源码之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是怎么实现配置更新的.

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