在学习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}
中的具体内容,而大括号中有三个参数
- • 第一个是格式化字符的定位,也就是下面数组中的下标定位
- • 第二个参数是格式化类型,支持
number
,date
,time
,choice
- • 第三个参数是格式化风格,支持
short
,medium
,long
,full
,integer
,currency
,percent
,SubformatPattern
下面我们演示一下基本使用:
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内置了几个默认的国际化资源实现,其中常用的两个实现有
ReloadableResourceBundleMessageSource
和ResourceBundleMessageSource
,两个都是基于AbstractMessageSource
抽象实现的- •
ReloadableResourceBundleMessageSource
: 提供外部资源(properties
、xml
)加载的国际化实现方案,并提供了定时刷新功能,允许在不重启系统的情况下,更新资源的信息- •
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内置了几个默认的国际化资源实现,其中常用的两个实现是ReloadableResourceBundleMessageSource
和ResourceBundleMessageSource
,他们两个都是基于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内置的ReloadableResourceBundleMessageSource
和ResourceBundleMessageSource
两个国际化实现的使用方式,但是,在使用的时候,我们必须遵守一个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);
}
}
知道了上面这些接下来我们就来简单演示一下ReloadableResourceBundleMessageSource
和ResourceBundleMessageSource
的使用
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;
}
}