• Spring Cloud Nacos实现动态配置加载的源码分析


    理解了上述Environment的基本原理后,如何从远程服务器上加载配置到Spring的Environment中。

    NacosPropertySourceLocator#

    顺着前面的分析思路,我们很自然的去找PropertySourceLocator的实现类,发现除了我们自定义的GpJsonPropertySourceLocator以外,还有另外一个实现类NacosPropertySourceLocator.

    于是,直接来看NacosPropertySourceLocator中的locate方法,代码如下。

    public PropertySource<?> locate(Environment env) {
        this.nacosConfigProperties.setEnvironment(env);
        ConfigService configService = this.nacosConfigManager.getConfigService();
        if (null == configService) {
            log.warn("no instance of config service found, can't load config from nacos");
            return null;
        } else {
            //获取客户端配置的超时时间
            long timeout = (long)this.nacosConfigProperties.getTimeout();
            this.nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
            //获取name属性,
            String name = this.nacosConfigProperties.getName();
            //在Spring Cloud中,默认的name=spring.application.name。
            String dataIdPrefix = this.nacosConfigProperties.getPrefix();
            if (StringUtils.isEmpty(dataIdPrefix)) {
                dataIdPrefix = name;
            }
    
            if (StringUtils.isEmpty(dataIdPrefix)) {
                dataIdPrefix = env.getProperty("spring.application.name"); //获取spring.application.name,赋值给dataIdPrefix
            }
           //创建一个Composite属性源,可以包含多个PropertySource
            CompositePropertySource composite = new CompositePropertySource("NACOS");
            this.loadSharedConfiguration(composite);   //加载共享配置 
             //加载扩展配置
            loadExtConfiguration(composite);
            //加载自身配置
            loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
            return composite;
        }
    }
    

    上述代码的实现不难理解

    1. 获取nacos客户端的配置属性,并生成dataId(这个很重要,要定位nacos的配置)
    2. 分别调用三个方法从加载配置属性源,保存到composite组合属性源中

    loadApplicationConfiguration#

    我们可以先不管加载共享配置、扩展配置的方法,最终本质上都是去远程服务上读取配置,只是传入的参数不一样。

    • fileExtension,表示配置文件的扩展名
    • nacosGroup表示分组
    • 加载dataid=项目名称的配置
    • 加载dataid=项目名称+扩展名的配置
    • 遍历当前配置的激活点(profile),分别循环加载带有profile的dataid配置
    private void loadApplicationConfiguration(
        CompositePropertySource compositePropertySource, String dataIdPrefix,
        NacosConfigProperties properties, Environment environment) {
        String fileExtension = properties.getFileExtension();  //默认的扩展名为: properties
        String nacosGroup = properties.getGroup(); //获取group
        //加载`dataid=项目名称`的配置
        loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
                               fileExtension, true);
        //加载`dataid=项目名称+扩展名`的配置
        loadNacosDataIfPresent(compositePropertySource,
                               dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
        // 遍历profile(可以有多个),根据profile加载配置
        for (String profile : environment.getActiveProfiles()) {
            //此时的dataId=${spring.application.name}.${profile}.${fileExtension}
            String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
            loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
                                   fileExtension, true);
        }
    
    }
    

    loadNacosDataIfPresent#

    调用loadNacosPropertySource加载存在的配置信息。

    把加载之后的配置属性保存到CompositePropertySource中。

    private void loadNacosDataIfPresent(final CompositePropertySource composite,
          final String dataId, final String group, String fileExtension,
          boolean isRefreshable) {
        //如果dataId为空,或者group为空,则直接跳过
       if (null == dataId || dataId.trim().length() < 1) {
          return;
       }
       if (null == group || group.trim().length() < 1) {
          return;
       }
        //从nacos中获取属性源
       NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
             fileExtension, isRefreshable);
        //把属性源保存到compositePropertySource中
       this.addFirstPropertySource(composite, propertySource, false);
    }
    

    loadNacosPropertySource#

    private NacosPropertySource loadNacosPropertySource(final String dataId,
          final String group, String fileExtension, boolean isRefreshable) {
       if (NacosContextRefresher.getRefreshCount() != 0) {
          if (!isRefreshable) { //是否支持自动刷新,// 如果不支持自动刷新配置则自动从缓存获取返回(不从远程服务器加载)
             return NacosPropertySourceRepository.getNacosPropertySource(dataId,
                   group);
          }
       }
        //构造器从配置中心获取数据
       return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
             isRefreshable);
    }
    

    NacosPropertySourceBuilder.build#

    NacosPropertySource build(String dataId, String group, String fileExtension,
          boolean isRefreshable) {
            //调用loadNacosData加载远程数据
       List<PropertySource<?>> propertySources = loadNacosData(dataId, group,
             fileExtension);
        //构造NacosPropertySource(这个是Nacos自定义扩展的PropertySource,和我们前面演示的自定义PropertySource类似)。
    //    相当于把从远程服务器获取的数据保存到NacosPropertySource中。
       NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
             group, dataId, new Date(), isRefreshable);
        //把属性缓存到本地缓存
       NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
       return nacosPropertySource;
    }
    

    NacosPropertySourceBuilder.loadNacosData#

    这个方法,就是连接远程服务器去获取配置数据的实现,关键代码是configService.getConfig

    private List<PropertySource<?>> loadNacosData(String dataId, String group,
          String fileExtension) {
       String data = null;
       try {
          data = configService.getConfig(dataId, group, timeout); //加载Nacos配置数据
          if (StringUtils.isEmpty(data)) {
             log.warn(
                   "Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
                   dataId, group);
             return Collections.emptyList();
          }
          if (log.isDebugEnabled()) {
             log.debug(String.format(
                   "Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
                   group, data));
          }
           //对加载的数据进行解析,保存到List<PropertySource>集合。
          return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
                fileExtension);
       }
       catch (NacosException e) {
          log.error("get data from Nacos error,dataId:{} ", dataId, e);
       }
       catch (Exception e) {
          log.error("parse data from Nacos error,dataId:{},data:{}", dataId, data, e);
       }
       return Collections.emptyList();
    }
    

    阶段性总结#

    通过上述分析,我们知道了Spring Cloud集成Nacos时的关键路径,并且知道在启动时,Spring Cloud会从Nacos Server中加载动态数据保存到Environment集合。

    从而实现动态配置的自动注入。

    Nacos客户端的数据的加载流程#

    配置数据的最终加载,是基于 configService.getConfig,Nacos提供的SDK来实现的。

    public String getConfig(String dataId, String group, long timeoutMs) throws NacosException
    

    关于Nacos SDK的使用教程: https://nacos.io/zh-cn/docs/sdk.html

    也就是说,接下来我们的源码分析,直接进入到Nacos这个范畴。

    NacosConfigService.getConfig#

    @Override
    public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
        return getConfigInner(namespace, dataId, group, timeoutMs);
    }
    
    private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
        group = blank2defaultGroup(group); //获取group,如果为空,则为default-group
        ParamUtils.checkKeyParam(dataId, group);   //验证请求参数
        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); //把内容设置到cr中。
            //获取容灾配置的encryptedDataKey
            String encryptedDataKey = LocalEncryptedDataKeyProcessor
                    .getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
            cr.setEncryptedDataKey(encryptedDataKey); //保存到cr
            configFilterChainManager.doFilter(null, cr); //执行过滤(目前好像没有实现)
            content = cr.getContent(); //返回文件content
            return content;
        }
        //如果本地文件中不存在相关内容,则发起远程调用
        try {
            ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs);
            //把响应内容返回
            cr.setContent(response.getContent());
            cr.setEncryptedDataKey(response.getEncryptedDataKey());
            
            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());
        }
        //如果出现NacosException,且不是403异常,则尝试通过本地的快照文件去获取配置进行返回。
        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);
        String encryptedDataKey = LocalEncryptedDataKeyProcessor
                .getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
        cr.setEncryptedDataKey(encryptedDataKey);
        configFilterChainManager.doFilter(null, cr);
        content = cr.getContent();
        return content;
    }
    

    从本地缓存读取配置#

    默认情况下,nacos先从本地缓存的配置中读取文件:C:\Users\mayn\nacos\config\fixed-192.168.8.133_8848-6a382560-ed4c-414c-a5e2-9d72c48f1a0e_nacos

    如果本地缓存内容存在,则返回内容数据,否则返回空值。

    public static String getFailover(String serverName, String dataId, String group, String tenant) {
        File localPath = getFailoverFile(serverName, dataId, group, tenant);
        if (!localPath.exists() || !localPath.isFile()) {
            return null;
        }
    
        try {
            return readFile(localPath);
        } catch (IOException ioe) {
            LOGGER.error("[" + serverName + "] get failover error, " + localPath, ioe);
            return null;
        }
    }
    

    从指定文件目录下读取文件内容。

    static File getFailoverFile(String serverName, String dataId, String group, String tenant) {
        File tmp = new File(LOCAL_SNAPSHOT_PATH, serverName + "_nacos");
        tmp = new File(tmp, "data");
        if (StringUtils.isBlank(tenant)) {
            tmp = new File(tmp, "config-data");
        } else {
            tmp = new File(tmp, "config-data-tenant");
            tmp = new File(tmp, tenant);
        }
        return new File(new File(tmp, group), dataId);
    }
    

    ClientWorker.getServerConfig#

    ClientWorker,表示客户端的一个工作类,它负责和服务端交互。

    public ConfigResponse getServerConfig(String dataId, String group, String tenant, long readTimeout)
            throws NacosException {
        ConfigResponse configResponse = new ConfigResponse();
        if (StringUtils.isBlank(group)) { //如果group为空,则返回默认group
            group = Constants.DEFAULT_GROUP;
        }
        
        HttpRestResult<String> result = null;
        try {
            Map<String, String> params = new HashMap<String, String>(3);  //构建请求参数
            if (StringUtils.isBlank(tenant)) { 
                params.put("dataId", dataId);
                params.put("group", group);
            } else {
                params.put("dataId", dataId);
                params.put("group", group);
                params.put("tenant", tenant);
            }
            //发起远程调用
            result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
        } catch (Exception ex) {
            String message = String
                    .format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s",
                            agent.getName(), dataId, group, tenant);
            LOGGER.error(message, ex);
            throw new NacosException(NacosException.SERVER_ERROR, ex);
        }
        //根据响应结果实现不同的处理
        switch (result.getCode()) { 
            case HttpURLConnection.HTTP_OK: //如果响应成功,保存快照到本地,并返回响应内容
                LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
                configResponse.setContent(result.getData());
                String configType;  //配置文件的类型,如text、json、yaml等
                if (result.getHeader().getValue(CONFIG_TYPE) != null) {
                    configType = result.getHeader().getValue(CONFIG_TYPE);
                } else {
                    configType = ConfigType.TEXT.getType();
                }
                configResponse.setConfigType(configType); //设置到configResponse中,后续要根据文件类型实现不同解析策略
                //获取加密数据的key
                String encryptedDataKey = result.getHeader().getValue(ENCRYPTED_DATA_KEY);
                //保存
                LocalEncryptedDataKeyProcessor
                        .saveEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant, encryptedDataKey);
                configResponse.setEncryptedDataKey(encryptedDataKey);
                return configResponse;
            case HttpURLConnection.HTTP_NOT_FOUND: //如果返回404, 清空本地快照
                LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
                LocalEncryptedDataKeyProcessor.saveEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant, null);
                return configResponse;
            case HttpURLConnection.HTTP_CONFLICT: {
                LOGGER.error(
                        "[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, "
                                + "tenant={}", agent.getName(), dataId, group, tenant);
                throw new NacosException(NacosException.CONFLICT,
                        "data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
            }
            case HttpURLConnection.HTTP_FORBIDDEN: {
                LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", agent.getName(),
                        dataId, group, tenant);
                throw new NacosException(result.getCode(), result.getMessage());
            }
            default: {
                LOGGER.error("[{}] [sub-server-error]  dataId={}, group={}, tenant={}, code={}", agent.getName(),
                        dataId, group, tenant, result.getCode());
                throw new NacosException(result.getCode(),
                        "http error, code=" + result.getCode() + ",dataId=" + dataId + ",group=" + group + ",tenant="
                                + tenant);
            }
        }
    }
    

    ServerHttpAgent.httpGet#

    发起远程请求的实现。

    @Override
    public HttpRestResult<String> httpGet(String path, Map<String, String> headers, Map<String, String> paramValues,
            String encode, long readTimeoutMs) throws Exception {
        final long endTime = System.currentTimeMillis() + readTimeoutMs;
        injectSecurityInfo(paramValues);  //注入安全信息
        String currentServerAddr = serverListMgr.getCurrentServerAddr();//获取当前服务器地址
        int maxRetry = this.maxRetry; //获取最大重试次数,默认3次
        //配置HttpClient的属性,默认的readTimeOut超时时间是3s
        HttpClientConfig httpConfig = HttpClientConfig.builder()
                .setReadTimeOutMillis(Long.valueOf(readTimeoutMs).intValue())
                .setConTimeOutMillis(ConfigHttpClientManager.getInstance().getConnectTimeoutOrDefault(100)).build();
        do {
           
            try {
                //设置header
                Header newHeaders = getSpasHeaders(paramValues, encode);
                if (headers != null) {
                    newHeaders.addAll(headers);
                }
                //构建query查询条件
                Query query = Query.newInstance().initParams(paramValues);
                //发起http请求,http://192.168.8.133:8848/nacos/v1/cs/configs
                HttpRestResult<String> result = NACOS_RESTTEMPLATE
                        .get(getUrl(currentServerAddr, path), httpConfig, newHeaders, query, String.class);
                if (isFail(result)) { //如果请求失败,
                    LOGGER.error("[NACOS ConnectException] currentServerAddr: {}, httpCode: {}",
                            serverListMgr.getCurrentServerAddr(), result.getCode());
                } else {
                    // Update the currently available server addr
                    serverListMgr.updateCurrentServerAddr(currentServerAddr);
                    return result;
                }
            } catch (ConnectException connectException) {
                LOGGER.error("[NACOS ConnectException httpGet] currentServerAddr:{}, err : {}",
                        serverListMgr.getCurrentServerAddr(), connectException.getMessage());
            } catch (SocketTimeoutException socketTimeoutException) {
                LOGGER.error("[NACOS SocketTimeoutException httpGet] currentServerAddr:{}, err : {}",
                        serverListMgr.getCurrentServerAddr(), socketTimeoutException.getMessage());
            } catch (Exception ex) {
                LOGGER.error("[NACOS Exception httpGet] currentServerAddr: " + serverListMgr.getCurrentServerAddr(),
                        ex);
                throw ex;
            }
            //如果服务端列表有多个,并且当前请求失败,则尝试用下一个地址进行重试
            if (serverListMgr.getIterator().hasNext()) {
                currentServerAddr = serverListMgr.getIterator().next();
            } else {
                maxRetry--; //重试次数递减
                if (maxRetry < 0) {
                    throw new ConnectException(
                            "[NACOS HTTP-GET] The maximum number of tolerable server reconnection errors has been reached");
                }
                serverListMgr.refreshCurrentServerAddr();
            }
            
        } while (System.currentTimeMillis() <= endTime);
        
        LOGGER.error("no available server");
        throw new ConnectException("no available server");
    }
    

    Nacos Server端的配置获取#

    客户端向服务端加载配置,调用的接口是:/nacos/v1/cs/configs , 于是,在Nacos的源码中找到该接口

    定位到Nacos源码中的ConfigController.getConfig中的方法,代码如下:

    @GetMapping
    @Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
    public void getConfig(HttpServletRequest request, HttpServletResponse response,
            @RequestParam("dataId") String dataId, @RequestParam("group") String group,
            @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
            @RequestParam(value = "tag", required = false) String tag)
            throws IOException, ServletException, NacosException {
        // check tenant
        ParamUtils.checkTenant(tenant);
        tenant = NamespaceUtil.processNamespaceParameter(tenant); //租户,也就是namespaceid
        // check params
        ParamUtils.checkParam(dataId, group, "datumId", "content"); //检查请求参数是否为空
        ParamUtils.checkParam(tag);
        
        final String clientIp = RequestUtil.getRemoteIp(request); //获取请求的ip
        inner.doGetConfig(request, response, dataId, group, tenant, tag, clientIp); //加载配置
    }
    

    inner.doGetConfig#

    public String doGetConfig(HttpServletRequest request, HttpServletResponse response, String dataId, String group,
            String tenant, String tag, String clientIp) throws IOException, ServletException {
        final String groupKey = GroupKey2.getKey(dataId, group, tenant);
        String autoTag = request.getHeader("Vipserver-Tag");
        String requestIpApp = RequestUtil.getAppName(request); //请求端的应用名称
       
        int lockResult = tryConfigReadLock(groupKey);  //尝试获取当前请求配置的读锁(避免读写冲突)
        
        final String requestIp = RequestUtil.getRemoteIp(request); //请求端的ip
        
        boolean isBeta = false;
        //lockResult>0 ,表示CacheItem(也就是缓存的配置项)不为空,并且已经加了读锁,意味着这个缓存数据不能被删除。
        //lockResult=0 ,表示cacheItem为空,不需要加读锁
        //lockResult=01 , 表示加锁失败,存在冲突。
        //下面这个if,就是针对这三种情况进行处理。
        if (lockResult > 0) {
            // LockResult > 0 means cacheItem is not null and other thread can`t delete this cacheItem
            FileInputStream fis = null;
            try {
                String md5 = Constants.NULL;
                long lastModified = 0L;
                //从本地缓存中,根据groupKey获取CacheItem
                CacheItem cacheItem = ConfigCacheService.getContentCache(groupKey);
                //判断是否是beta发布,也就是测试版本
                if (cacheItem.isBeta() && cacheItem.getIps4Beta().contains(clientIp)) {
                    isBeta = true;
                }
                //获取配置文件的类型
                final String configType =
                        (null != cacheItem.getType()) ? cacheItem.getType() : FileTypeEnum.TEXT.getFileType();
                response.setHeader("Config-Type", configType);
                //返回文件类型的枚举对象
                FileTypeEnum fileTypeEnum = FileTypeEnum.getFileTypeEnumByFileExtensionOrFileType(configType);
                String contentTypeHeader = fileTypeEnum.getContentType();
                response.setHeader(HttpHeaderConsts.CONTENT_TYPE, contentTypeHeader);
                
                File file = null;
                ConfigInfoBase configInfoBase = null;
                PrintWriter out = null;
                if (isBeta) { //如果是测试配置
                    md5 = cacheItem.getMd54Beta();
                    lastModified = cacheItem.getLastModifiedTs4Beta();
                    if (PropertyUtil.isDirectRead()) {
                        configInfoBase = persistService.findConfigInfo4Beta(dataId, group, tenant);
                    } else {
                        file = DiskUtil.targetBetaFile(dataId, group, tenant); //从磁盘中获取文件,得到的是一个完整的File
                    }
                    response.setHeader("isBeta", "true");
                } else {
                    if (StringUtils.isBlank(tag)) { //判断tag标签是否为空,tag对应的是nacos配置中心的标签选项
                        if (isUseTag(cacheItem, autoTag)) {
                            if (cacheItem.tagMd5 != null) {
                                md5 = cacheItem.tagMd5.get(autoTag);
                            }
                            if (cacheItem.tagLastModifiedTs != null) {
                                lastModified = cacheItem.tagLastModifiedTs.get(autoTag);
                            }
                            if (PropertyUtil.isDirectRead()) {
                                configInfoBase = persistService.findConfigInfo4Tag(dataId, group, tenant, autoTag);
                            } else {
                                file = DiskUtil.targetTagFile(dataId, group, tenant, autoTag);
                            }
                            
                            response.setHeader("Vipserver-Tag",
                                    URLEncoder.encode(autoTag, StandardCharsets.UTF_8.displayName()));
                        } else {//直接走这个逻辑(默认不会配置tag属性)
                            md5 = cacheItem.getMd5(); //获取缓存的md5
                            lastModified = cacheItem.getLastModifiedTs(); //获取最后更新时间
                            if (PropertyUtil.isDirectRead()) {  //判断是否是stamdalone模式且使用的是derby数据库,如果是,则从derby数据库加载数据
                                configInfoBase = persistService.findConfigInfo(dataId, group, tenant);
                            } else {
                                //否则,如果是数据库或者集群模式,先从本地磁盘得到文件
                                file = DiskUtil.targetFile(dataId, group, tenant);
                            }
                            //如果本地磁盘文件为空,并且configInfoBase为空,则表示配置数据不存在,直接返回null
                            if (configInfoBase == null && fileNotExist(file)) {
                                // FIXME CacheItem
                                // No longer exists. It is impossible to simply calculate the push delayed. Here, simply record it as - 1.
                                ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1,
                                        ConfigTraceService.PULL_EVENT_NOTFOUND, -1, requestIp);
                                
                                // pullLog.info("[client-get] clientIp={}, {},
                                // no data",
                                // new Object[]{clientIp, groupKey});
                                
                                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                                response.getWriter().println("config data not exist");
                                return HttpServletResponse.SC_NOT_FOUND + "";
                            }
                        }
                    } else {//如果tag不为空,说明配置文件设置了tag标签
                        if (cacheItem.tagMd5 != null) {
                            md5 = cacheItem.tagMd5.get(tag); 
                        }
                        if (cacheItem.tagLastModifiedTs != null) {
                            Long lm = cacheItem.tagLastModifiedTs.get(tag);
                            if (lm != null) {
                                lastModified = lm;
                            }
                        }
                        if (PropertyUtil.isDirectRead()) {
                            configInfoBase = persistService.findConfigInfo4Tag(dataId, group, tenant, tag);
                        } else {
                            file = DiskUtil.targetTagFile(dataId, group, tenant, tag);
                        }
                        if (configInfoBase == null && fileNotExist(file)) {
                            // FIXME CacheItem
                            // No longer exists. It is impossible to simply calculate the push delayed. Here, simply record it as - 1.
                            ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1,
                                    ConfigTraceService.PULL_EVENT_NOTFOUND, -1, requestIp);
                            
                            // pullLog.info("[client-get] clientIp={}, {},
                            // no data",
                            // new Object[]{clientIp, groupKey});
                            
                            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                            response.getWriter().println("config data not exist");
                            return HttpServletResponse.SC_NOT_FOUND + "";
                        }
                    }
                }
                //把获取的数据结果设置到response中返回
                
                response.setHeader(Constants.CONTENT_MD5, md5);
                
                // Disable cache.
                response.setHeader("Pragma", "no-cache");
                response.setDateHeader("Expires", 0);
                response.setHeader("Cache-Control", "no-cache,no-store");
                if (PropertyUtil.isDirectRead()) {
                    response.setDateHeader("Last-Modified", lastModified);
                } else {
                    fis = new FileInputStream(file);
                    response.setDateHeader("Last-Modified", file.lastModified());
                }
                //如果是单机模式,直接把数据写回到客户端
                if (PropertyUtil.isDirectRead()) {
                    out = response.getWriter();
                    out.print(configInfoBase.getContent());
                    out.flush();
                    out.close();
                } else {//否则,通过trasferTo
                    fis.getChannel()
                            .transferTo(0L, fis.getChannel().size(), Channels.newChannel(response.getOutputStream()));
                }
                
                LogUtil.PULL_CHECK_LOG.warn("{}|{}|{}|{}", groupKey, requestIp, md5, TimeUtils.getCurrentTimeStr());
                
                final long delayed = System.currentTimeMillis() - lastModified;
                
                // TODO distinguish pull-get && push-get
                /*
                 Otherwise, delayed cannot be used as the basis of push delay directly,
                 because the delayed value of active get requests is very large.
                 */
                ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, lastModified,
                        ConfigTraceService.PULL_EVENT_OK, delayed, requestIp);
                
            } finally { 
                releaseConfigReadLock(groupKey); //释放锁
                IoUtils.closeQuietly(fis);
            }
        } else if (lockResult == 0) { //说明缓存为空,
            
            // FIXME CacheItem No longer exists. It is impossible to simply calculate the push delayed. Here, simply record it as - 1.
            ConfigTraceService
                    .logPullEvent(dataId, group, tenant, requestIpApp, -1, ConfigTraceService.PULL_EVENT_NOTFOUND, -1,
                            requestIp);
            
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            response.getWriter().println("config data not exist");
            return HttpServletResponse.SC_NOT_FOUND + "";
            
        } else {//
            
            PULL_LOG.info("[client-get] clientIp={}, {}, get data during dump", clientIp, groupKey);
            
            response.setStatus(HttpServletResponse.SC_CONFLICT);
            response.getWriter().println("requested file is being modified, please try later.");
            return HttpServletResponse.SC_CONFLICT + "";
            
        }
        
        return HttpServletResponse.SC_OK + "";
    }
    

    persistService.findConfigInfo#

    从derby数据库中获取数据内容,这个就是一个基本的数据查询操作。

    @Override
    public ConfigInfo findConfigInfo(final String dataId, final String group, final String tenant) {
        final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;
        final String sql = "SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info "
                + " WHERE data_id=? AND group_id=? AND tenant_id=?";
        final Object[] args = new Object[] {dataId, group, tenantTmp};
        return databaseOperate.queryOne(sql, args, CONFIG_INFO_ROW_MAPPER);
        
    }
    

    DiskUtil.targetFile#

    从磁盘目录中获取目标文件,直接根据dataId/group/tenant ,查找指定目录下的文件即可

    public static File targetFile(String dataId, String group, String tenant) {
        File file = null;
        if (StringUtils.isBlank(tenant)) {
            file = new File(EnvUtil.getNacosHome(), BASE_DIR);
        } else {
            file = new File(EnvUtil.getNacosHome(), TENANT_BASE_DIR);
            file = new File(file, tenant);
        }
        file = new File(file, group);
        file = new File(file, dataId);
        return file;
    }
    

    至此,NacosPropertySourceLocator 完成了从Nacos Server上动态获取配置并缓存到本地,从而实现Nacos动态配置获取的能力!

    版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构
    如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注「跟着Mic学架构」公众号公众号获取更多技术干货!

  • 相关阅读:
    LeetCode·76.最小覆盖子串·滑动窗口
    查找外文文献的几种方式
    C#运算符重载
    [7天通关Python基础]-13:面向对象的三大特征:封装、继承和多态
    netty 客户端 实现断开重连
    《Python零基础入门》——关于PyCharm使用技巧及python基本概念
    springboot基于Android的洗衣店预约APP毕业设计源码260839
    图的十字链表存储结构
    分布式缓存寻址算法
    JS踩坑: for let 和 for var的区别
  • 原文地址:https://www.cnblogs.com/mic112/p/15948495.html