前言
LocaleResolver接口
基于 Web 的区域设置解析策略的接口,允许通过请求进行区域设置解析,以及通过请求和响应进行区域设置修改。
此接口允许基于请求、会话、cookie 等的实现。默认实现是 org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
,只需使用相应 HTTP 标头提供的请求区域设置即可。
用于 org.springframework.web.servlet.support.RequestContext.getLocale()
检索控制器或视图中的当前区域设置,与实际解析策略无关。
注意:从Spring 4.0开始,有一个名为 LocaleContextResolver的扩展策略接口,允许解析 org.springframework.context.i18n.LocaleContext 对象,可能包括相关的时区信息。Spring 提供的解析器实现在适当的时候实现了扩展 LocaleContextResolver 接口。
AcceptHeaderLocaleResolver
LocaleResolver 实现,它仅使用 HTTP 请求的 Accept-Language 标头中指定的主区域设置(即客户端浏览器发送的区域设置,通常是客户端操作系统的区域设置)。
MessageSource接口
用于解析消息的策略接口,支持此类消息的参数化和国际化。
Spring 为生产环境提供了两种开箱即用的实现:
org.springframework.context.support.ResourceBundleMessageSource:建立在标准 java.util.ResourceBundle
之上,分享其局限性。org.springframework.context.support.ReloadableResourceBundleMessageSource
:高度可配置,特别是在重新加载消息定义方面。
MessageSourceAutoConfiguration
源码:
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.context;
import java.time.Duration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.ResourceBundleCondition;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link MessageSource}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Eddú Meléndez
* @since 1.5.0
*/
@AutoConfiguration
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@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());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
protected static class ResourceBundleCondition extends SpringBootCondition {
private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();
@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());
}
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;
}
}
}
}
通过阅读MessageSource自动配置代码可知:
使用ResourceBundleMessageSource作为消息源
配置文件应位于classpath路径下,文件后缀
.properties
,为通过spring.messages.basename
指定,可配置多个,可通过.
或者/
配置多级路径
应用实践一、基本使用
新建资源包
在resources下新建i18n路径,在i18n下新建资源包:
资源包基名称:messages
添加区域设置:如en、zh_CN等
messages_en.properties:
demo=demo-en
messages_zh_CN.properties:
demo={0},中文,{1}
配置
spring:
messages:
beannames: i18n/messages
测试接口
@RestController
@RequestMapping("/i18n")
public class TestApi
extends AcceptHeaderLocaleResolver {
@Autowired
private MessageSource messageSource;
@GetMapping("/get")
public String get(@RequestHeader(name = "Accept-Language") String lang, HttpServletRequest request) {
System.out.println(lang);
return messageSource.getMessage("demo", new Object[]{"你好", "I18n"}, request.getLocale());
}
}
测试结果:
注意
请求的参数不是下划线
_
而是-
应用实践二、自定义配置
自定义配置属性
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.support.AbstractResourceBasedMessageSource;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import java.util.Locale;
/**
* 国际化配置类
*/
@Data
@ConfigurationProperties(prefix = "jcext.i18n")
public class I18nProperties {
private boolean enabled = true;
/**
* 系统默认的应用场所
*/
private Locale defaultLocale = Locale.CHINA;
/**
* 配置加载国际化资源文件的类默认使用 ReloadableResourceBundleMessageSource
* ResourceBundleMessageSource是Spring框架提供的用于加载国际化资源文件的类,它是基于Java标准的ResourceBundle实现的。
* 它在应用启动时会加载所有的资源文件,并且不支持动态刷新资源文件。
* <p>
* ReloadableResourceBundleMessageSource是对ResourceBundleMessageSource的扩展,
* 它支持动态刷新资源文件。当资源文件发生变化时,可以通过调用特定的方法来重新加载资源文件,而不需要重启应用程序。
*/
private Class<? extends AbstractResourceBasedMessageSource> messageSourceClass = ReloadableResourceBundleMessageSource.class;
/**
* 设置国际化文件存储路径和名称 i18n目录,messages文件名,需要编译打包后的资源文件的位置
*/
// private String[] beannames = new String[]{"i18n/message"};
private String[] beannames = new String[]{"classpath:i18n/message"};
/**
* 根据code如果没有获取到对应的文本信息,则返回code作为信息
*/
private boolean useCode = false;
/**
* 设置字符编码
*/
private String defaultEncoding = "UTF-8";
}
自定义解析
package cn.com.jcoo.ext.i18n;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
/**
* 配置http请求Locale解析器
*/
@RequiredArgsConstructor
public class HttpRequestLocaleResolver implements LocaleResolver {
private final HttpServletRequest httpServletRequest;
public Locale getRequestLocale() {
return resolveLocale(httpServletRequest);
}
@Override
public Locale resolveLocale(HttpServletRequest request) {
// String lang = request.getHeader("lang");
String lang = request.getParameter("lang");
if (!StringUtils.hasLength(lang)) {
return Locale.getDefault();
}
// return new Locale(lang);
if (lang.contains("-")) {
String[] split = lang.split("_");
return new Locale(split[0], split[1]);
}
return new Locale(lang);
}
/**
* 用于实现Locale的切换。比如SessionLocaleResolver获取Locale的方式是从session中读取,但如果
* 用户想要切换其展示的样式(由英文切换为中文),那么这里的setLocale()方法就提供了这样一种可能
*
* @param request 请求
* @param response 响应
* @param locale 本地化配置
*/
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response,
Locale locale) {
}
}
自定义工具组件
package cn.com.jcoo.ext.i18n;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.util.StringUtils;
import java.util.Locale;
@Slf4j
@RequiredArgsConstructor
public class I18nHelper {
private static MessageSource messageSource;
private static HttpRequestLocaleResolver httpRequestLocaleResolver;
@Autowired
public void setMessageSource(MessageSource messageSource) {
I18nHelper.messageSource = messageSource;
}
@Autowired
public void setHttpRequestLocaleResolver(HttpRequestLocaleResolver resolver) {
I18nHelper.httpRequestLocaleResolver = resolver;
}
public static String getMessage(String code) {
Locale locale = httpRequestLocaleResolver.getRequestLocale();
return getMessage(code, null, "", locale);
}
public static String getMessage(String code, String lang) {
Locale locale;
if (!StringUtils.hasLength(lang)) {
locale = Locale.getDefault();
} else {
try {
locale = new Locale(lang);
} catch (Exception e) {
locale = Locale.getDefault();
}
}
return getMessage(code, null, code, locale);
}
public static String getMessage(String code, Object[] args, String defaultMessage,
Locale locale) {
String content;
try {
content = messageSource.getMessage(code, args, locale);
} catch (Exception e) {
log.error("国际化参数获取失败===>{}", e.getMessage(), e);
content = defaultMessage;
}
return content;
}
}
自定义自动配置
package cn.com.jcoo.ext.i18n;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.AbstractResourceBasedMessageSource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Locale;
@Slf4j
@Configuration
@EnableConfigurationProperties(I18nProperties.class)
@ConditionalOnProperty(prefix = "jcext.i18n", name = "enabled", havingValue = "true", matchIfMissing = true)
public class I18nConfig {
public static final String JCEXT_I18N_MS_BEAN_NAME = "JcextI18nMessageSource";
private final I18nProperties i18nProperties;
public I18nConfig(I18nProperties i18nProperties) {
this.i18nProperties = i18nProperties;
}
@SneakyThrows
@Bean(JCEXT_I18N_MS_BEAN_NAME)
public MessageSource messageSource() {
Locale.setDefault(i18nProperties.getDefaultLocale());
Class<? extends AbstractResourceBasedMessageSource> messageSourceClass = i18nProperties.getMessageSourceClass();
AbstractResourceBasedMessageSource source = messageSourceClass.newInstance();
source.setBasenames(i18nProperties.getBeannames());
source.setUseCodeAsDefaultMessage(false);
source.setDefaultEncoding(i18nProperties.getDefaultEncoding());
log.info("消息资源绑定 {}", Arrays.toString(i18nProperties.getBeannames()));
return source;
}
@Bean
@ConditionalOnBean(MessageSource.class)
public HttpRequestLocaleResolver httpRequestLocaleResolver(HttpServletRequest httpServletRequest) {
return new HttpRequestLocaleResolver(httpServletRequest);
}
@Bean
@ConditionalOnBean(value = {MessageSource.class, HttpRequestLocaleResolver.class})
public I18nHelper i18nHelper() {
return new I18nHelper();
}
}
简单使用
@Slf4j
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestApi {
@GetMapping("/i18n/hello")
public String i18nHello() {
return I18nHelper.getMessage("hello");
}
}
评论区