Springboot国际化使用Nacos做动态配置

Springboot国际化使用Nacos做动态配置

  • 背景
  • 网上已有的现成实现
  • Spring国际化的实现
  • i18n相关配置
  • Nacos相关配置
        • 代码配置如下:
  • 自定义的MessageSource接口实现类代码


背景

公司项目需求将国际化配置放入Nacos配置中心,以实现动态修改更新国际化配置而避免后端服务发版


网上已有的现成实现

在网上百度了一番后发现有两种实现
1.Nacos实现SpringBoot国际化的增强
2.Springboot基于Nacos的动态国际化
经过实验后第二种方式可以实现nacos动态配置国际化,本文也是参照方式二去做的,第一种未做实验。

Spring国际化的实现

Spring的国际化基于MessageSource接口,此接口为最顶层的接口
其主要有两个实现类:

  • ResourceBundleMessageSource
  • ReloadableResourceBundleMessageSource

ResourceBundleMessageSource是Springboot国际化自动装配类MessageSourceAutoConfiguration的默认的实现类,其就是读取国际化properties文件的配置缓存国际化数据。

ReloadableResourceBundleMessageSource是可重加载国际化配置文件的实现,其中对于国际化配置有两级缓存,分别是文件名-国际化配置数据缓存、时区-对应的配置缓存。
由于spring中的两种实现都与我们读取nacos配置并动态更新的需求不符合,所以只能实现自己的MessageSource接口实现类。

i18n相关配置

代码如下(示例):

@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上配置国际化文件名
在这里插入图片描述
同时在nacos上新建对应的多语言配置文件
Springboot国际化使用Nacos做动态配置_第1张图片

代码配置如下:

@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("国际化初始配置结束");
    }

}

自定义的MessageSource接口实现类代码

@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;
        }
    }
}

你可能感兴趣的:(spring,boot,spring,boot,后端,java)