国际化支持
简单实现
通过请求头获取浏览器的语言
- 国际化配置文件的定义
- 国际化文件的暴露
配置MessageSource
- 前端国际化文件的引入
超链接方式切换语言的实现
国际化需要解决的问题
如何将选择的语言传递给其他页面:
- url携带(无状态)
- session存储(推荐)
- 数据库存储
实现步骤:
- 添加国际化资源文件
- 配置MessageSource,设置国际化资源文件
- springboot提供了MessageSourceAutoConfiguration,默认配置了MessageSource,因此不需要去配置MessageSource
WebMvcConfigurer中注入
spring.messages.basename=i18n/messages
spring.messages.encoding=utf-8
- 解析locale参数,默认添加了解析accept-language中的locale ,请求方式为url?local=en_US
```java @DeprecatedConfigurationProperty( replacement = “spring.web.locale” ) public Locale getLocale() { return this.locale; }public class WebMvcAutoConfiguration { @Bean @ConditionalOnMissingBean( name = {"localeResolver"} ) public LocaleResolver localeResolver() { // 配置了spring.web.locale-resolver=fixed,使用配置的本地化语言 if (this.webProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.WebProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.webProperties.getLocale()); } else if (this.mvcProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.mvcProperties.getLocale()); } else { // 默认使用解析accept-language中的AcceptHeaderLocaleResolver作为本地化语言解析器 AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); // 是否配置了spring.web.locale作为指定的语言 Locale locale = this.webProperties.getLocale() != null ? this.webProperties.getLocale() : this.mvcProperties.getLocale(); localeResolver.setDefaultLocale(locale); return localeResolver; } } }
public void setLocale(Locale locale) { this.locale = locale; }
@DeprecatedConfigurationProperty( replacement = “spring.web.locale-resolver” ) public WebMvcProperties.LocaleResolver getLocaleResolver() { return this.localeResolver; }
```java
public class AcceptHeaderLocaleResolver implements LocaleResolver {
// 当Accept-Language为null时使用spring.web.locale
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = this.getDefaultLocale();
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
} else {
// 通过request.getLocale()获取locale的值
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = this.getSupportedLocales();
if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
} else {
return defaultLocale != null ? defaultLocale : requestLocale;
}
} else {
return requestLocale;
}
}
}
}
更改默认的本地化语言的解析器LocaleResolver为SessionLocaleResolver/CookieLocaleResolver
@Bean public LocaleResolver localeResolver() { CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver(); cookieLocaleResolver.setCookieMaxAge(60 * 60 * 24 * 30); cookieLocaleResolver.setCookieName("locale"); return cookieLocaleResolver; }
切换语言的获取和存储,注意,两种实现方式的请求参数方式不一样
- 通过请求接口手动设置到session,并返回页面
- 使用拦截器LocaleChangeInterceptor ```java // 支持自定义配置messageSource @ConditionalOnMissingBean( name = {“messageSource”}, search = SearchStrategy.CURRENT ) @AutoConfigureOrder(-2147483648) @Conditional({MessageSourceAutoConfiguration.ResourceBundleCondition.class}) @EnableConfigurationProperties public class MessageSourceAutoConfiguration {
@Bean @ConfigurationProperties( prefix = “spring.messages” )
@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; }
// ResourceBundleCondition可以重写matchs实现自定义匹配规则, protected static class ResourceBundleCondition extends SpringBootCondition { private static ConcurrentReferenceHashMap
cache = new ConcurrentReferenceHashMap(); protected ResourceBundleCondition() { } // 实现了具体的匹配规则,匹配路径成功返回true public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 获取配置文件的spring.messages.basename的属性值,默认是messages String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages"); ConditionOutcome outcome = (ConditionOutcome)cache.get(basename); if (outcome == null) { outcome = this.getMatchOutcomeForBasename(context, basename); cache.put(basename, outcome); } return outcome;
}
// 根据message获取该路径下的所有properties资源文件 // 因此生效的条件是指定的message路径下有properties资源文件
private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
Builder message = ConditionMessage.forCondition("ResourceBundle", new Object[0]); String[] var4 = StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename)); int var5 = var4.length; for(int var6 = 0; var6 < var5; ++var6) { String name = var4[var6]; Resource[] var8 = this.getResources(context.getClassLoader(), name); int var9 = var8.length; for(int var10 = 0; var10 < var9; ++var10) { Resource resource = var8[var10]; // 只要存在资源文件就能匹配成功 if (resource.exists()) { return ConditionOutcome.match(message.found("bundle").items(new Object[]{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 var5) { return MessageSourceAutoConfiguration.NO_RESOURCES; }
} } } ```
源码解析
MessageSource的常用实现类 https://blog.csdn.net/likun557/article/details/105942210/
- ResourceBundleMessageSource 基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源
- ReloadableResourceBundleMessageSource 增加定时刷新功能,允许在不重启系统的情况下,更新资源的信息
- StaticMessageSource 允许通过编程的方式提供国际化信息,可以用来实现db中存储国际化信息
微服务中也可以使用session存储的方案,但需要解决session共享的问题
springmvc
LocaleResolver:默认通过获取请求头的语言设置
SessionLocaleResolver:session中国际化和时区的操作
LocaleChangeInterceptor:拦截器获取locale的参数值实现切换
public class LocaleChangeInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {
String newLocale = request.getParameter(this.getParamName());
if (newLocale != null && this.checkHttpMethod(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, this.parseLocaleValue(newLocale));
} catch (IllegalArgumentException var7) {
if (!this.isIgnoreInvalidLocale()) {
throw var7;
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + var7.getMessage());
}
}
}
}
springboot
MessageSourceAutoConfiguration
MessageSourceResourceBundle
实战案例
前端文字资源(文字内容)的国际化以及后端返回内容(日志和错误信息)的国际化
https://www.csdn.net/tags/NtTaYgwsNTk2NjktYmxvZwO0O0OO0O0O.html
多时区的实现
需要处理的业务:
- 页面选择时区,时间显示选择时区的时间
- 根据时区时间查询数据
- 实现时间字段时区的统一:主要是后端接收前端的参数进行业务处理、数据库时间数据的存储
- 还有导出导入的业务(未实现)
- 导出数据包括了时间和时区
- 或者时间存储的是国际标准时间
多时区的实现严格来说不是显式通过对时间进行时区转换,而是对时区的统一定义
- 后端服务和数据库时区需要保持一致
- 前端选择时区,将时间根据时区转换为国际标准时间,国际标准时间其实就是一个带有时区标识的时间格式,如2022-06-27T05:16:58.405+0000表示UTC的时间,2022-06-27T05:16:58.405+0800表示GMT+8的时间,有了时区的对照,前端可以通过选择的时区转换为想要的时间,后端也可以根据配置的时区转换的时间去做查询和存储。
- 前端可以使用layui和moment插件去实现时间的转换
<script type="text/javascript" src="../../assets/libs/moment.js"></script> <script type="text/javascript" src="../../assets/libs/moment-with-locales.js"></script> moment(d.createTime).format('YYYY-MM-DD HH:mm:ss'); layui.util.toDateString('2018-09-11T00:02:25.000+0800')
多时区统一的设置
系统:date -R
数据库时区:SHOW VARIABLES LIKE ‘%timezone%’
数据库连接时区:_serverTimezone=Asia/Shanghai
spring.datasource.url=jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=utf-8&useLegacyDatetimeCode=false&serverTimezone=UTC
JVM的时区:-Duser.timezone
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
json序列化时区:spring.jackson.time-zone