国际化支持

简单实现

通过请求头获取浏览器的语言
image.png

  1. 国际化配置文件的定义
  2. 国际化文件的暴露

配置MessageSource
image.png
image.png

  1. 前端国际化文件的引入

超链接方式切换语言的实现

国际化需要解决的问题
如何将选择的语言传递给其他页面:

  • url携带(无状态)
  • session存储(推荐)
  • 数据库存储

实现步骤:

  1. 添加国际化资源文件

image.png

  1. 配置MessageSource,设置国际化资源文件
  • springboot提供了MessageSourceAutoConfiguration,默认配置了MessageSource,因此不需要去配置MessageSource

WebMvcConfigurer中注入

  1. spring.messages.basename=i18n/messages
  2. spring.messages.encoding=utf-8
  1. 解析locale参数,默认添加了解析accept-language中的locale ,请求方式为url?local=en_US
    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;
       }
     }
    }
    
    ```java @DeprecatedConfigurationProperty( replacement = “spring.web.locale” ) public Locale getLocale() { return this.locale; }

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共享的问题
image.png
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://blog.csdn.net/qq_33220089/article/details/104837066?spm=1001.2101.3001.6650.13&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-13-104837066-blog-117883076.pc_relevant_blogantidownloadv1&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-13-104837066-blog-117883076.pc_relevant_blogantidownloadv1&utm_relevant_index=18

后端内容国际化
https://blog.csdn.net/weixin_44519874/article/details/121460572?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1-121460572-blog-109152633.pc_relevant_multi_platform_whitelistv1&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1-121460572-blog-109152633.pc_relevant_multi_platform_whitelistv1&utm_relevant_index=1

前端文字资源(文字内容)的国际化以及后端返回内容(日志和错误信息)的国际化
https://www.csdn.net/tags/NtTaYgwsNTk2NjktYmxvZwO0O0OO0O0O.html

多时区的实现

需要处理的业务:

  1. 页面选择时区,时间显示选择时区的时间
  2. 根据时区时间查询数据
  3. 实现时间字段时区的统一:主要是后端接收前端的参数进行业务处理、数据库时间数据的存储
  4. 还有导出导入的业务(未实现)
    1. 导出数据包括了时间和时区
    2. 或者时间存储的是国际标准时间

多时区的实现严格来说不是显式通过对时间进行时区转换,而是对时区的统一定义

  1. 后端服务和数据库时区需要保持一致
  2. 前端选择时区,将时间根据时区转换为国际标准时间,国际标准时间其实就是一个带有时区标识的时间格式,如2022-06-27T05:16:58.405+0000表示UTC的时间,2022-06-27T05:16:58.405+0800表示GMT+8的时间,有了时区的对照,前端可以通过选择的时区转换为想要的时间,后端也可以根据配置的时区转换的时间去做查询和存储。
  3. 前端可以使用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