- 1. 创建文件
- 1.1 创建 bundle
- 1.2 添加文件名及语言
- 1.3 文本编写
- 2. springboot自带配置类解析
- 3. 获取值
- **LocaleContextHolder.getLocale()**
- debug追踪
- **解析器(LocaleResolver)的配置**
- 拦截器的配置
- RequestContextUtils.getLocale(request)
- 总结
- 4. 应用
- 4.1 配置文件
- 4.2 WxLocaleConfigs
- 附录:
- **HeaderLocaleContextResolver**
- **HeaderLocaleChangeInterceptor**
这里是在 resources目录下新建的i18n目录中创建。
1.2 添加文件名及语言添加文件名:messages(建议,也可以任意),并添加中文(zh_CN),英文(en_US)两种语言。
添加完成,打开任意一个文件,切换编写模式,进行对应的文本编写。这里我们需要的文件已经创建完毕。
2. springboot自带配置类解析springboot 有很多自动装配类:XXXAutoConfiguration,国际化配置->MessageSourceAutoConfiguration
@Configuration(proxyBeanMethods = false)
@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 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;
}
}
}
}
3. 获取值
配置类加载好了,那么接下来如何获取值呢?
spring 提供了两种总方式:
LocaleContextHolder.getLocale()查看源码:重点看 如何获取 LocaleContext
private static final ThreadLocaldebug追踪localeContextHolder = new NamedThreadLocal<>("LocaleContext"); private static final ThreadLocal inheritableLocaleContextHolder = new NamedInheritableThreadLocal<>("LocaleContext"); public static Locale getLocale() { return getLocale(getLocaleContext()); } public static Locale getLocale(@Nullable LocaleContext localeContext) { if (localeContext != null) { Locale locale = localeContext.getLocale(); if (locale != null) { return locale; } } return (defaultLocale != null ? defaultLocale : Locale.getDefault()); } @Nullable public static LocaleContext getLocaleContext() { LocaleContext localeContext = localeContextHolder.get(); if (localeContext == null) { localeContext = inheritableLocaleContextHolder.get(); } return localeContext; } public static void setLocaleContext(@Nullable LocaleContext localeContext, boolean inheritable) { if (localeContext == null) { resetLocaleContext(); } else { if (inheritable) { inheritableLocaleContextHolder.set(localeContext); localeContextHolder.remove(); } else { localeContextHolder.set(localeContext); inheritableLocaleContextHolder.remove(); } } }
- 进入过滤器RequestContextFilter.initContextHolders()方法,获取request头中的accept-language来初始化 locale(默认是zh_CN),此时为SimpleLocaleContext
实际就是将国际化上下文localeContext放入 ThreadLocal,再使用的时候取出来。
- 进入DispatcherServlet.buildLocaleContext()方法,此时注意到LocaleContextResolver 进入我们的视野
查看源码得知:localeResolver 来自项目初始化,会获取LocaleResolver 实例注入。此时我们就需要创建一个属于我们自己的解析器。HeaderLocaleContextResolver
public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver";
private void initLocaleResolver(ApplicationContext context) {
try {
this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.localeResolver);
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.localeResolver.getClass().getSimpleName());
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME +
"': using default [" + this.localeResolver.getClass().getSimpleName() + "]");
}
}
}
HeaderLocaleContextResolver.resolveLocaleContext() 设置我们自定义的LocaleContext放入ThreadLocal中。
package com.wx.wxcommonlocale.support.i18n;
@Override
public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
return new TimeZoneAwareLocaleContext() {
@Override
@Nullable
public Locale getLocale() {
Object attribute = request.getAttribute(localeAttributeName);
return attribute != null ? (Locale) attribute : getDefaultLocale();
}
@Override
@Nullable
public TimeZone getTimeZone() {
Object attribute = request.getAttribute(timeZoneAttributeName);
return attribute != null ? (TimeZone) attribute : getDefaultTimeZone();
}
};
}
接下来继续追踪,进入frameworkServlet.processRequest()方法的initContextHolders()将上一步自定义的国际化上下文放入ThreadLocal中。【DispatcherServlet 继承了 frameworkServlet,在processRequest方法中,调用上一步buildLocaleContext()方法】
private void initContextHolders(HttpServletRequest request,
@Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
if (localeContext != null) {
LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
}
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
}
}
此时在看LocaleContextHolder.getLocale()方法如何获取值:实际上是从 自定义的方法中 获取。
package com.wx.wxcommonlocale.support.i18n;
@Override
public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
return new TimeZoneAwareLocaleContext() {
@Override
@Nullable
public Locale getLocale() {
Object attribute = request.getAttribute(localeAttributeName);
return attribute != null ? (Locale) attribute : getDefaultLocale();
}
@Override
@Nullable
public TimeZone getTimeZone() {
Object attribute = request.getAttribute(timeZoneAttributeName);
return attribute != null ? (TimeZone) attribute : getDefaultTimeZone();
}
};
}
拦截器的配置
当过滤器执行完成,就要执行拦截器的代码,这里可以看到,是获取自定义的头getParamName()【locale】,来判断是那种语言。
package com.wx.wxcommonlocale.support.i18n;
@Slf4j
public class HeaderLocaleChangeInterceptor extends LocaleChangeInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
Locale locale = null;
TimeZone timeZone = null;
//从请求头中获取信息格式:zh_CN Asia/Shanghai zh_CN GMT+8
String newLocale = request.getHeader(getParamName());
...
/
@Override
public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
Locale locale = null;
TimeZone timeZone = null;
if (localeContext != null) {
locale = localeContext.getLocale();
if (localeContext instanceof TimeZoneAwareLocaleContext) {
timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
}
//设置响应头,微服务之间传递
response.setHeader(getLocaleHeadName(),
(locale != null ? locale.toString() : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
}
/
public static final String LOCALE_REQUEST_ATTRIBUTE_NAME = HeaderLocaleContextResolver.class.getName() + ".LOCALE";
public static final String TIME_ZONE_REQUEST_ATTRIBUTE_NAME = HeaderLocaleContextResolver.class.getName() + ".TIME_ZONE";
private String localeAttributeName = LOCALE_REQUEST_ATTRIBUTE_NAME;
private String timeZoneAttributeName = TIME_ZONE_REQUEST_ATTRIBUTE_NAME;
private String localeHeadName;
public String getLocaleHeadName() {
return localeHeadName;
}
public void setLocaleHeadName(String localeHeadName) {
this.localeHeadName = localeHeadName;
}
@Override
public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
return new TimeZoneAwareLocaleContext() {
@Override
@Nullable
public Locale getLocale() {
Object attribute = request.getAttribute(localeAttributeName);
return attribute != null ? (Locale) attribute : getDefaultLocale();
}
@Override
@Nullable
public TimeZone getTimeZone() {
Object attribute = request.getAttribute(timeZoneAttributeName);
return attribute != null ? (TimeZone) attribute : getDefaultTimeZone();
}
};
}
@Override
public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
Locale locale = null;
TimeZone timeZone = null;
if (localeContext != null) {
locale = localeContext.getLocale();
if (localeContext instanceof TimeZoneAwareLocaleContext) {
timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
}
//设置响应头,微服务之间传递
response.setHeader(getLocaleHeadName(),
(locale != null ? locale.toString() : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
}
request.setAttribute(localeAttributeName,
(locale != null ? locale : getDefaultLocale()));
request.setAttribute(timeZoneAttributeName,
(timeZone != null ? timeZone : getDefaultTimeZone()));
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
TimeZone timeZone = (TimeZone) request.getAttribute(HeaderLocaleContextResolver.TIME_ZONE_REQUEST_ATTRIBUTE_NAME);
setLocaleContext(request, response, (locale != null ? timeZone != null ? new SimpleTimeZoneAwareLocaleContext(locale, timeZone) : new SimpleLocaleContext(locale) : null));
}
}
HeaderLocaleChangeInterceptor
@Slf4j
public class HeaderLocaleChangeInterceptor extends LocaleChangeInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
Locale locale = null;
TimeZone timeZone = null;
//从请求头中获取信息格式:zh_CN Asia/Shanghai zh_CN GMT+8
String newLocale = request.getHeader(getParamName());
if (newLocale != null) {
//过滤不需要变化的方法
if (checkHttpMethod(request.getMethod())) {
String localePart = newLocale;
String timeZonePart = null;
int spaceIndex = localePart.indexOf(' ');
if (spaceIndex != -1) {
localePart = newLocale.substring(0, spaceIndex);
timeZonePart = newLocale.substring(spaceIndex + 1);
}
try {
locale = (!"-".equals(localePart) ? parseLocalevalue(localePart) : null);
if (!StringUtils.isEmpty(timeZonePart)) {
timeZone = StringUtils.parseTimeZoneString(timeZonePart);
}
} catch (IllegalArgumentException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
// Error dispatch: ignore locale/timezone parse exceptions
log.info("ignore locale/timezone parse exceptions");
} else {
throw new IllegalStateException("Invalid locale Header '" + getParamName() +
"' with value [" + newLocale + "]: " + ex.getMessage());
}
}
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException(
"No LocaleResolver found: not in a DispatcherServlet request?");
}
try {
//设置地区
localeResolver.setLocale(request, response, locale);
if(locale != null){
request.setAttribute(HeaderLocaleContextResolver.LOCALE_REQUEST_ATTRIBUTE_NAME,locale);
}
//设置区时
if(timeZone != null){
request.setAttribute(HeaderLocaleContextResolver.TIME_ZONE_REQUEST_ATTRIBUTE_NAME,timeZone);
}
}
catch (IllegalArgumentException ex) {
if (isIgnoreInvalidLocale()) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
}
}
else {
throw ex;
}
}
}
}
// Proceed in any case.
return true;
}
private boolean checkHttpMethod(String currentMethod) {
String[] configuredMethods = getHttpMethods();
if (ObjectUtils.isEmpty(configuredMethods)) {
return true;
}
for (String configuredMethod : configuredMethods) {
if (configuredMethod.equalsIgnoreCase(currentMethod)) {
return true;
}
}
return false;
}
}
springcloud 脚手架(wx-cloud)
- spring boot 2.3.3.RELEASE
- nacos
- geteway
- oauth2
- cache(spring+caffeine)
- mybaits



