问题现象
一个 Spring Boot 项目,在微服务的 resources 路径下有 messages.properties
和 messages_zh.properties
两个文件,同时还有一个 Controller
对外提供了一个接口,这个接口会根据 key 去获取对应的国际化信息然后返回。
现在遇到了一个很坑的问题是明明传入的 Locale
是 Locale.US
时,但是获取到的国际化信息居然是中文的。
复现问题的 Demo 项目文件结构如下 : 整体就一个 HelloController
提供接口,然后有messages.properties
和 messages_zh.properties
两个国际化文件。
messages.properties 文件中的内容是:
json
hello=hello
messages_zh_CH.properties 文件中的内容是:
json
hello=你好
HelloController
对外提供了一个 /hello
接口,根据 hello 这个 key 去国际化文件获取数据并返回。这里为了复现问题,这里手动传入 Locale.US
:
java
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Locale;
@RestController
public class HelloController {
@Autowired
private MessageSource messageSource;
@GetMapping("/hello")
public String hello() {
return messageSource.getMessage("hello", null, Locale.US);
}
}
调用接口之后返回的结果如下: 同时查看操作系统的语言明明是英文的,但是通过 DEBUG 程序发现,
Locale.getDefault()
返回居然是 zh_CN
。但是手动重启一下服务之后,它获取的 locale
又和操作系统语言是匹配得上的了。好吧,明明只有一个问题的,现在定位了一下,有两个问题了 。

问题原理
Spring Boot 查找国际化文件的过程分析
首先最开始的时候项目路径下实际上是有 messages.properties,messages_en_US.properties 以及 messages_zh_CN.properties 三个文件的,但是后来认为找不到对应 locale 的相匹配文件时,默认就会退化到 messages.properties 文件中查找,而 messages.properties 文件中的实际上配置的就是英文的国际化,因此就把 messages_en_US.properties 给删了(这就是个血淋淋的教训啊,程序能跑就不要动,真的手贱啊),而问题的关键就出在对默认退化查找的文件这个理解有误。
Spring Boot 实际上提供了一个名为 spring.messages.fallback-to-system-locale
的配置项,它的作用就是决定在找不到和传入的 locale 相匹配的国际化文件的时候,默认查找的文件。
在 spring-boot-autoconfigure 这个 jar 包中的 spring-configuration-metadata.json
中有对这个配置项的详细说明:
json
{
"name": "spring.messages.fallback-to-system-locale",
"type": "java.lang.Boolean",
"description": "Whether to fall back to the system Locale if no files for a specific Locale have been found. if this is turned off, the only fallback will be the default file (e.g. "messages.properties" for basename "messages").",
"sourceType": "org.springframework.boot.autoconfigure.context.MessageSourceProperties",
"defaultValue": true
}
从上面的说明可以看出,当该配置项关闭时才会默认去 messages.properties 文件中查找,否则就会去和当前系统 locale 相同的文件中去查找,并且该配置项的默认值是 true
。
基于以上信息来解释一下为什么上面传入 Locale.US
拿到的结果还是中文? spring.messages.fallback-to-system-locale
配置项的默认值是 true
且项目文件结构下没有 messages.en_US.properties
文件。
因为传入的 locale
是 Locale.US
,所以首先会去 messages.en_US.properties
文件中查找,但是并没有这个文件存在。接着就应该去和系统 locale
相匹配的文件中去查找,通过 DEBUG 发现 Locale.getDefault()
返回的值是 zh_CN
,按照这个结果就应该去 messages.zh_CN.properties
文件中去查找,这个文件是存在的,从这个文件中获取到值就返回了,从而拿到的就是中文。
那 spring.messages.fallback-to-system-locale
这个配置项的默认值又是在什么时候配置的呢?这个默认值实际上是在 MessageSourceProperties
这个类的构造函数中初始化为 true
的,代码如下:
java
@ConfigurationProperties("spring.messages")
public class MessageSourceProperties {
private List<String> basename = new ArrayList(List.of("messages"));
private List<Resource> commonMessages;
private Charset encoding;
@DurationUnit(ChronoUnit.SECONDS)
private Duration cacheDuration;
private boolean fallbackToSystemLocale;
private boolean alwaysUseMessageFormat;
private boolean useCodeAsDefaultMessage;
public MessageSourceProperties() {
this.encoding = StandardCharsets.UTF_8;
//这里设置默认值为true
this.fallbackToSystemLocale = true;
this.alwaysUseMessageFormat = false;
this.useCodeAsDefaultMessage = false;
}
}
然后在 MessageSourceAutoConfiguration
类中会配置将 MessageSourceProperties
加载为一个 Spring 的 Bean,然后还会配置一个 MessageSource
类型的 Bean,然后这个 Bean 依赖 MessageSourceProperties
的默认值值来对自己进行初始化,从而将自己的 fallbackToSystemLocale
属性也设置为了 true
,代码如下:
java
@AutoConfiguration
@ConditionalOnMissingBean(
name = {"messageSource"},
search = SearchStrategy.CURRENT
)
@AutoConfigureOrder(Integer.MIN_VALUE)
@Conditional({ResourceBundleCondition.class})
//配置加载MessageSourceProperties为Spring的Bean
@EnableConfigurationProperties({MessageSourceProperties.class})
@ImportRuntimeHints({MessageSourceRuntimeHints.class})
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = new Resource[0];
//这里依赖注入MessageSourceProperties这个Bean
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (!CollectionUtils.isEmpty(properties.getBasename())) {
messageSource.setBasenames((String[])properties.getBasename().toArray(new String[0]));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
//这里设置MessageSource的fallbackToSystemLocale的属性值为true
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
messageSource.setCommonMessages(this.loadCommonMessages(properties.getCommonMessages()));
return messageSource;
}
}
操作系统语言环境和获取的默认Locale不一致分析
这个问题是因为部署的时候使用的 Jenkins 打包,然后上传到服务器上,然后通过 SSH 连接到服务器上执行命令启动的 Java 进程。 在建立 SSH 连接的时候,客户端会把自己的语言环境变量发送给服务端设置,这个是在 /etc/ssh/sshd_config
配置文件中有一个 SendEnv
配置项来决定的:
而 Jenkins 服务器的语言是设置的是中文,因此在当前打开的 SSH 连接里面执行 echo $LANG
输出的结果就是 zh_CN
。Java 进程启动的时候获取到的系统语言就是 zh_CN
,从而通过 Locale.getDefault()
获取到的也是 zh_CN
。重启之后好了的原因就是,重启之后又重新读取到了操作系统的语言环境为 en_US
,所以 Locale.getDefault()
返回的结果也正确了。