Spring Cloud Nacos實現動態配置載入的原始碼分析

跟著Mic學架構發表於2022-03-01

理解了上述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學架構」公眾號公眾號獲取更多技術乾貨!

相關文章