目前项目越来越偏向于国际化,所以,我们在开发中也要偏向于国际化。这篇文章讲述的就是在spring boot项目中实现国际化翻译。
其实,我们实现国家化是很简单的事情,不需要引用任何的外部依赖。
单体项目
我们日常开发中,如果要输出一段文字,我们可能直接就是:
log.info("用户未登录");
但是,如果我们要启用国际化,我们就不能这样写了。而是要写一个编码来对应相应的语言信息;
如:
@Resource
private MessageSource messageSource;
@Test
void contextLoads() {
Locale locale =Locale.US;
String message = messageSource.getMessage("user.login",
null, locale);
}
然后,得益于spring boot的自动装配原理,我们直接在对应的中文文件和英文文件中把这个编码对应的数据给补上就可以了;
如,我们创建两个文件分别用来记录中英文。
messages_zh_***.properties和messages_en_UK.properties文件,并在文件中指定对应的中英文数据为:
user.login=用户未登录
user.login=User not logged in
这样,我们就可以通过访问编码,(user.login)然后再由相应的编码转化为某一中语言就可以了。
spring官方已经提供了一个bean用来默认实现多语言问题;MessageSource
我们在使用的时候只需要把这个bean注入进来,然后,直接使用getMessage方法就可以了。
这个方法有三个实现,我们现在只讲最常用的一种。传入三个参数。
第一个参数为语言编码,第二个参数为占位符,第三个参数为语言类型。(这个语言类型现在大部分常用的国家已经有了,可以直接通过枚举得到。如果对应的国家类型不存在时,自行添加即可)
应该很好理解的
这个是正常输出的结果,需要注意的一点是,如果你写的语言编码在文件中不存在时,那么会直接报错的
占位符也很好理解,如:我们有一段文字需要:当前时间下{},用户名 {} 已存在
那么,我们只需要在第二个参数中传递一个Object[] 类型的数组即可。
如:
user.hello=At the current time {0}, the username {1} already exists
@Test
void contextLoads() {
Locale locale =Locale.US;
Object[] params = new Object[]{
new Date(), // 这个值会替换 {0}
"张三" // 这个值会替换 {1}
};
String message = messageSource.
getMessage("user.hello",params, locale);
log.info("国际化的数据为:{}",message);
}
那么,这样就会直接把我们传递的参数给对应起来:
这是基于spring boot项目的自动装配的原理实现的方案。
我们只需要配置文件中指定国际化的路径:
spring:
messages:
basename: i18n/messages
encoding: UTF-8
然后,就可以直接在资源路径下存放相应的国际化文件了。
要能保证到相应的资源路径一定是在对应的路径下存放。如果要更改路径,记得同时更改对应的文件地址和名称,如:
spring:
messages:
basename: i18n/mess
encoding: UTF-8
那么,对应的资源文件下就应该是:
这样才对。我们一版会在项目中写一个拦截器,根据前端传递的数据来确认本次请求使用中文/英文数据。我们可以这样写:
1、注册一个MessageSource的bean:
@Configuration
public class MessageSourceConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:i18n/mess");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(3600); // 缓存1小时
return messageSource;
}
}
你也可以不注册这个bean,那么就需要显式在配置文件中指定路径和编码了。配置了这个bean之后,就可以不在配置文件中再写那个路径了。有的人就是犟,那么,我在bean注册时指定了一个路径,我又在application.yml文件中指定了不同的路径,那么会使用那个路径?
会使用你注册bean时的路径,你在配置文件中写的路径将会被覆盖掉。这也是spring boot官方的默认执行流程;我们知道spring boot默认是 “约定大于配置,配置优于编码” 的,但是,我们在真正执行时,肯定是编码优先的、其次是配置文件中写死的,最后才是官方约定的。
2、创建语言拦截器:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.***ponent;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Locale;
@***ponent
public class LanguageInterceptor implements HandlerInterceptor {
private static final String LANGUAGE_HEADER = "language";
private static final String DEFAULT_LANGUAGE = "zh";
private static final String EN_LANGUAGE = "en";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String language = request.getHeader(LANGUAGE_HEADER);
if (StringUtils.hasText(language)) {
// 根据请求头设置语言环境
Locale locale = parseLocale(language);
LocaleContextHolder.setLocale(locale);
} else {
// 默认语言
LocaleContextHolder.setLocale(Locale.forLanguageTag(DEFAULT_LANGUAGE));
}
return true;
}
@Override
public void after***pletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 清理ThreadLocal,避免内存泄漏
LocaleContextHolder.resetLocaleContext();
}
private Locale parseLocale(String language) {
return switch (language.toLowerCase()) {
case DEFAULT_LANGUAGE -> Locale.CHINA;
case EN_LANGUAGE -> Locale.US;
default -> Locale.forLanguageTag(DEFAULT_LANGUAGE);
};
}
}
如果你的名称不是language,对应的中英文标识这些都可以自行更改。
3、注册拦截器
@Configuration
public class WebMv***onfig implements WebMv***onfigurer {
@Autowired
private LanguageInterceptor languageInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(languageInterceptor)
.addPathPatterns("/**");
}
}
4、创建自动化的MessageSource工具类
@***ponent
public class I18nMessageUtil {
@Autowired
private MessageSource messageSource;
/**
* 自动使用当前线程的语言环境获取消息
*/
public String getMessage(String code) {
return messageSource.getMessage(code, null, LocaleContextHolder.getLocale());
}
/**
* 根据消息代码和参数数组获取国际化消息。
*
* @param code 消息代码,用于从消息源中检索相应的国际化消息。
* @param args 消息参数数组,用于替换消息中的占位符。
* @return 格式化后的国际化消息字符串。
*/
public String getMessage(String code, Object[] args) {
return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
}
/**
* 获取国际化消息。
*
* @param code 消息代码,用于从资源文件中查找对应的消息。
* @param args 消息参数,用于替换消息中的占位符。
* @param defaultMessage 如果没有找到对应的消息,则返回此默认值。
* @return 国际化后的消息字符串。
*/
public String getMessage(String code, Object[] args, String defaultMessage) {
return messageSource.getMessage(code, args, defaultMessage, LocaleContextHolder.getLocale());
}
}
那么,我们就可以直接在代码中使用了。
如下代码所示:
@RestController
public class TestController {
@Resource
private I18nMessageUtil i18nMessageUtil;
@GetMapping("/test")
public String test() {
return i18nMessageUtil.getMessage
("user.hello",new Object[]{"张乔", "2025-11-01"});
}
}
我们可以简单的测试一下:
英文环境下的测试结果为:
那么,至此为止。我们在spring boot项目中整合多语言翻译已经完成了。
但是,思考一个问题,我们这种情况只适合于单体项目。如果是微服务项目的话,我们就需要为每个模块中都书写中英文的语言配置,这无疑有很多重复的词语。那么,我们对于微服务项目的国际化,可以使用nacos的配置中心来做,把中英文的词库只写一次,然后给其他的微服务使用就可以了。
微服务项目
我们在微服务项目中,我们就可以只在配置中心写两套词库,然后,所有的模块全部都依赖与这两个词库,避免出现重复书写。
首先,引入相应的naocs的配置。
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2023.0.3.3</version>
</dependency>
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2023.0.3.3</version>
</dependency>
在配置文件中指定naocs的配置信息:
spring:
application:
name: test-fanyi
cloud:
nacos:
discovery:
server-addr: 192.168.231.110:8848
config:
server-addr: 192.168.231.110:8848 #配置中心地址
config:
import:
- nacos:name-zh-***.yml #配置文件名称
- nacos:name-en-US.yml #配置文件名称
我这里引入了两个配置文件。中文词库和英文词库,那么,你的nacos中也应该建立相应的配置文件信息。
相应的文件内的信息为:
wel***e:
message: 欢迎使用我们的服务
ll: 你好啊,{0}。伐木工。现在时间是:{1}
user:
notfound: 用户不存在
wel***e:
message: Wel***e to our service
ll: Hello, {0}. Lumberjack. The current time is {1}
user:
notfound: User not found
好的,那么,我们现在就可以使用naocs的配置文件来承担我们国际化的配置了。
只不过,我们需要注意的一点是:我门不需要再手动的指定message文件的路径了,之前在本地项目中,我们有springboot来帮我们映射文件,现在需要我们手动映射文件。
创建一个nacos配置文件工具,用来读取我们远程的配置文件。
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.***ponent;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@***ponent
public class NacosI18nUtil {
private final ConfigurableEnvironment configEnv;
private final Map<String, Map<String, String>> configCache = new ConcurrentHashMap<>();
public NacosI18nUtil(Environment environment) {
this.configEnv = (ConfigurableEnvironment) environment;
}
/**
* 获取消息 - 自动使用当前语言环境
*/
public String getMessage(String code) {
return getMessage(code, null, LocaleContextHolder.getLocale());
}
/**
* 获取带参数的消息 - 自动使用当前语言环境
*/
public String getMessage(String code, Object[] args) {
return getMessage(code, args, LocaleContextHolder.getLocale());
}
/**
* 核心方法:根据语言环境获取消息
*/
public String getMessage(String code, Object[] args, Locale locale) {
if (code == null || code.trim().isEmpty()) return "";
String languageTag = getLanguageTag(locale);
String message = getFromNacos(code, languageTag);
// 回退到中文
if (message == null && !"zh-***".equals(languageTag)) {
message = getFromNacos(code, "zh-***");
}
if (message == null) {
log.debug("未找到配置: {}, 语言: {}", code, languageTag);
return code;
}
return replaceArgs(message, args);
}
/**
* 从Nacos配置源获取值
*/
private String getFromNacos(String code, String languageTag) {
try {
Map<String, String> configMap = configCache.***puteIfAbsent(languageTag, this::loadConfig);
return configMap.get(code);
} catch (Exception e) {
log.warn("获取Nacos配置失败: {} {}", languageTag, code, e);
return null;
}
}
/**
* 加载指定语言的配置
*/
private Map<String, String> loadConfig(String languageTag) {
String sourceName = "DEFAULT_GROUP@name-" + languageTag + ".yml";
return configEnv.getPropertySources().stream()
.filter(ps -> sourceName.equals(ps.getName()))
.findFirst()
.map(ps -> (Map<String, String>) ps.getSource())
.orElseGet(() -> {
log.warn("配置源未找到: {}", sourceName);
return Map.of();
});
}
/**
* 参数替换
*/
private String replaceArgs(String message, Object[] args) {
if (args == null) return message;
for (int i = 0; i < args.length; i++) {
message = message.replace("{" + i + "}",
args[i] != null ? args[i].toString() : "");
}
return message;
}
/**
* 语言标签转换
*/
private String getLanguageTag(Locale locale) {
if (locale == null) return "zh-***";
String lang = locale.getLanguage();
return "zh".equals(lang) ? "zh-***" : "en-US";
}
/**
* 清空缓存(用于热更新)
*/
public void refresh() {
configCache.clear();
log.info("国际化配置缓存已刷新");
}
}
这是一个nacos的配置类,用来读取特定nacos配置文件的属性,并把属性放在缓存中。为了项目的热更新,我们还需要写一个监听器,用来监听nacos配置文件的更新,并清空缓存。
代码如下:
@***ponent
@Slf4j
public class NacosConfigListener {
@Autowired
private NacosI18nUtil nacosI18nUtil;
@EventListener
public void handleRefreshEvent(EnvironmentChangeEvent event) {
// 当配置发生变化时自动刷新缓存
nacosI18nUtil.refresh();
log.info("检测到配置变化,已刷新国际化缓存");
}
}
当Nacos中修改配置并发布后,Spring Cloud会自动触发 EnvironmentChangeEvent,我们的监听器会捕获这个事件并清空缓存,下次请求时就会加载最新配置。
还有语言的拦截器和注册拦截器这两个是保持不变的。
创建拦截器:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.***ponent;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Locale;
@***ponent
public class LanguageInterceptor implements HandlerInterceptor {
private static final String LANGUAGE_HEADER = "language";
private static final String DEFAULT_LANGUAGE = "zh";
private static final String EN_LANGUAGE = "en";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String language = request.getHeader(LANGUAGE_HEADER);
if (StringUtils.hasText(language)) {
// 根据请求头设置语言环境
Locale locale = parseLocale(language);
LocaleContextHolder.setLocale(locale);
} else {
// 默认语言
LocaleContextHolder.setLocale(Locale.forLanguageTag(DEFAULT_LANGUAGE));
}
return true;
}
@Override
public void after***pletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 清理ThreadLocal,避免内存泄漏
LocaleContextHolder.resetLocaleContext();
}
private Locale parseLocale(String language) {
return switch (language.toLowerCase()) {
case DEFAULT_LANGUAGE -> Locale.CHINA;
case EN_LANGUAGE -> Locale.US;
default -> Locale.forLanguageTag(DEFAULT_LANGUAGE);
};
}
}
注册相应的拦截器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMv***onfigurer;
@Configuration
public class WebMv***onfig implements WebMv***onfigurer {
@Autowired
private LanguageInterceptor languageInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(languageInterceptor)
.addPathPatterns("/**");
}
}
这样,我们就可以直接使用了;
使用如下所示:
@Slf4j
@RestController
public class TestController {
@Autowired
private NacosI18nUtil nacosI18nUtil;
@GetMapping("/test")
public String test() {
Locale locale = LocaleContextHolder.getLocale();
log.info("本地文件的语言类型为----->{}",locale);
Object[] params = new Object[]{
"张三", // 这个值会替换 {0}
new Date(), // 这个值会替换 {1}
};
return nacosI18nUtil.getMessage("wel***e.ll",params);
}
}
我们在NacosI18nUtil 工具类中定义了一些我们国际化常用的方法,我们直接使用就可以了。
这是我们中文环境下。我们的英文环境下,直接切换请求头的参数就可以了;
我们可以尝试在不重启项目的时候更新一下nacos的配置文件中的属性,那么,我们通过监听器就可以监听到文件改变,并清空缓存,下次请求时就会加载最新配置。
如上所示;
至此,我们本篇文章,spring boot项目整合国际化就完成了。我们主要分了两步,单体项目中实现国际化和微服务项目中使用nacos作为统一的配置中心的国际化。