公司项目需求将国际化配置放入Nacos配置中心,以实现动态修改更新国际化配置而避免后端服务发版
在网上百度了一番后发现有两种实现
1.Nacos实现SpringBoot国际化的增强
2.Springboot基于Nacos的动态国际化
经过实验后第二种方式可以实现nacos动态配置国际化,本文也是参照方式二去做的,第一种未做实验。
Spring的国际化基于MessageSource接口,此接口为最顶层的接口
其主要有两个实现类:
ResourceBundleMessageSource是Springboot国际化自动装配类MessageSourceAutoConfiguration的默认的实现类,其就是读取国际化properties文件的配置缓存国际化数据。
ReloadableResourceBundleMessageSource是可重加载国际化配置文件的实现,其中对于国际化配置有两级缓存,分别是文件名-国际化配置数据缓存、时区-对应的配置缓存。
由于spring中的两种实现都与我们读取nacos配置并动态更新的需求不符合,所以只能实现自己的MessageSource接口实现类。
代码如下(示例):
@Slf4j
@Configuration
public class I18Config {
@Value("${spring.messages.encoding}")
private String encoding;
@Value("${spring.cloud.nacos.discovery.group}")
private String group;
@Resource
private NacosConfigManager nacosConfigManager;
@Bean("messageSource")
public NacosBundleMessageSource messageSource() {
NacosBundleMessageSource messageSource = new NacosBundleMessageSource();
messageSource.setBasenames("message");
messageSource.setDefaultEncoding(encoding);
messageSource.setNacosGroup(group);
messageSource.setNacosConfigManager(nacosConfigManager);
messageSource.setCacheSeconds(10);
return messageSource;
}
/**
* @return LocaleResolver
*/
@Bean
public LocaleResolver localeResolver() {
return new LocaleResolver() {
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 通过请求头的lang参数解析locale
String temp = request.getHeader("lang");
if (!StringUtils.isEmpty(temp)) {
String[] split = temp.split("_");
// 构造器要用对,不然时区对象在MessageSource实现类中获取的fileName将与传入的配置信息不匹配导致问题
Locale locale = new Locale(split[0], split[1]);
log.info("locale:" + locale);
return locale;
} else {
return Locale.getDefault();
}
}
@Override
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
}
};
}
}
在nacos上配置国际化文件名
同时在nacos上新建对应的多语言配置文件
@Data
//@Component
//@Configuration
@ConfigurationProperties(prefix = "locale")
public class LocaleProperties {
private List<String> fileName;
}
@Slf4j
@Component
@RequiredArgsConstructor
public class NacosConfig {
@Value("${spring.cloud.nacos.discovery.group}")
private String group;
private final LocaleProperties localeProperties;
private final NacosConfigManager nacosConfigManager;
private final NacosBundleMessageSource messageSource;
@PostConstruct
public void init() throws Exception {
//1.从nacos获取国际化文件名,遍历每个文件名
List<String> fileNameList = localeProperties.getFileName();
if(ObjectUtil.isEmpty(fileNameList)){
fileNameList = Lists.newArrayList("message_zh_CN", "message_en_US");
}
// 2.遍历每个文件名,添加nacos监听器,监听对应配置变化
for (String fileName : fileNameList) {
// nacos查询配置是以文件名来查询
String dataId = fileName + ".properties";
nacosConfigManager.getConfigService().addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
try {
// 更新对应配置信息,可以带入对应时区信息,做到仅更新对应时区的信息,避免全量的开销
messageSource.forceRefresh(fileName, configInfo);
} catch (Exception e) {
log.error("国际化配置监听异常", e);
}
}
@Override
public Executor getExecutor() {
return null;
}
});
}
log.info("国际化初始配置结束");
}
}
@Data
public class NacosBundleMessageSource extends AbstractResourceBasedMessageSource {
private boolean concurrentRefresh = true;
private NacosConfigManager nacosConfigManager;
private String nacosGroup;
private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
/**
* Cache to hold filename lists per Locale
*/
private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFilenames = new ConcurrentHashMap<>();
/**
* Cache to hold already loaded properties per filename
*/
private final ConcurrentMap<String, NacosBundleMessageSource.PropertiesHolder> cachedProperties = new ConcurrentHashMap<>();
/**
* Cache to hold already loaded properties per filename
*/
private final ConcurrentMap<Locale, NacosBundleMessageSource.PropertiesHolder> cachedMergedProperties = new ConcurrentHashMap<>();
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
if (getCacheMillis() < 0) {
NacosBundleMessageSource.PropertiesHolder propHolder = getMergedProperties(locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
} else {
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
NacosBundleMessageSource.PropertiesHolder propHolder = getProperties(filename);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
}
}
return null;
}
@Override
@Nullable
protected MessageFormat resolveCode(String code, Locale locale) {
if (getCacheMillis() < 0) {
NacosBundleMessageSource.PropertiesHolder propHolder = getMergedProperties(locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
} else {
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
NacosBundleMessageSource.PropertiesHolder propHolder = getProperties(filename);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
}
}
return null;
}
/**
* 强制刷新
*
* @param fileName 文件名
* @param config 变更后的配置内容
*/
public void forceRefresh(String fileName, String config) throws IOException {
synchronized (this) {
Properties props = newProperties();
ByteArrayInputStream inputStream = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8));
this.propertiesPersister.load(props, inputStream);
long fileTimestamp = -1;
NacosBundleMessageSource.PropertiesHolder propHolder = new NacosBundleMessageSource.PropertiesHolder(props, fileTimestamp);
this.cachedProperties.put(fileName, propHolder);
this.cachedMergedProperties.clear();
}
}
protected NacosBundleMessageSource.PropertiesHolder getMergedProperties(Locale locale) {
NacosBundleMessageSource.PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
if (mergedHolder != null) {
return mergedHolder;
}
Properties mergedProps = newProperties();
long latestTimestamp = -1;
String[] basenames = StringUtils.toStringArray(getBasenameSet());
for (int i = basenames.length - 1; i >= 0; i--) {
List<String> filenames = calculateAllFilenames(basenames[i], locale);
for (int j = filenames.size() - 1; j >= 0; j--) {
String filename = filenames.get(j);
NacosBundleMessageSource.PropertiesHolder propHolder = getProperties(filename);
if (propHolder.getProperties() != null) {
mergedProps.putAll(propHolder.getProperties());
if (propHolder.getFileTimestamp() > latestTimestamp) {
latestTimestamp = propHolder.getFileTimestamp();
}
}
}
}
mergedHolder = new NacosBundleMessageSource.PropertiesHolder(mergedProps, latestTimestamp);
NacosBundleMessageSource.PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
}
return mergedHolder;
}
protected List<String> calculateAllFilenames(String basename, Locale locale) {
Map<Locale, List<String>> localeMap = this.cachedFilenames.get(basename);
if (localeMap != null) {
List<String> filenames = localeMap.get(locale);
if (filenames != null) {
return filenames;
}
}
// Filenames for given Locale
List<String> filenames = new ArrayList<>(7);
filenames.addAll(calculateFilenamesForLocale(basename, locale));
// Filenames for default Locale, if any
Locale defaultLocale = getDefaultLocale();
if (defaultLocale != null && !defaultLocale.equals(locale)) {
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, defaultLocale);
for (String fallbackFilename : fallbackFilenames) {
if (!filenames.contains(fallbackFilename)) {
// Entry for fallback locale that isn't already in filenames list.
filenames.add(fallbackFilename);
}
}
}
// Filename for default bundle file
filenames.add(basename);
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
}
}
localeMap.put(locale, filenames);
return filenames;
}
protected List<String> calculateFilenamesForLocale(String basename, Locale locale) {
List<String> result = new ArrayList<>(3);
String language = locale.getLanguage();
String country = locale.getCountry();
String variant = locale.getVariant();
StringBuilder temp = new StringBuilder(basename);
temp.append('_');
if (language.length() > 0) {
temp.append(language);
result.add(0, temp.toString());
}
temp.append('_');
if (country.length() > 0) {
temp.append(country);
result.add(0, temp.toString());
}
if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) {
temp.append('_').append(variant);
result.add(0, temp.toString());
}
return result;
}
protected NacosBundleMessageSource.PropertiesHolder getProperties(String filename) {
NacosBundleMessageSource.PropertiesHolder propHolder = this.cachedProperties.get(filename);
long originalTimestamp = -2;
if (propHolder != null) {
originalTimestamp = propHolder.getRefreshTimestamp();
if (originalTimestamp == -1 || originalTimestamp > System.currentTimeMillis() - getCacheMillis()) {
// Up to date
return propHolder;
}
} else {
propHolder = new NacosBundleMessageSource.PropertiesHolder();
NacosBundleMessageSource.PropertiesHolder existingHolder = this.cachedProperties.putIfAbsent(filename, propHolder);
if (existingHolder != null) {
propHolder = existingHolder;
}
}
// At this point, we need to refresh...
if (this.concurrentRefresh && propHolder.getRefreshTimestamp() >= 0) {
// A populated but stale holder -> could keep using it.
if (!propHolder.refreshLock.tryLock()) {
// Getting refreshed by another thread already ->
// let's return the existing properties for the time being.
return propHolder;
}
} else {
propHolder.refreshLock.lock();
}
try {
NacosBundleMessageSource.PropertiesHolder existingHolder = this.cachedProperties.get(filename);
if (existingHolder != null && existingHolder.getRefreshTimestamp() > originalTimestamp) {
return existingHolder;
}
return refreshProperties(filename, propHolder);
} finally {
propHolder.refreshLock.unlock();
}
}
/**
* 刷新多语言配置
* @param filename
* @param propHolder
* @return
*/
protected NacosBundleMessageSource.PropertiesHolder refreshProperties(String filename, @Nullable NacosBundleMessageSource.PropertiesHolder propHolder) {
long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis());
long fileTimestamp = -1;
try {
Properties props = loadProperties(filename);
propHolder = new NacosBundleMessageSource.PropertiesHolder(props, fileTimestamp);
} catch (IOException | NacosException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not get properties form nacos ", ex);
}
// Empty holder representing "not valid".
propHolder = new NacosBundleMessageSource.PropertiesHolder();
}
propHolder.setRefreshTimestamp(refreshTimestamp);
this.cachedProperties.put(filename, propHolder);
return propHolder;
}
/**
* 根据文件从nacos中加载多语言配置文件
* @param fileName
* @return 配置类对象
* @throws IOException
* @throws NacosException
*/
protected Properties loadProperties(String fileName) throws IOException, NacosException {
Properties props = newProperties();
String config = nacosConfigManager.getConfigService().getConfig(fileName + ".properties", nacosGroup, 5000);
ByteArrayInputStream inputStream = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8));
this.propertiesPersister.load(props, inputStream);
return props;
}
/**
* 新建一个配置类对象
* @return
*/
protected Properties newProperties() {
return new Properties();
}
protected class PropertiesHolder {
/**
* 配置内容-key,vlaue形式
*/
@Nullable
private final Properties properties;
/**
* 文件时间
*/
private final long fileTimestamp;
/**
* 刷新时间
*/
private volatile long refreshTimestamp = -2;
private final ReentrantLock refreshLock = new ReentrantLock();
/**
* Cache to hold already generated MessageFormats per message code.
*/
private final ConcurrentMap<String, Map<Locale, MessageFormat>> cachedMessageFormats =
new ConcurrentHashMap<>();
public PropertiesHolder() {
this.properties = null;
this.fileTimestamp = -1;
}
public PropertiesHolder(Properties properties, long fileTimestamp) {
this.properties = properties;
this.fileTimestamp = fileTimestamp;
}
@Nullable
public Properties getProperties() {
return this.properties;
}
public long getFileTimestamp() {
return this.fileTimestamp;
}
public void setRefreshTimestamp(long refreshTimestamp) {
this.refreshTimestamp = refreshTimestamp;
}
public long getRefreshTimestamp() {
return this.refreshTimestamp;
}
/**
* 根据key获取内容
* @param code
* @return
*/
@Nullable
public String getProperty(String code) {
if (this.properties == null) {
return null;
}
return this.properties.getProperty(code);
}
/**
* 根据语言环境和内容key获取对应的多语言内容
* @param code 内容key
* @param locale 语言环境
* @return 多语言内容
*/
@Nullable
public MessageFormat getMessageFormat(String code, Locale locale) {
if (this.properties == null) {
return null;
}
//根据code获取多语言,一个code对应多个国家语言内容
Map<Locale, MessageFormat> localeMap = this.cachedMessageFormats.get(code);
//再根据语言环境locale获取对应国家的语言内容
if (localeMap != null) {
MessageFormat result = localeMap.get(locale);
if (result != null) {
return result;
}
}
//如果根据语言环境locale未从缓存中获取,则从配置中获取对应的内容
String msg = this.properties.getProperty(code);
if (msg != null) {
//如果code还没有缓存起来,创建一个新的多语言缓存容器(数据为空,语言环境locale与内容关联的缓存),将其赋值给localeMap
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map<Locale, MessageFormat> existing = this.cachedMessageFormats.putIfAbsent(code, localeMap);
if (existing != null) {
localeMap = existing;
}
}
//把locale与语言内容挂钩,然后存入缓存
MessageFormat result = createMessageFormat(msg, locale);
localeMap.put(locale, result);
return result;
}
return null;
}
}
}