Spring的国际化是通过接口MessageSource来定义的。
先看下该接口:
public interface MessageSource {
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
一般用前两个就行,指定message的key和locale,如果有占位符替换,就传入args数组。
Spring的高级容器Application是继承了该接口的,说明Spring的高级容器也具备了国际化消息的能力。
但是最终的实现类一般是ReloadableResourceBundleMessageSource和ResourceBundleMessageSource,前者具备了定时刷新的能力。这里只看ResourceBundleMessageSource。
另外,其实java本省就提供了国际化的能力,主要是通过ResourceBundle实现的。而Spring的国际化底层还是通过ResourceBundle来实现的,可以理解为是对ResourceBundle的更高一级的封装。增强了消息的缓存以及增加了占位符替换的能力。
下面看下如何使用。
首先需要在spring的配置文件中配置消息的bean,这里用的是ResourceBundleMessageSource:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>message</value>
</list>
</property>
</bean>
</beans>
“basenames”就是国际化资源文件的前缀,这里支持配置多个资源化文件。
下面配置国际化资源文件:
这里只配置一条,你好,xxx的文案,支持中文和美式英文的。
message_zh_CN.properties:
helloKey=\u4f60\u597d\uff0c{0}
中文文件需要使用native2ascii命令转换格式才行,命令:native2ascii raw.properties message_zh_CN.properties
message_en_US.properties:
helloKey=hello,{0}
再写一个测试类:
public class Main {
public static void main(String args[]) {
ApplicationContext context = new ClassPathXmlApplicationContext("message.xml");
Locale locale = Locale.getDefault();
System.out.println(context.getMessage("helloKey", new String[]{"ly"}, locale));
System.out.println(context.getMessage("helloKey", new String[]{"ly"}, new Locale("en", "US")));
}
}
打印结果如下:
你好,ly
hello,ly
Process finished with exit code 0
最后看下实现。
Spring高级容器对getMessage的实现,其实是在AbstractApplicationContext里的:
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
return getMessageSource().getMessage(resolvable, locale);
}
private MessageSource getMessageSource() throws IllegalStateException {
if (this.messageSource == null) {
throw new IllegalStateException("MessageSource not initialized - " +
"call 'refresh' before accessing messages via the context: " + this);
}
return this.messageSource;
}
/** MessageSource we delegate our implementation of this interface to. */
@Nullable
private MessageSource messageSource;
本身是代理给了内部的一个MessageSource类型的实例做的。
插一嘴啊,现在发现,Spring有好多类似这样的代理逻辑,类的签名上implement了一个接口,但是真正实现的时候确实代理给其他类做的。感觉也是一个不错的设计。一个模块可能有N多功能,根据接口分离原则,不同类型的功能需要分成到不同的接口。最终的实现类不一定实现所有的接口,可以用代理的方式,转给其他实现类,这样,该类在使用上,具备了所有接口所约束的能力,但是类本身却不会有太过臃肿,一些接口的实现是代理给其他类做的,两全其美。
好,言归正传。
那么,这个messageSource实例是什么时候赋值的呢?
refresh函数有一步:
// Initialize message source for this context.
nitMessageSource();
就是在这里赋值的:
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 + "]");
}
}
}
从容器中取出name为“messageSource”的bean,所以是通过名字来取的,xml里的名字不能变。
最终实现是在ResourceBundleMessageSource的父类AbstractMessageSource中实现的:
@Override
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
if (defaultMessage == null) {
return getDefaultMessage(code);
}
return renderDefaultMessage(defaultMessage, args, locale);
}
@Nullable
protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {
if (code == null) {
return null;
}
if (locale == null) {
locale = Locale.getDefault();
}
Object[] argsToUse = args;
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
// Optimized resolution: no arguments to apply,
// therefore no MessageFormat needs to be involved.
// Note that the default implementation still uses MessageFormat;
// this can be overridden in specific subclasses.
String message = resolveCodeWithoutArguments(code, locale);
if (message != null) {
return message;
}
}
else {
// Resolve arguments eagerly, for the case where the message
// is defined in a parent MessageSource but resolvable arguments
// are defined in the child MessageSource.
argsToUse = resolveArguments(args, locale);
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(argsToUse);
}
}
}
// Check locale-independent common messages for the given message code.
Properties commonMessages = getCommonMessages();
if (commonMessages != null) {
String commonMessage = commonMessages.getProperty(code);
if (commonMessage != null) {
return formatMessage(commonMessage, args, locale);
}
}
// Not found -> check parent, if any.
return getMessageFromParent(code, argsToUse, locale);
}
里面区分了是否需要替换占位符,如果不需要就是走的resolveCodeWithoutArguments这个方法,这里就看下这个方法,因为整体上区别不大:
@Nullable
protected String resolveCodeWithoutArguments(String code, Locale locale) {
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(new Object[0]);
}
}
return null;
}
这里的resolveCode方法是在子类ResourceBundleMessageSource中实现的:
@Override
@Nullable
protected MessageFormat resolveCode(String code, Locale locale) {
Set<String> basenames = getBasenameSet();
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
if (messageFormat != null) {
return messageFormat;
}
}
}
return null;
}
这里就是从之前的xml中取出basenames属性,其实就是资源文件的前缀。
然后遍历资源文件,每一个前缀都能取出一个ResourceBundle实例,该实例是有缓存的。然后再取出一个MessageFormat来格式化消息,该实例也是有缓存的。具体的取逻辑就不深入了。