在学习Spring国际化之前,我们需要先来看一下Java中提供的国际化支持。

Java 国际化支持

ResourceBundle

将资源绑定在特定区域的对象中,然后我们通过运行时指定的区域地址找到特定的资源绑定对象取出值,通常情况,ResourceBundle支持peoperties和使用Java对象绑定区域资源的两种方式,下面简单演示一下使用方式:

  • • 使用properties文件方式绑定

先在resource目录下面创建三个properties文件,注意,文件名前半部分是相同的,后半部分是固定的,主要是各地区的名称代码,详情值可以到Locale对象中参照。

my_en_US.properties:

cancelKey=cancel

my_zh_CN.properties:

cancelKey=\u53D6\u6D88

my.properties

cancelKey=default cancel

测试:

通过配置Locale所属地区,我们会到对应的不同的文件中取值

public class ResourceBundleDemo {

  public static void main(String[] args) {
    // 本地环境地址默认是zh_CN的
    ResourceBundle defaultBundle = ResourceBundle.getBundle("my");
    // 打印:取消
    System.out.println(defaultBundle.getString("cancelKey"));

    ResourceBundle usBundle = ResourceBundle.getBundle("my", Locale.US);
    // 打印:cancel
    System.out.println(usBundle.getString("cancelKey"));

    ResourceBundle ukBundle = ResourceBundle.getBundle("my", Locale.UK);
    // 打印:取消
    System.out.println(ukBundle.getString("cancelKey"));
  }
}

说明:如上面示例中,如果是中文操作系统下并且未指定Locale时,程序会首先在classpath下寻找my_zh_CN.properties文件,若my_zh_CN.properties文件不存在,则取找my_zh.properties,如还是不存在,继续寻找my.properties,若都找不到就抛出异常。如果设置了的地区未找到对应的配置文件,则会以本地的来进行解析。

  • • Java对象进行绑定

Java对象绑定的一个核心接口是ListResourceBundle,所有通过对象绑定的都要实现改对象,改对象主要返回一个二维数组的形式来模拟了properties中的那种key-value的形式。

下面我们来看一下使用方式:

如properties中一样,我们首先我们先创建如下几个对象,对象命名方式和properties一样:

MyResourceBundle_zh_CN:

public class MyResourceBundle_zh_CN extends ListResourceBundle {

  @Override
  protected Object[][] getContents() {
    return new Object[][]{
      {"OkKey", "确定"},
      {"CancelKey", "取消"}
    };
  }
}

MyResourceBundle_en_US:

public class MyResourceBundle_en_US extends ListResourceBundle {

  @Override
  protected Object[][] getContents() {
    return new Object[][]{
      {"OkKey", "OK"},
      {"CancelKey", "Cancel"}
    };
  }
}

测试:

public class ListResourceBundleDemo {

  public static void main(String[] args) {
    // 本地环境地址默认是zh_CN的
    ResourceBundle defaultResources = ResourceBundle.getBundle("cn.phshi.resourcebundle.MyResourceBundle");
    // 打印:确定
    System.out.println(defaultResources.getString("OkKey"));

    ResourceBundle usResources = ResourceBundle.getBundle("cn.phshi.resourcebundle.MyResourceBundle", Locale.US);
    // 打印:OK
    System.out.println(usResources.getString("OkKey"));

    ResourceBundle ukResources = ResourceBundle.getBundle("cn.phshi.resourcebundle.MyResourceBundle", Locale.UK);
    // 打印:确定
    System.out.println(ukResources.getString("OkKey"));
  }
}

在未匹配到对应区域的配置时,匹配方式和上面的prperties一样。

文本格式化

Java文本格式化的核心接口是MessageFormat,他在Java中提供了一种以与语言无关的方式生成连接消息的方法。使用此选项可以构造为最终用户显示的消息。

下面我们主要讲的是MessageFormat的简单实用方式,首先,我们先来看一下简单示例:

public class MessageFormatDemo {

  public static void main(String[] args) {
    MessageFormat messageFormat = new MessageFormat("The current time is:{0,date,full}");
    String format = messageFormat.format(new Object[]{new Date()});
    // 输出:The current time is:2023年5月1日 星期一
    System.out.println(format);
  }
}

从上面我们可以看出,在创建MessageFormat时候我们传入需要格式化的字符串,字符串中的{0,date,full}中的具体内容,而大括号中有三个参数

  • • 第一个是格式化字符的定位,也就是下面数组中的下标定位
  • • 第二个参数是格式化类型,支持numberdate timechoice
  • • 第三个参数是格式化风格,支持shortmediumlongfull integercurrencypercentSubformatPattern

下面我们演示一下基本使用:

public class MessageFormatDemo {

  public static void main(String[] args) {
    // 1、简单格式化
    MessageFormat messageFormat = new MessageFormat("The current time is:{0,date,full}");
    String format = messageFormat.format(new Object[]{new Date()});
    System.out.println(format);

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

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

    // 4、根据参数索引来设置 Pattern
    messageFormat.setFormat(1,new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"));
    format = messageFormat.format(new Object[]{7, new Date(), "a disturbance in the Force"});
    System.out.println(format);

    // 5、ChoiceFormat格式化
    messageFormat.applyPattern("The disk \"{1}\" contains {0}.");
    // 标识参数参数为0展示no files,1展示:one file,2或者其他都展示 数量 files
    ChoiceFormat fileform = new ChoiceFormat(new double[]{0, 1, 2}, new String[]{"no files", "one file", "{0,number} files"});
    messageFormat.setFormatByArgumentIndex(0, fileform);
    System.out.println(messageFormat.format(new Object[]{0, "MyDisk"}));
    System.out.println(messageFormat.format(new Object[]{1, "MyDisk"}));
    System.out.println(messageFormat.format(new Object[]{2, "MyDisk"}));
    System.out.println(messageFormat.format(new Object[]{100, "MyDisk"}));
  }
}

输出结果:

The current time is:2023年5月1日 星期一
At 11:07:15 on 2023年5月1日 星期一, there was a disturbance in the Force on planet 7
At 11:07:15 AM on Monday, May 1, 2023, there was a disturbance in the Force on planet 7
At 11:07:15 AM on 2023-05-01 11:07:15, there was a disturbance in the Force on planet 7
The disk "MyDisk" contains no files.
The disk "MyDisk" contains one file.
The disk "MyDisk" contains 2 files.
The disk "MyDisk" contains 100 files.

更多使用方式可以参照Java源码中MessageFormat的相关注释。

Spring国际化

在Spring中,他对Java中提供的ResourceBundle + MessageFormat组合起来共同实现的国际化。

需要关注的部分:

  • • Spring国际化的核心接口是MessageSource
  • • Spring中的MessageSource也采用了类似于BeanFactory的分层结构(存在父级MessageSource),父级抽象接口为HierarchicalMessageSource
  • • Spring内置了几个默认的国际化资源实现,其中常用的两个实现有ReloadableResourceBundleMessageSourceResourceBundleMessageSource,两个都是基于AbstractMessageSource抽象实现的
  • • ReloadableResourceBundleMessageSource: 提供外部资源(propertiesxml)加载的国际化实现方案,并提供了定时刷新功能,允许在不重启系统的情况下,更新资源的信息
  • • ResourceBundleMessageSource: 提供 Java对象进行绑定 进行国际化绑定的方式

现在,我们基于上面我们需要重点关注的部分来进行一一查看:

核心接口MessageSource

Spring国际化的核心接口是MessageSource,他的实现也比较简单,我们来看一下该接口:

public interface MessageSource {

  /**
   * 获取国际化信息
   * @param code 表示国际化资源中的属性名;
   * @param args用于传递格式化串占位符所用的运行参数;
   * @param defaultMessage 当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;
   * @param locale 表示本地化对象
   */
  @Nullable
  String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);

  /**
   * 与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出NoSuchMessageException异常
   */
  String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

  /**
   * @param MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一个方法相同
   */
  String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}

国际化分层结构的

国际化MessageSource也被设定为如BeanFactory的一样,具有分层结构,对于国际化的分层结构的具体实现是HierarchicalMessageSource结接口,该接口也比较简单

public interface HierarchicalMessageSource extends MessageSource {

    /**
     * 设置父级的国际化资源对象
     */
    void setParentMessageSource(@Nullable MessageSource parent);

    /**
     * 返回父级的国际化资源对象
     */
    @Nullable
    MessageSource getParentMessageSource();
}

Spring针对国际化的抽象

上面说过,Spring内置了几个默认的国际化资源实现,其中常用的两个实现是ReloadableResourceBundleMessageSourceResourceBundleMessageSource,他们两个都是基于AbstractMessageSource抽象实现的,而且,如果我们想要实现自定义的国际化方式也可以继承该抽象方法,下面我们来简单看一下该抽象方法的核心内容。

针对与AbstractMessageSource,我们选取MessageSource接口中的其中一个getMessage方法来分析,其他方法也是差不多的。

getMessage:

// 实现了HierarchicalMessageSource,支持国际化分层结构
public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {

  // 注意改方法被final修饰,不能重写了
  @Override
  public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
    // 内部获取消息的方式
    String msg = getMessageInternal(code, args, locale);
    if (msg != null) {
      return msg;
    }
    // 通过code查询返回默认消息(子类也可以重写该方法设置默认的消息内容)
    String fallback = getDefaultMessage(code);
    if (fallback != null) {
      return fallback;
    }
    throw new NoSuchMessageException(code, locale);
  }
}

getMessageInternal:

protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {

  Object[] argsToUse = args;
  if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
    // 如果是允许不使用消息格式并且占位符参数为空的时候直接返回的字符串
    // 其实内部也是执行的resolveCode(code, locale),只不过格式化时候arg为空数组
    String message = resolveCodeWithoutArguments(code, locale);
    if (message != null) {
      return message;
    }
  } else {
    // 先国际化参数
    argsToUse = resolveArguments(args, locale);
    // 通过code获得国际化的消息(抽象方法,子类通过实现该方法实现国际化)
    MessageFormat messageFormat = resolveCode(code, locale);
    if (messageFormat != null) {
      synchronized (messageFormat) {
        // 格式化输出
        return messageFormat.format(argsToUse);
      }
    }
  }

  // 通过code到公共消息中查找
  Properties commonMessages = getCommonMessages();
  if (commonMessages != null) {
    String commonMessage = commonMessages.getProperty(code);
    if (commonMessage != null) {
      return formatMessage(commonMessage, args, locale);
    }
  }

  // 未找到消息,到父级中继续查找
  return getMessageFromParent(code, argsToUse, locale);
}

@Nullable
protected abstract MessageFormat resolveCode(String code, Locale locale);

所以通常我们需要实现自定义的国际化的时候只需要继承AbstractMessageSource,并且重写resolveCode这个方法即可。

Spring内置国际化实现

接下来我们来看一下Spring内置的ReloadableResourceBundleMessageSourceResourceBundleMessageSource两个国际化实现的使用方式,但是,在使用的时候,我们必须遵守一个Spring中针对于国际化资源实现的约定。

注意:Spring中要求注入IOC中的国际化资源的Bean名称必须是messageSource

为什么说Spring中约定了国际化资源的Bean名称必须是messageSource,首先我们要知道,其实Spring的上下文本身就是一个MessageSource的实现类。如下图我们可以看到AbstractApplicationContext对象是实现了MessageSource这个对象的。

image-20230505223148658

但是,在Spring上下文中,本身并没有对国际化资源实现,而是进行了一个代理,我们可以看看下面代码:

public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {

  // 国际化资源的代理
  private MessageSource messageSource;

  @Override
  public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
    // 委托给代理对象执行
    return getMessageSource().getMessage(code, args, locale);
  }
}

我们知道了Spring的国际化其实是委托给了messageSource这个属性进行执行的,那么下面我们就来看看messageSource这个属性的初始化。他的初始化主要是在AbstractApplicationContext#initMessageSource()这个方法中。

public static final String MESSAGE_SOURCE_BEAN_NAME = "messageSource";

protected void initMessageSource() {
  ConfigurableListableBeanFactory beanFactory = getBeanFactory();
  // MESSAGE_SOURCE_BEAN_NAME便是前面所说的messageSource,所以只有IOC容器中存在这个对象才会进入方法中
  if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
    // 复制messageSource为容器中名称为messageSource这个对象
    this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
    if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
      HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
      if (hms.getParentMessageSource() == null) {
        // 如果messageSource是分层结构并且不存在上级,则设置他的上级为当前Spring上下文的parent
        hms.setParentMessageSource(getInternalParentMessageSource());
      }
    }
  }
  else {
    // 否则注册一个默认的MessageSource
    DelegatingMessageSource dms = new DelegatingMessageSource();
    dms.setParentMessageSource(getInternalParentMessageSource());
    this.messageSource = dms;
    beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
  }
}

知道了上面这些接下来我们就来简单演示一下ReloadableResourceBundleMessageSourceResourceBundleMessageSource的使用

ReloadableResourceBundleMessageSource:

首先在resource目录下创建三个文件:

  • • my.properties
cancelKey=default cancel
  • • my_en_US.properties
cancelKey=cancel
  • • my_zh_CN.properties
cancelKey=\u53D6\u6D88

简单实用方式:

public class ReloadableMessageSourceDemo {

  public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(ReloadableMessageSourceDemo.class);
    context.refresh();

    String cancelKey = context.getMessage("cancelKey", null, null);
    // 打印:取消
    System.out.println(cancelKey);
    context.close();
  }

  // 注意注册的Bean名称
  @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)
  public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    messageSource.setBasename("classpath:my");
    // 刷新缓存时间
    messageSource.setCacheSeconds(10);
    return messageSource;
  }
}

注意虽然ReloadableResourceBundleMessageSource是可以通过设置setCacheSeconds来设置缓存加载文件的缓存时间,但是,他是存在着限制的,官方严格表面,请勿在生产环境中使用 。所以如果想要实现动态加载的国际化方式只能够自己去实现AbstractMessageSource实现。

ResourceBundleMessageSource:

首先先创建读取的资源对象:

  • • MyResourceBundle_en_US
public class MyResourceBundle_en_US extends ListResourceBundle {

  @Override
  protected Object[][] getContents() {
    return new Object[][]{
      {"OkKey", "OK"},
      {"CancelKey", "Cancel"}
    };
  }
}
  • • MyResourceBundle_zh_CN
public class MyResourceBundle_zh_CN extends ListResourceBundle {

  @Override
  protected Object[][] getContents() {
    return new Object[][]{
      {"OkKey", "确定"},
      {"CancelKey", "取消"}
    };
  }
}

简单实用方式:

public class ResourceBundleMessageSourceDemo {

  public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(ResourceBundleMessageSourceDemo.class);
    context.refresh();

    String cancelKey = context.getMessage("OkKey", null, null);
    System.out.println(cancelKey);
    context.close();
  }

  // 注意注册的Bean名称
  @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)
  public MessageSource messageSource() {
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasename("cn.phshi.resourcebundle.MyResourceBundle");
    return messageSource;
  }
}