背景
公司使用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读取配置文件的场景,因为生产,测试环境需要完全隔离,互不影响,上述方案都增加的维护成本和之后维护的额外开销