Nacos实现SpringBoot国际化的增强

Cain ·
更新时间:2024-11-01
· 991 次阅读

一.  概述

阅读本文之前,你应该了解过SpringBoot的国际化实现与原理,在这里简单介绍下:

1. 国际化

国际化(internationalization),又称为i18n(因为这个单词从i到n有18个英文字母,因此命名)。对于某些应用系统而言,它需要发布到不同的国家地区,因此需要特殊的做法来支持,也即是国际化。通过国际化的方式,实现界面信息,各种提示信息等内容根据不同国家地区灵活展示的效果。比如在中国,系统以简体中文进行展示,在美国则以美式英文进行展示。如果使用传统的硬编码方式,是无法做到国际化支持的。

所以通俗来讲,国际化就是为每种语言配置一套单独的资源文件,保存在项目中,由系统根据客户端需要选择合适的资源文件。

2. SpringBoot对国际化的支持

SpringBoot默认提供了国际化的支持,它通过自动配置类MessageSourceAutoConfiguration实现。

org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration

这个类注册了MessageSource,用来获取国际化配置。

@Bean public MessageSource messageSource() { MessageSourceProperties properties = messageSourceProperties(); ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); if (StringUtils.hasText(properties.getBasename())) { messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray( StringUtils.trimAllWhitespace(properties.getBasename()))); } if (properties.getEncoding() != null) { messageSource.setDefaultEncoding(properties.getEncoding().name()); } messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale()); Duration cacheDuration = properties.getCacheDuration(); if (cacheDuration != null) { messageSource.setCacheMillis(cacheDuration.toMillis()); } messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat()); messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage()); return messageSource; }

在SpringBoot代码中实现原生国际化配置仅需要以下三步:

指定国际化资源路径

通过application.properties指定:

spring.messages.basename=classpath:i18n/messages

其中,i18n表示resources路径上的一个文件夹,messages就是这个文件夹下的资源文件名,例如:messages.properties、messages_zh_CN.properties、messages_en_US.properties 等。

 注入国际化Resolver对象

通过指定LocaleResolver对象,实现国际化策略。

@Bean public LocaleResolver localeResolver() { SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver(); sessionLocaleResolver.setDefaultLocale(Locale.CHINA); return sessionLocaleResolver; }

使用

在resources目录下建立i18n文件夹,文件夹中建立:messages.properties、messages_zh_CN.properties、messages_en_US.properties 三个文件,添加需要的配置即可。

同时,在需要使用的Bean中注入MessageSource对象,通过getMessage方法使用

String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

3. SpringBoot 国际化存在的问题与不足

通过以上三个步骤,我们实现了SpringBoot的原生国际化。但是在使用过程中,我们也发现了一些不足,这主要有:

配置是存储在jar包中的,不够灵活。

绝大部分公司系统都做了微服务化,应用太多,希望有一个地方可以统一管理配置。

希望配置读取是高效的,及时的。

国际化的工作,前后端都不应该过多参与,尽量在框架层面解决,而不是应用层传输国际化参数来解决。

4. SpringBoot 国际化增强的目标

在实际工作中,我们应该且有必要对国际化做进一步的增强,让它更能满足要求。基于上述的问题,我们做了一些改进,最终达到的效果如下:

1. 配置中心存储应用的国际化配置,配置支持动态刷新,实时生效。

2. 实现高效的配置读取。

3. 简化前后端的工作量。

因此,本文的后半部分将介绍如何通过Nacos实现SpringBoot国际化的增强。

二.  实施过程

1. 总体设计

根据需要,我们会将应用服务的配置存储到配置中心,每次客户端需要获取配置时,应用服务再到配置中心去拉取对应的配置,并最终返回。方案如下:

但是,这种方案存在的问题是,每次获取配置时,应用服务都需要去配置中心中获取配置,每次都需要发送HTTP请求,这在性能上是低效。因此,需要做出改进,改进方案如下:

如上图所示,与最初的方案相比,每次获取配置时,都会先从服务的本地缓存中拿,如果没有,再从配置中心中获取。但是,配置如何实现实时生效呢?这需要再对上述方案进行优化,最终优化方案如下:

应用服务启动时,从配置中心拉取配置,并存储配置到本地缓存。

客户端获取配置中,应用服务直接从本地缓存中获取。

客户端订阅应用服务的配置更新事件。当配置中心有配置更新时,主动推送配置到应用服务,应用服务重新更新本地缓存。

国际化配置近乎实时生效。

2. 实施步骤

Nacos配置

新增"提示语"的命名空间。

在Nacos上新增应用的国际化配置,命名空间选择"提示语",Data ID为:

user-message.properties,user-message_zh_CN.properties,user-message_en_US.properties。

里面的配置内容为:

user-message.properties:test=测试

user-message_zh_CN.properties:test=测试

user-message_en_US.properties:test=test

新增国际化配置

在application.yaml中新增以下国际化配置:

spring: messages: baseFolder: i18n/ basename: ${spring.application.name}-message encoding: UTF-8 cacheMillis: 10000

其中,各字段含义如下:

spring.messages.baseFolder:指定国际化配置存放的本地路径(在程序的当前路径下的路径)
spring.messages.basename:国际化配置名称
spring.messages.encoding:编码格式
spring.messages.cacheMillis:国际化配置刷新的时间间隔

代码改造

1~ 国际化解析器

新增自定义国际化解析器DefaultLocaleResolver,用于解析请求头的国际化信息。代码如下:

import org.apache.commons.lang3.StringUtils; import org.springframework.web.servlet.LocaleResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.util.Locale; /** * 自定义国际化解析器 * @author zz * @date 2020/2/28 15:37 **/ public class DefaultLocaleResolver implements LocaleResolver { @Override public Locale resolveLocale(HttpServletRequest request) { String lang = request.getHeader(LANG); Locale locale = Locale.getDefault(); if (StringUtils.isNotBlank(lang)){ String[] language = lang.split("_"); locale = new Locale(language[0], language[1]); HttpSession session = request.getSession(); session.setAttribute(LANG_SESSION, locale); }else{ HttpSession session = request.getSession(); Locale localeInSession = (Locale) session.getAttribute(LANG_SESSION); if (localeInSession != null){ locale = localeInSession; } } return locale; } @Override public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) { } /** * 请求header字段 */ private static final String LANG = "lang"; /** * session */ private static final String LANG_SESSION = "lang_session"; }

2~ 新增国际化配置

读取配置文件中的国际化配置,代码如下:

import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Component; /** * 国际化配置 * @author zz * @date 2020/2/28 15:38 **/ @Data @RefreshScope @Component @ConfigurationProperties(prefix = "spring.messages") public class MessageConfig { /** * 国际化文件目录 */ private String baseFolder; /** * 国际化文件名称 */ private String basename; /** * 国际化编码 */ private String encoding; /** * 缓存刷新时间 */ private long cacheMillis; }

3~ 注册国际化解析器,配置消息资源管理器

新增Spring配置,注册自定义国际化解析器DefaultLocaleResolver,同时注册ReloadableResourceBundleMessageSource用于实时获取国际化配置。

import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Primary; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.util.ResourceUtils; import org.springframework.web.servlet.LocaleResolver; import java.io.File; /** * Spring配置 * @author zz * @date 2020/2/28 15:50 **/ @Slf4j @Configuration public class SpringConfig { @Bean public LocaleResolver localeResolver(){ return new DefaultLocaleResolver(); } @Primary @Bean(name = "messageSource") @DependsOn(value = "messageConfig") public ReloadableResourceBundleMessageSource messageSource() { String path = ResourceUtils.FILE_URL_PREFIX + System.getProperty("user.dir") + File.separator + messageConfig.getBaseFolder() + File.separator + messageConfig.getBasename(); log.info("国际化配置内容:{}", messageConfig); log.info("国际化配置路径:{}", path); ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename(path); messageSource.setDefaultEncoding(messageConfig.getEncoding()); messageSource.setCacheMillis(messageConfig.getCacheMillis()); return messageSource; } /** * 应用名称 */ @Value("${spring.application.name}") private String applicationName; @Autowired private MessageConfig messageConfig; }

注:这里简单介绍下ReloadableResourceBundleMessageSource。ReloadableResourceBundleMessageSource是AbstractResourceBasedMessageSource的两个实现类之一,它提供强大的定时刷新配置文件的功能,支持应用在不重启的情况下重载配置文件,保证应用的长期稳定运行。所以,我们通过它来实现国际化信息的动态更新。

4~ 新增Nacos配置管理器

新增Nacos配置管理器,该类主要做两个工作:配置拉取,配置更新。

import com.alibaba.nacos.api.NacosFactory; import com.alibaba.nacos.api.PropertyKeyConst; import com.alibaba.nacos.api.config.ConfigService; import com.alibaba.nacos.api.config.listener.Listener; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; import java.util.Locale; import java.util.Properties; import java.util.concurrent.Executor; /** * Nacos配置管理器 * @author zz * @date 2020/2/28 16:30 **/ @Slf4j @Component public class NacosConfig { @Autowired public void init() { serverAddr = applicationContext.getEnvironment().getProperty("spring.cloud.nacos.config.server-addr"); dNamespace = applicationContext.getEnvironment().getProperty("spring.cloud.nacos.config.dNamespace"); if (StringUtils.isEmpty(dNamespace)) { dNamespace = DEFAULT_NAMESPACE; } initTip(null); initTip(Locale.CHINA); initTip(Locale.US); log.info("初始化系统参数成功!应用名称:{},Nacos地址:{},提示语命名空间:{}", applicationName, serverAddr, dNamespace); } private void initTip(Locale locale) { String content = null; String dataId = null; ConfigService configService = null; try { if (locale == null) { dataId = messageConfig.getBasename() + ".properties"; } else { dataId = messageConfig.getBasename() + "_" + locale.getLanguage() + "_" + locale.getCountry() + ".properties"; } Properties properties = new Properties(); properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr); properties.put(PropertyKeyConst.NAMESPACE, dNamespace); configService = NacosFactory.createConfigService(properties); content = configService.getConfig(dataId, DEFAULT_GROUP, 5000); if (StringUtils.isEmpty(content)) { log.warn("配置内容为空,跳过初始化!dataId:{}", dataId); return; } log.info("初始化国际化配置!配置内容:{}", content); saveAsFileWriter(dataId, content); setListener(configService, dataId, locale); } catch (Exception e) { log.error("初始化国际化配置异常!异常信息:{}", e); } } private void setListener(ConfigService configService, String dataId, Locale locale) throws com.alibaba.nacos.api.exception.NacosException { configService.addListener(dataId, DEFAULT_GROUP, new Listener() { @Override public void receiveConfigInfo(String configInfo) { log.info("接收到新的国际化配置!配置内容:{}", configInfo); try { initTip(locale); } catch (Exception e) { log.error("初始化国际化配置异常!异常信息:{}", e); } } @Override public Executor getExecutor() { return null; } }); } private void saveAsFileWriter(String fileName, String content) { String path = System.getProperty("user.dir") + File.separator + messageConfig.getBaseFolder(); try { fileName = path + File.separator + fileName; File file = new File(fileName); FileUtils.writeStringToFile(file, content); log.info("国际化配置已更新!本地文件路径:{}", fileName); } catch (IOException e) { log.error("初始化国际化配置异常!本地文件路径:{}异常信息:{}", fileName, e); } } /** * 应用名称 */ @Value("${spring.application.name}") private String applicationName; /** * 命名空间 */ private String dNamespace; /** * 服务器地址 */ private String serverAddr; @Autowired private MessageConfig messageConfig; @Autowired private ConfigurableApplicationContext applicationContext; private static final String DEFAULT_GROUP = "DEFAULT_GROUP"; private static final String DEFAULT_NAMESPACE = "515667c5-0450-4d1f-b14f-6f243079b6fb"; }

在该类中,主要做了几个事情:

第一,应用启动时,通过init方法初始化配置国际化配置。

第二,在init方法中,会拉取Nacos服务端配置并写入到本地缓存,同时注册一个监听器,实时监听配置的变化并及时更新本地配缓存。

最后,读取的命名空间通过spring.cloud.nacos.config.dNamespace指定,如果没有取到,则取默认值。

5~ 新增国际化配置获取工具类

通过注入MessageSource,通过getMessage方法获取对应的国际化配置。

import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.NoSuchMessageException; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Component; import java.util.Locale; /** * 国际化配置获取工具类 * @author zz * @date 2020/2/28 17:00 **/ @Slf4j @Component public class PropertiesTools { public String getProperties(String name) { try { Locale locale = LocaleContextHolder.getLocale(); return messageSource.getMessage(name, null, locale); } catch (NoSuchMessageException e) { log.error("获取配置异常!异常信息:{}", e); } return null; } @Autowired private MessageSource messageSource; }

6~ 使用

通过注入PropertiesTools,调用getProperties获取国际化配置。

import com.demo.util.PropertiesTools; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * 样例 * @author zz * @date 2020/2/28 17:30 **/ @RestController public class DemoController { /** * 获取国际化配置 * @param name 配置名称 * @return String */ @PostMapping(value = "/getProperties") public String getProperties (String name) { return propertiesTools.getProperties(name); } @Autowired private PropertiesTools propertiesTools; }

验证

利用postman进行验证,结果如下:

1~ 简体中文版:

2~ 美式英文版:

三. 总结

经过上述改造,我们实现了:通过Nacos增强了SpringBoot的国际化。最后,我们做个简单的总结。

1. 配置文件统一通过 Nacos 配置中心管理,实时更新,实时生效。

2. 读取配置文件时,会读取本地缓存文件,提高效率。更新配置后,服务端会推送配置到客户端,客户端更新本地的配置文件。

3. 配置文件其实不是实时生效,这取决于spring.messages.cacheMillis(刷新间隔)。


作者:小缘缘



nacos springboot

需要 登录 后方可回复, 如果你还没有账号请 注册新账号
相关文章