文章目录
前言
SpringBoot 提供了国际化功能,其原理是将配置的各个语言资源文件信息,以Map 的形式进行缓存。当前端请求给定某个语言标识时(一般是放到请求头中
),拿去指定的语言标识去获取响应的响应信息。在Springboot 项目启动时,由MessageSourceAutoConfiguration 类进行消息资源自动配置。
该类存在 @Conditional 条件注解,也就是说必须满足某个条件是才会进行自动装载配置。ResourceBundleCondition 类用于判断是否满足自动注入条件。 getMatchOutcome 用于返回一个 ConditionOutcome 对象,用于后续判断是否满足自动注入条件。该方法会自动读取spring.messages.basename 配置的资源文件地址信息,通过getResources 方法获取默认的文件资源。如果该资源不存在,则不满足自动注入条件。getResources 明确标注了只能从classpath*
下拿去资源文件,文件类型为properties。
java
/**
* 判断是否满足自动注入条件
* @param context the condition context
* @param metadata the annotation metadata
* @return
*/
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
ConditionOutcome outcome = cache.get(basename);
if (outcome == null) {
outcome = getMatchOutcomeForBasename(context, basename);
cache.put(basename, outcome);
}
return outcome;
}
private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {
for (Resource resource : getResources(context.getClassLoader(),name)) {
if (resource.exists()) {
return ConditionOutcome.match(message.found("bundle").items(resource));
}
}
}
return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
}
//读取消息资源文件,从classpath下寻找格式为properties类型的资源文件
private Resource[] getResources(ClassLoader classLoader, String name) {
String target = name.replace('.', '/');
try {
return new PathMatchingResourcePatternResolver(classLoader)
.getResources("classpath*:" + target + ".properties");
}
catch (Exception ex) {
return NO_RESOURCES;
}
}
MessageSourceProperties 类则用于配置消息源的配置属性。在ResourceBundleCondition 返回条件成立的情况下,会通过注解Bean 进行注入。读取前缀带有spring.messages的配置信息。
java
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
MessageSourceProperties类可配置的属性并不多,具体属性含义用途如下:
java
#配置国际化资源文件路径,基础包名名称地址
spring.messages.basename=il8n/messages
#编码格式,默认使用UTF-8
spring.messages.encoding=UTF-8
#是否总是应用MessageFormat规则解析信息,即使是解析不带参数的消息
spring.messages.always-use-message-format=false
# 是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException.建议在开发测试时开启,避免不必要的抛错而影响正常业务流程
spring.messages.use-code-as-default-message=true
在MessageSourceProperties 完成读取配置之后,将会自动注入MessageSource ,而默认注入的MessageSource 的实现类ResourceBundleMessageSource 。ResourceBundleMessageSource 是SpringBoot 实现国际化的核心。其采用的是及时加载文件的形式。即:只有当某种特定的语言要求被返回时,才会去读取资源文件,将消息内容缓存起来并通过响应码进行返回具体消息。ResourceBundleMessageSource的源码并不复杂,这里就不展开讲解。
java
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
//消息资源绑定类,用于缓存资源消息
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());
}
//是否总是应用MessageFormat规则解析信息,即使是解析不带参数的消息
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
//是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException.建议在开发测试时开启,避免不必要的抛错而影响正常业务流程
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
一、配置消息资源文件
消息资源文件用于存储不同国家语言响应的消息,我们通过源码知道该文件必须是properties 类型(在你不去更改源码的时候
),因此我们需要在resources 资源文件下存放消息文件。这里我存放三个文件,message 基础资源文件(内容可以为空
),messages_en_US.properties 英文文件,messages_zh_CN.properties 中文文件。
messages_zh_CN.properties 和messages_en_US.properties 内容如下:
二、配置消息源
这里采用的是以application.properties 形式进行配置,您也可以采用yaml文件形式进行配置:
java
#配置国际化资源文件路径
spring.messages.basename=il8n/messages
#编码格式,默认使用UTF-8
spring.messages.encoding=UTF-8
#是否总是应用MessageFormat规则解析信息,即使是解析不带参数的消息
spring.messages.always-use-message-format=false
# 是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException.建议在开发测试时开启,避免不必要的抛错而影响正常业务流程
spring.messages.use-code-as-default-message=true
三、配置消息拦截器
拦截器用于从请求头中获取语言标识,以便于后续根据语言标识响应不同的响应信息。
java
package com.il8n.config;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.support.RequestContextUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Author: Greyfus
* @Create: 2024-01-12 13:06
* @Version: 1.0.0
* @Description:国际化拦截器
*/
@Setter
@Getter
public class IL8nLangInterceptor extends LocaleChangeInterceptor {
private String langHeader;
@Override
public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws ServletException {
String locale = request.getHeader(getLangHeader());
if (locale != null) {
if (iL8nCheckHttpMethod(request.getMethod())) {
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
}
try {
localeResolver.setLocale(request, response, parseLocaleValue(locale));
} catch (IllegalArgumentException ex) {
if (isIgnoreInvalidLocale()) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid locale value [" + locale + "]: " + ex.getMessage());
}
} else {
throw ex;
}
}
}
}
// Proceed in any case.
return true;
}
public boolean iL8nCheckHttpMethod(String currentMethod) {
String[] configuredMethods = getHttpMethods();
if (ObjectUtils.isEmpty(configuredMethods)) {
return true;
}
for (String configuredMethod : configuredMethods) {
if (configuredMethod.equalsIgnoreCase(currentMethod)) {
return true;
}
}
return false;
}
}
注入消息拦截器,并设置默认语言为中文:
java
package com.il8n.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;
/**
* @Author: Greyfus
* @Create: 2024-01-12 00:22
* @Version: 1.0.0
* @Description:语言国际化配置
*/
@Configuration
public class IL8nLangConfig implements WebMvcConfigurer {
private static final String LANG_HEADER = "lang";
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
sessionLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return sessionLocaleResolver;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
IL8nLangInterceptor lci = new IL8nLangInterceptor();
//设置请求的语言变量
lci.setLangHeader(LANG_HEADER);
return lci;
}
/**
* 注册拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
四、编写消息工具类
消息工具类用于通过指定的消息码获取对应的响应消息
java
package com.il8n.config;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @Author: Greyfus
* @Create: 2024-01-12 00:36
* @Version: 1.0.0
* @Description: 工具
*/
@Component
public class SpringUtils implements ApplicationContextAware {
@Getter
private static ApplicationContext applicationContext;
public SpringUtils() {
}
public void setApplicationContext(@NotNull ApplicationContext applicationContext) {
SpringUtils.applicationContext = applicationContext;
}
public static <T> T getBean(String name) {
return (T) applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
public static <T> T getBean(String name, Class<T> clazz) {
return applicationContext.getBean(name, clazz);
}
public static <T> Map<String, T> getBeansOfType(Class<T> type) {
return applicationContext.getBeansOfType(type);
}
public static String[] getBeanNamesForType(Class<?> type) {
return applicationContext.getBeanNamesForType(type);
}
public static String getProperty(String key) {
return applicationContext.getEnvironment().getProperty(key);
}
public static String[] getActiveProfiles() {
return applicationContext.getEnvironment().getActiveProfiles();
}
}
java
package com.il8n.config;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
/**
* @Author: Greyfus
* @Create: 2024-01-12 00:29
* @Version: 1.0.0
* @Description: IL8N语言转换工具
*/
public class IL8nMessageUtils {
private static final MessageSource messageSource;
static {
messageSource = SpringUtils.getBean(MessageSource.class);
}
/**
* 获取国际化语言值
*
* @param messageCode
* @param args
* @return
*/
public static String message(String messageCode, Object... args) {
return messageSource.getMessage(messageCode, args, LocaleContextHolder.getLocale());
}
}
五、测试国际化
编码一个Controller用于模拟测试效果,代码如下:
java
package com.il8n.controller;
import com.il8n.config.IL8nMessageUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: DI.YIN
* @Date: 2025/1/6 9:37
* @Version:
* @Description:
**/
@RestController
@RequestMapping("/mock")
public class IL8nTestController {
@RequestMapping(value = "/login")
public ResponseEntity<String> login(@RequestParam(value = "userName") String userName, @RequestParam(value = "password") String password) {
if (!"admin".equals(userName) || !"admin".equals(password)) {
return new ResponseEntity<>(IL8nMessageUtils.message("LoginFailure", (Object) null), HttpStatus.OK);
}
return new ResponseEntity<>(IL8nMessageUtils.message("loginSuccess", userName), HttpStatus.OK);
}
}
消息拦截器会尝试从请求头中获取属性为lang 的值,将其作为语言标识,因此我们在使用PostMan 模拟时,需要在请求头中增加lang 属性。
模拟结果如下:
中文语言标识
:
英文语言标识
: