【java-nacos】跨 namespace 獲取配置

背景

公司使用nacos-discovery作為服務(wù)注冊和服務(wù)發(fā)現(xiàn),使用nacos-conf作為配置中心,對于公共的資源配置信息都在global.xml上面,且在一個(gè)特殊的namespace下,和生產(chǎn)環(huán)境的namespace不一樣,現(xiàn)在需要適配。

技術(shù)方案

一. nacos 多配置文件

naocs可以通過spring.cloud.nacos.config.extension-configs的配置來添加額外的配置文件,該配置項(xiàng)是一個(gè)list,可以配置多個(gè),越靠前優(yōu)先級越高。于是我們信心滿滿的配置了global.xml的三要素,dataId,group,namespace。

然而并沒生效,通過查看配置映射的java類com.alibaba.cloud.nacos.NacosConfigProperties發(fā)現(xiàn)extension-configs對應(yīng)的內(nèi)部類Config 只有 dataId,group,refresh三個(gè)屬性,完全不支持namespace配置,查資料發(fā)現(xiàn)springboot還支持在extension-configs中配置namespace,springcloud中認(rèn)為不應(yīng)該支持跨namespace讀取配置文件

二. 跨namespace讀取配置

通過源碼閱讀發(fā)現(xiàn),nacos讀取配置文件是發(fā)生在com.alibaba.nacos.client.config.NacosConfigService#getConfig方法中,但該方法不支持傳遞namespace參數(shù)且該類初始化的時(shí)候固定namespace,不支持動(dòng)態(tài)修改。但是有私有方法支持namespace,如下:

// tenant 就是指namespace
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
.....
}

同時(shí)我們發(fā)現(xiàn)NacosConfigService 可以通過NacosConfigManager獲得,而NacosConfigManager又是注冊到spring的一個(gè)bean,我們可以通過自動(dòng)注入很輕易的獲取,然后通過反射執(zhí)行該方法并獲取配置文件,并解析放置到spring的Environment中,供其他服務(wù)使用

@Autowired
private ConfigurableEnvironment environment;
@Autowired
private NacosConfigManager nacosConfigManager;
public Map<String, Object> loadConfigManually(String namespace,String dataId,String group,long timeoutMs){
        ConfigService configService = nacosConfigManager.getConfigService();
        try {
            Method method = NacosConfigService.class.getDeclaredMethod("getConfigInner",
                    String.class, String.class, String.class, long.class);
            method.setAccessible(true);
            String res = (String)method.invoke(configService, namespace, dataId, group, timeoutMs);
            NacosByteArrayResource nacosByteArrayResource = new NacosByteArrayResource(
                    NacosConfigUtils.selectiveConvertUnicode(res).getBytes(), dataId);
            Map<String, Object> map = xmlLoader.parseXml2Map(nacosByteArrayResource);
            MapPropertySource mapPropertySource = new MapPropertySource("global", map);
            environment.getPropertySources().addLast(mapPropertySource);
            return map;
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            log.error("invoke error,please check your parameter, ",e);
        } catch (IOException e) {
            log.error("config can not be parse ",e);
        }
        return null;
    }

三. 早于spring bean實(shí)例化前加載配置

本來以為問題順利的解決了,業(yè)務(wù)部門的小伙伴反應(yīng),有些依賴的第三方庫需要根據(jù)配置來決定是否實(shí)例化,如果采用上節(jié)的方法,即使拿到配置文件還需要手動(dòng)啟動(dòng)那些bean,極其不優(yōu)雅
所以我們需要在spring開始實(shí)例化bean前拿到該配置文件,通過繼承接口ApplicationContextInitializer 并在application.yml中配置context.initializer.classes=XXX.XXX.ContextPreconditioning可以在Spring加載完已知的配置文件后執(zhí)行該方法。
參考com.alibaba.cloud.nacos.NacosConfigProperties#assembleConfigServiceProperties方法,大致模擬nacos的配置文件,

public Properties assembleConfigServiceProperties() {
        Properties properties = new Properties();
        properties.put(SERVER_ADDR, Objects.toString(this.serverAddr, ""));
        properties.put(USERNAME, Objects.toString(this.username, ""));
        properties.put(PASSWORD, Objects.toString(this.password, ""));
        properties.put(ENCODE, Objects.toString(this.encode, ""));
        properties.put(NAMESPACE, Objects.toString(this.namespace, ""));
        properties.put(ACCESS_KEY, Objects.toString(this.accessKey, ""));
        properties.put(SECRET_KEY, Objects.toString(this.secretKey, ""));
        properties.put(CLUSTER_NAME, Objects.toString(this.clusterName, ""));
        properties.put(MAX_RETRY, Objects.toString(this.maxRetry, ""));
        properties.put(CONFIG_LONG_POLL_TIMEOUT,
                Objects.toString(this.configLongPollTimeout, ""));
        properties.put(CONFIG_RETRY_TIME, Objects.toString(this.configRetryTime, ""));
        properties.put(ENABLE_REMOTE_SYNC_CONFIG,
                Objects.toString(this.enableRemoteSyncConfig, ""));
        String endpoint = Objects.toString(this.endpoint, "");
        if (endpoint.contains(":")) {
            int index = endpoint.indexOf(":");
            properties.put(ENDPOINT, endpoint.substring(0, index));
            properties.put(ENDPOINT_PORT, endpoint.substring(index + 1));
        }
        else {
            properties.put(ENDPOINT, endpoint);
        }

        enrichNacosConfigProperties(properties);
        return properties;
    }

手動(dòng)創(chuàng)建NacosConfigService 并傳遞該配置(這些配置可以通過spring的Environment中獲取),如下:

@Order(1)
@Slf4j
public class selfContextPreconditioning implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        String namespace = "";
        String dataId = "";
        String group = "";
        Properties properties = new Properties();
        properties.put(SERVER_ADDR, environment.getProperty("spring.cloud.nacos.config.server-addr",""));
        boolean nacosAuthEnable = environment.getProperty("spring.cloud.nacos.config.auth-enable"
               ,boolean.class,false);
        if (nacosAuthEnable) {
            properties.put(USERNAME, "nacos");
            properties.put(PASSWORD, "nacos");
        }
        properties.put(ENCODE, environment.getProperty("spring.cloud.nacos.config.encode", ""));
        properties.put(NAMESPACE, namespace);
        properties.put(MAX_RETRY, environment.getProperty("spring.cloud.nacos.config.max-retry", ""));
        properties.put(CONFIG_LONG_POLL_TIMEOUT,
                environment.getProperty("spring.cloud.nacos.config.config-long-poll-timeout", ""));
        properties.put(CONFIG_RETRY_TIME,
                environment.getProperty("spring.cloud.nacos.config.config-retry-time", ""));

        try {
            NacosConfigService service = new NacosConfigService(properties);
            Method method = NacosConfigService.class.getDeclaredMethod("getConfigInner",
                    String.class, String.class, String.class, long.class);
            method.setAccessible(true);
            String res = (String)method.invoke(service, namespace, dataId, group, 5000l);
            NacosByteArrayResource nacosByteArrayResource = new NacosByteArrayResource(
                    NacosConfigUtils.selectiveConvertUnicode(res).getBytes(), dataId);
//自定義的xmlloader
            XmlPropertySourceLoader xmlLoader = new XmlPropertySourceLoader();
            Map<String, Object> map = xmlLoader.parseXml2Map(nacosByteArrayResource);
            if (map != null) {
                MapPropertySource source = new MapPropertySource("global", map);
                environment.getPropertySources().addLast(source);
            } else {
                log.error("global config is null, namespace: {},dataId: {},group: {}",namespace,dataId,group);
            }
        } catch (NacosException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | IOException e) {
            log.error("global config parse Exception,please check your Nacos config, namespace: {},dataId: {},group: {}",namespace,dataId,group);
        }
    }
}

總結(jié)

無論這兩種方案實(shí)際上都是奇淫巧技,按照nacos官方的意見來說,不應(yīng)該存在跨namespace讀取配置文件的場景,因?yàn)樯a(chǎn),測試環(huán)境需要完全隔離,互不影響,上述方案都增加的維護(hù)成本和之后維護(hù)的額外開銷

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容