明明说好的国际化,可你却还是返回了中文

问题现象

一个 Spring Boot 项目,在微服务的 resources 路径下有 messages.propertiesmessages_zh.properties 两个文件,同时还有一个 Controller 对外提供了一个接口,这个接口会根据 key 去获取对应的国际化信息然后返回。

现在遇到了一个很坑的问题是明明传入的 LocaleLocale.US 时,但是获取到的国际化信息居然是中文的

复现问题的 Demo 项目文件结构如下 : 整体就一个 HelloController 提供接口,然后有messages.propertiesmessages_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 文件。

因为传入的 localeLocale.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() 返回的结果也正确了。

相关推荐
饮长安千年月12 分钟前
JavaSec-SpringBoot框架
java·spring boot·后端·计算机网络·安全·web安全·网络安全
代码匠心13 分钟前
从零开始学Flink:揭开实时计算的神秘面纱
java·大数据·后端·flink
Livingbody32 分钟前
Transformers Pipeline 加载whisper模型实现语音识别ASR
后端
异常君1 小时前
@Bean 在@Configuration 中和普通类中的本质区别
java·spring·面试
WindSearcher1 小时前
大模型微调相关知识
后端·算法
考虑考虑1 小时前
Jpa中的@ManyToMany实现增删
spring boot·后端·spring
你不是我我2 小时前
【Java开发日记】说一说 SpringBoot 中 CommandLineRunner
java·开发语言·spring boot
yuan199972 小时前
Spring Boot 启动流程及配置类解析原理
java·spring boot·后端
楚歌again2 小时前
【如何在IntelliJ IDEA中新建Spring Boot项目(基于JDK 21 + Maven)】
java·spring boot·intellij-idea