【java-nacos】跨 namespace 获取配置

背景

公司使用nacos-discovery作为服务注册和服务发现,使用nacos-conf作为配置中心,对于公共的资源配置信息都在global.xml上面,且在一个特殊的namespace下,和生产环境的namespace不一样,现在需要适配。

技术方案

一. nacos 多配置文件

naocs可以通过spring.cloud.nacos.config.extension-configs的配置来添加额外的配置文件,该配置项是一个list,可以配置多个,越靠前优先级越高。于是我们信心满满的配置了global.xml的三要素,dataId,group,namespace

然而并没生效,通过查看配置映射的java类com.alibaba.cloud.nacos.NacosConfigProperties发现extension-configs对应的内部类Config 只有 dataId,group,refresh三个属性,完全不支持namespace配置,查资料发现springboot还支持在extension-configs中配置namespace,springcloud中认为不应该支持跨namespace读取配置文件

二. 跨namespace读取配置

通过源码阅读发现,nacos读取配置文件是发生在com.alibaba.nacos.client.config.NacosConfigService#getConfig方法中,但该方法不支持传递namespace参数且该类初始化的时候固定namespace,不支持动态修改。但是有私有方法支持namespace,如下:

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

同时我们发现NacosConfigService 可以通过NacosConfigManager获得,而NacosConfigManager又是注册到spring的一个bean,我们可以通过自动注入很轻易的获取,然后通过反射执行该方法并获取配置文件,并解析放置到spring的Environment中,供其他服务使用

@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实例化前加载配置

本来以为问题顺利的解决了,业务部门的小伙伴反应,有些依赖的第三方库需要根据配置来决定是否实例化,如果采用上节的方法,即使拿到配置文件还需要手动启动那些bean,极其不优雅
所以我们需要在spring开始实例化bean前拿到该配置文件,通过继承接口ApplicationContextInitializer 并在application.yml中配置context.initializer.classes=XXX.XXX.ContextPreconditioning可以在Spring加载完已知的配置文件后执行该方法。
参考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;
    }

手动创建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);
        }
    }
}

总结

无论这两种方案实际上都是奇淫巧技,按照nacos官方的意见来说,不应该存在跨namespace读取配置文件的场景,因为生产,测试环境需要完全隔离,互不影响,上述方案都增加的维护成本和之后维护的额外开销

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容