背景
公司使用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ù)的額外開銷