夯实Spring系列|第十七章:Spring 国际化(i18n)

夯实Spring系列|第十七章:Spring 国际化(i18n)

前言

本章会讨论 Spring 中国际化的接口和相关实现以及 Java 中对国际化的相关支持。虽然在 Spring 体系中,国际化属于比较边缘的技术,但是基于两点原因,我们也可以进行一些学习和了解

1.在 AbstractApplicationContext#refresh 应用上下文启动的过程中,initMessageSource() 来进行国际化的初始化,作为启动中重要的一环,不可避免需要学习和了解

2.后续章节中的很多内容和国际化结合的比较紧密,国际化主要提供一些文案的适配和支持

1.项目环境

2.Spring 国际化使用场景

四个主要使用场景

  • 普通国际化文案
  • Bean Validation 效验国际化文案
    • 这一部分在下一章进行讨论
  • Web 站点页面渲染
  • Web MVC 错误消息提示

3.Spring 国际化接口

核心接口

  • org.springframework.context.MessageSource

Spring 提供两个 out-of-the-box(开箱即用)的实现

  • org.springframework.context.support.ResourceBundleMessageSource
  • org.springframework.context.support.ReloadableResourceBundleMessageSource

主要概念

  • 文案模板编码(code)
  • 文案模板参数(args)
  • 区域(Locale)
    • java.util.Locale

3.层次性 MessageSource

Spring 层次性接口回顾

  • org.springframework.beans.factory.HierarchicalBeanFactory
  • org.springframework.context.ApplicationContext
  • org.springframework.beans.factory.config.BeanDefinition

Spring 层次性国际化接口

  • org.springframework.context.HierarchicalMessageSource
public interface HierarchicalMessageSource extends MessageSource {

	/**
	 * Set the parent that will be used to try to resolve messages
	 * that this object can't resolve.
	 * @param parent the parent MessageSource that will be used to
	 * resolve messages that this object can't resolve.
	 * May be {@code null}, in which case no further resolution is possible.
	 */
	void setParentMessageSource(@Nullable MessageSource parent);

	/**
	 * Return the parent of this MessageSource, or {@code null} if none.
	 */
	@Nullable
	MessageSource getParentMessageSource();

}

层次性接口都有一个共同的特点,一般会有一个 getParent 的方法,可以获取到双亲的相关信息,这里的双亲有可能是对象也有可能是对象的名称。

4.Java 国际化标准实现

4.1 核心接口

  • 抽象实现 - java.util.ResourceBundle

    • 列举实现 - java.util.ListResourceBundle(不常用)

      • sun.security.util.AuthResources_zh_CN
      • 通过硬编码的方式将文案相关的信息维护成一个二维数组
      public class AuthResources_zh_CN extends ListResourceBundle {
          private static final Object[][] contents = new Object[][]{
          {"invalid.null.input.value", "无效的空输入: {0}"}, 
          {"NTDomainPrincipal.name", "NTDomainPrincipal: {0}"}, 
          {"NTNumericCredential.name", "NTNumericCredential: {0}"}, 
          {"Invalid.NTSid.value", "无效的 NTSid 值"}, {"NTSid.name", "NTSid: {0}"}, 
          ...
      
          public AuthResources_zh_CN() {
          }
      
          public Object[][] getContents() {
              return contents;
          }
      }
      
    • Properties 资源实现 - java.util.PropertyResourceBundle

      • 使用 Properties 文件中的文案信息进行相应转换

4.2 ResourceBundle 核心特性

  • Key - Value 设计

  • 层次性

    • java.util.ResourceBundle#setParent
    • java.util.ResourceBundle#getObject
      • 查找对象的时候,会先从 parent 中进行查找
    • java.util.ResourceBundle#containsKey
  • 缓存设计

    • java.util.ResourceBundle.CacheKey
        private static final ConcurrentMap<CacheKey, BundleReference> cacheList
            = new ConcurrentHashMap<>(INITIAL_CACHE_SIZE);
    
  • 字段编码控制 - java.util.ResourceBundle.Control(@since 1.6)

  • Control SPI 扩展- java.util.spi.ResourceBundleContorlProvider(@since 1.8)

5.Java 文本格式化

核心接口

  • java.text.MessageFormat

基本用法

  • 设置消息格式模式 - new MessageFormat(…)
  • 格式化 - format(new Object[]{…})

消息格式模式

  • 格式元素:{ArgumentIndex,(FormatType),(FormatStyle)}
  • FormatType:消息格式类型,可选项,每种类型在 number、date、time 和 choice 类型选其一
  • FormatStyle:消息格式风格,可选项,包括:short、medium、long、full、integer、currency、percent

高级特性

  • 重置消息格式模式
  • 重置 java.util.Locale
  • 重置 java.text.Format

示例

我们先用 java doc 中提供的示例

    private static void javaDocDemo() {
        int planet = 7;
        String event = "a disturbance in the Force";

        String result = MessageFormat.format(
                "At {1,time,long} on {1,date,full}, there was {2} on planet {0,number,integer}.",
                planet, new Date(), event);

        System.out.println(result);
    }

执行结果:

At 上午09时52分06秒 on 2020年6月12日 星期五, there was a disturbance in the Force on planet 7.

演示高级特性

  • 重置消息格式模式
  • 重置 java.util.Locale
  • 重置 java.text.Format
    public static void main(String[] args) {
        javaDocDemo();

        // 重置 MessageFormatPatten
        MessageFormat messageFormat = new MessageFormat("This is a text : {0}");
        messageFormat.applyPattern("This is a new text : {0}");
        String result = messageFormat.format(new Object[]{"hello,world"});
        System.out.println(result);

        // 重置 Locale
        messageFormat.setLocale(Locale.ENGLISH);
        messageFormat.applyPattern("At {1,time,long} on {1,date,full}, there was {2} on planet {0,number,integer}.");
        int planet = 7;
        String event = "a disturbance in the Force";
        result = messageFormat.format(new Object[]{planet, new Date(), event});
        System.out.println(result);

        // 重置 Format
        // 根据参数索引来设置 Pattern
        messageFormat.setFormat(1,new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"));
        result = messageFormat.format(new Object[]{planet, new Date(), event});
        System.out.println(result);
    }

执行结果:

At 上午09时52分06秒 on 2020年6月12日 星期五, there was a disturbance in the Force on planet 7.
This is a new text : hello,world
At 9:52:06 AM CST on Friday, June 12, 2020, there was a disturbance in the Force on planet 7.
At 9:52:06 AM CST on 2020-06-12 09:52:06, there was a disturbance in the Force on planet 7.

6.MessageSource 开箱即用实现

基于 ResourceBundle + MessageFomat 组合 MessageSource 实现

  • org.springframework.context.support.ResourceBundleMessageSource

可重载 Properties + MessageFormat 组合 MessageSource 实现

  • org.springframework.context.support.ReloadableResourceBundleMessageSource

7.MessageSource 内建实现

MessageSource 内建 Bean 可能来源

  • 预注册 Bean 名称为:messageSource,类型为:MessageSource Bean
  • 默认内建实现- DelegatingMessageSource
    • 层次性查找 MessageSource 对象

源码分析

  • org.springframework.context.support.AbstractApplicationContext#initMessageSource
    • beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME) 判断当前上下文中是否包含这个 bean
    • 如果不包含,第一次进来显然是不包含的
      • beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); 注册一个 MessageSource 的实例对象放到 IOC 容器中,这个对象是通过 new DelegatingMessageSource() 创建,通过 jdk 的注释可以看到这是一个空实现
      • 然后通过 this.messageSource = dms; 将当前的 Application 和新建的 DelegatingMessageSource 实例对象做关联
    • 如果包含,表示我们已经通过别的方式将 MessageSource 对象注册到当前上下文中,那么通过依赖查找的方式获取这个 bean 对象,并与当前的 Application 做关联
protected void initMessageSource() {
   ConfigurableListableBeanFactory beanFactory = getBeanFactory();
   if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
      this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
      // Make MessageSource aware of parent MessageSource.
      if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
         HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
         if (hms.getParentMessageSource() == null) {
            // Only set parent context as parent MessageSource if no parent MessageSource
            // registered already.
            hms.setParentMessageSource(getInternalParentMessageSource());
         }
      }
      if (logger.isTraceEnabled()) {
         logger.trace("Using MessageSource [" + this.messageSource + "]");
      }
   }
   else {
      // Use empty MessageSource to be able to accept getMessage calls.
      DelegatingMessageSource dms = new DelegatingMessageSource();
      dms.setParentMessageSource(getInternalParentMessageSource());
      this.messageSource = dms;
      beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
      if (logger.isTraceEnabled()) {
         logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]");
      }
   }
}

8.Spring Boot 中应用

8.1 Spring Boot 为什么要新建 MessageSource Bean?

  • AbstractApplicationContext 的实现决定 MessageSource 内建实现
    • 从第 7 小结中的分析可以看到,Spring上下文启动过程中,初始化 MessageSource 相关的代码,会先判断我们是否有自己的实现,以我们的实现为主,Spring Boot 可以使用这个特性新建自己的 MessageSource Bean
  • Spring Boot 通过外部化配置简化 MessageSource Bean 构建
  • Spring Boot 基于 Bean Validation 效验非常普遍(主要原因)
    • MessageSource 可以提供相关的文案

8.2 MessageSource 自动装配

org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration

  • 先通过外部化配置的方式将 Properties 文件中前缀为 spring.messages 相关内容封装成 MessageSourceProperties 对象
  • 再通过 @Bean 的方式注入 MessageSource Bean 对象
    • 这里最好的方式是 @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME) 使用常量的名称限定,保证这个 Bean 一定满足 AbstractApplicationContext#initMessageSource 中的判断条件
@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;
	}
    ...

8.3 示例

8.3.1 条件装配分析

上面 MessageSource 自动装配的实现中有两个条件装配

  • @ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
  • @Conditional(ResourceBundleCondition.class)

第一个条件装配的意思是如果当前上下文中不存在 Bean 的 name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME 时,才会进行自动装配;换言之,如果我们注册一个名称为 AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME Bean对象到当前上下文中,Spring Boot 中 MessageSource 的自动装配就会失效

第二个条件装配的意思是,需要在 resources 目录下面建立一个 messages.properties 的文件(细节就不展开了可以参考 走向自动装配|第三章-Spring Boot 条件装配

  • MessageSourceAutoConfiguration.ResourceBundleCondition#getMatchOutcome
8.3.2 测试代码
@EnableAutoConfiguration
public class CustomizedMessageSourceBeanDemo {

    /**
     * 在 Spring Boot 场景中,Primary Configuration Sources(Classes) 高于 *AutoConfiguration
     * @return
     */
    @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)
    public MessageSource messageSource(){
        return new ReloadableResourceBundleMessageSource();
    }
    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(CustomizedMessageSourceBeanDemo.class, args);
        ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
        if (beanFactory.containsLocalBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)) {
            MessageSource messageSource = applicationContext.getBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
            System.out.println(messageSource);
        }
        applicationContext.close();
    }
}

执行结果:

可以看到当前上下文中的 MessageSource 对象是我们自己定义的 ReloadableResourceBundleMessageSource 对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IBQWoUAk-1591933251662)(G:\workspace\csdn\learn-document\spring-framework\csdn\image-20200612110657733.png)]
如果我们需要看到 Spring 中的默认实现 DelegatingMessageSource

可以注释掉 @EnableAutoConfiguration 和 @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)

执行结果:

DelegatingMessageSource#toString 源码如下:所以输出的是 Empty MessageSource

	@Override
	public String toString() {
		return this.parentMessageSource != null ? this.parentMessageSource.toString() : "Empty MessageSource";
	}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6D80AmPP-1591933251665)(G:\workspace\csdn\learn-document\spring-framework\csdn\image-20200612111221081.png)]
Spring Boot 中的默认实现 ResourceBundleMessageSource

可以注释掉 @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)

执行结果:
在这里插入图片描述

9.面试题

9.1 Spring 国际化接口有哪些?

核心接口

  • org.springframework.context.MessageSource

层次性接口

  • org.springframework.context.HierarchicalMessageSource

9.2 Spring 有哪些 MessageSource 内建实现?

  • org.springframework.context.support.ResourceBundleMessageSource
  • org.springframework.context.support.ReloadableResourceBundleMessageSource
  • org.springframework.context.support.StaticMessageSource
  • org.springframework.context.support.DelegatingMessageSource

9.3 如何实现配置自动更新 MessageSource ?

主要技术

  • Java NIO 2 : java.nio.file.WatchService
  • Java Concurrency : java.util.concurrent.ExecutorService
  • Spring : org.springframework.context.support.AbstractMessageSource

实现步骤大致分为 6 步

  • 1.定位资源位置(properties 文件)
  • 2.初始化 Properties 对象
  • 3.实现 AbstractMessageSource#resolveCode
  • 4.监听资源文件(Java NIO 2 WatchService)
  • 5.线程池处理文件变化
  • 6.重新装载 Properties 对象

相关实现代码地址

github相关实现类 DynamicResourceMessageSource.java

调用代码

    public static void main(String[] args) throws InterruptedException {
        DynamicResourceMessageSource source = new DynamicResourceMessageSource();
        for (int i = 0; i < 10000; i++) {
            System.out.println(source.getMessage("name", new Object[]{}, Locale.getDefault()));
            Thread.sleep(1000L);
        }
    }

启动之后,找到 target/classes/META-INF 下面的 msg.properties 文件修改,保存文件 ctrl+s,实时修改保存之后,控制台打印结果会实时变化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5R8eElMZ-1591933251667)(G:\workspace\csdn\learn-document\spring-framework\csdn\image-20200612112947026.png)]
控制台输出结果:
在这里插入图片描述

10.参考

  • 极客时间-小马哥《小马哥讲Spring核心编程思想》
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值