Springboot基于Nacos的动态国际化

背景

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

面向搜索引擎

现成的实现

基于文件下载的nacos国际化
这个是将nacos的配置通过http接口读取到内存,再写到磁盘上,通过ReloadableResourceBundleMessageSource类定时检查资源文件是否更新来重新读取国际化配置。此方法直接利用Spring框架已经提供的轮子来实现功能,但是感觉缺点也很明显:

  1. 已经通过http调用读取配置数据到了内存,还要经过IO操作落到磁盘上,再读取磁盘上的文件到内存更新国际化配置数据,为何不直接修改?
  2. 项目部署时会打包,更新配置信息落到磁盘还要修改jar包
  3. 如果将国际化配置文件放到项目外,作为外部资源文件,那么我们为何还需要nacos,直接更新了就上传新的配置文件覆盖原来的就行了,且现在都容器化部署,外部文件极为不便

因此,此方案被放弃

Spring国际化的实现

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

  • ResourceBundleMessageSource
  • ReloadableResourceBundleMessageSource

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

ReloadableResourceBundleMessageSource是可重加载国际化配置文件的实现,其中对于国际化配置有两级缓存,分别是文件名-国际化配置数据缓存、时区-对应的配置缓存。

思路

由于这两种实现都依赖于读取文件,与我们读取nacos配置并动态更新的需求不符合,所以只能实现自己的MessageSource接口实现类了,又因为读取nacos配置是网络请求,肯定不能每次国际化翻译都进行请求,则本地缓存、增量更新尤为重要。因此我们可以基本结构沿用ReloadableResourceBundleMessageSource的实现,只修改生成Properties对象的方法,换成请求nacos服务获取配置信息来组装,这样还可直接沿用其缓存机制。

读取配置

无缓存时请求nacos获取配置信息,组装为Properties对象返回
有缓存时直接读取本地缓存(沿用ReloadableResourceBundleMessageSource的实现)

// 成员变量,在创建对象时设置进来
private NacosConfigManager nacosConfigManager;
private String nacosGroup;
// 查询配置数据,组装为Properties对象
protected Properties loadProperties(String fileName) throws IOException, NacosException {
    // 从nacos中读取配置
    Properties props = newProperties();
    // nacos查询配置信息是使用文件名,而沿用ReloadableResourceBundleMessageSource获取的fileName仅为前缀,故此处拼接了一个后缀
    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;
}

增量更新

由于本地缓存是放入ConcurrentMap,所以我们更新缓存就直接操作对应的Map,替换或清空对应key的value即可

    // 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<>();
   /**
     * 强制刷新
     *
     * @param fileName 文件名
     * @param config   变更后的配置内容
     */
    public void forceRefresh(String fileName, String config) throws IOException {
    /*
    此处只是简单的实现功能,对于this.cachedMergedProperties仅仅简单粗暴的全部清空,细化的话可以考虑仅清空对应时区的数据
    */
        synchronized (this) {
            Properties props = newProperties();
            ByteArrayInputStream inputStream = new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8));
            this.propertiesPersister.load(props, inputStream);
            // 此参数好像是ReloadableResourceBundleMessageSource用来判断是否重新读取资源配置信息,我们通过主动读取nacos和等待nacos通知变化来更新数据,所以可以不用ReloadableResourceBundleMessageSource的重新读取功能
            long fileTimestamp = -1;
            NacosBundleMessageSource.PropertiesHolder propHolder = new NacosBundleMessageSource.PropertiesHolder(props, fileTimestamp);
            this.cachedProperties.put(fileName, propHolder);

            this.cachedMergedProperties.clear();
        }
    }

i18n相关配置

将我们自己实现的NacosBundleMessageSource放入容器,注意对象名必须为messageSourcee,使用此名字则Springboot国际化自动装配类才不会自动装载国际化实现类


@Configuration(proxyBeanMethods = false)
// 缺少名为messageSourcee的bean时才加载
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
// 国际化自动装配类
public class MessageSourceAutoConfiguration {...}
// 常量
public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {

	/**
	 * Name of the MessageSource bean in the factory.
	 * If none is supplied, message resolution is delegated to the parent.
	 * @see MessageSource
	 */
	public static final String MESSAGE_SOURCE_BEAN_NAME = "messageSource";
	// ...
}
@Configuration
public class I18Config {
    @Value("${spring.messages.basename}")
    private String baseName;

    @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("web_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.getParameter("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中配置的国际化信息配置文件,主要做两件事:

  1. 遍历每个文件名,初始化查询并缓存国际化信息(代码里未做)
  2. 遍历每个文件名,设置对应的监听器监听nacos的配置变更
@Slf4j
@Component
@RequiredArgsConstructor
public class NacosConfig {

    @Value("${spring.cloud.nacos.discovery.group}")
    private String group;

    private final NacosConfigManager nacosConfigManager;
    private final NacosBundleMessageSource messageSource;

    @PostConstruct
    public void init() throws Exception {
        // 基本国际化文件名、各个时区的国际化文件名
        List<String> fileNameList = new ArrayList<>();
        fileNameList.add("web_message");
        String[] langs = {"zh_CN", "en_US"};
        for (String lang : langs) {
            fileNameList.add("web_message" + "_" + lang);
        }
        // TODO 初始化本地国际化配置信息
        // 遍历添加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("国际化初始配置结束");
    }

}

NacosBundleMessageSource代码

@Getter
@Setter
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();
        }
    }

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

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

    protected Properties newProperties() {
        return new Properties();
    }

    protected class PropertiesHolder {

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

        @Nullable
        public String getProperty(String code) {
            if (this.properties == null) {
                return null;
            }
            return this.properties.getProperty(code);
        }

        @Nullable
        public MessageFormat getMessageFormat(String code, Locale locale) {
            if (this.properties == null) {
                return null;
            }
            Map<Locale, MessageFormat> localeMap = this.cachedMessageFormats.get(code);
            if (localeMap != null) {
                MessageFormat result = localeMap.get(locale);
                if (result != null) {
                    return result;
                }
            }
            String msg = this.properties.getProperty(code);
            if (msg != null) {
                if (localeMap == null) {
                    localeMap = new ConcurrentHashMap<>();
                    Map<Locale, MessageFormat> existing = this.cachedMessageFormats.putIfAbsent(code, localeMap);
                    if (existing != null) {
                        localeMap = existing;
                    }
                }
                MessageFormat result = createMessageFormat(msg, locale);
                localeMap.put(locale, result);
                return result;
            }
            return null;
        }
    }
}
  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
对于Spring Boot集成Nacos动态路由,你可以通过使用Spring Cloud Gateway来实现。下面是一个简单的示例: 1. 首先,确保你的项目中引入了以下依赖: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-nacos-discovery</artifactId> </dependency> ``` 2. 在application.properties或application.yml中配置Nacos服务发现和网关的相关信息: ```yaml spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true routes: - id: route1 uri: lb://service-provider # 这里的service-provider是Nacos中注册的服务名 predicates: - Path=/api/** # 路由规则 - id: route2 uri: https://example.com # 直接指定URL的路由规则 ``` 3. 创建一个`@Configuration`注解的类,来定义路由规则: ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; import static org.springframework.web.reactive.function.server.RequestPredicates.*; import static org.springframework.web.reactive.function.server.RouterFunctions.route; @Configuration public class GatewayConfig { @Bean public RouterFunction<ServerResponse> fallback() { return route(request -> true, request -> ServerResponse.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue("Fallback response"))); } } ``` 这里的`fallback()`方法定义了一个默认的回退处理,当所有的路由规则都不匹配时,会返回一个自定义的回退响应。 4. 启动你的Spring Boot应用程序,它将会从Nacos注册中心获取服务信息并进行动态路由。 注意:这只是一个简单的示例,实际应用中可能还需要进行更多的配置和处理。你可以参考Spring Cloud Gateway和Spring Cloud Alibaba Nacos的官方文档来了解更多详细信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值