浅出深入的了解下 Spring 国际化的使用和实现。
使用场景
当 App 或者 Web 需要国际化支持时, 以下场景可能会涉及到国际化文案
- 根据所在的语言地区获取服务器动态接口数据时,需要返回相应语言地区的文本文案。
- 利用 Hibernate Validator 验证
- Bean Validation 校验国际化文案
- Web MVC 错误消息提示
- 给 App 发送通知
Just Try
1. pom 文件引入
新建 springboot 工程项目,引入以下依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
新建测试文件
文件安排如上图所示,
- MessageSource_ZH_cn.properties
name=\u8001\u5e05
这里需要 unicode 编码, 因为ResourceBundleMessageSource 类注释上有这个说明。
On the classpath, bundle resources will be read with the locally configured encoding: by default, ISO-8859-1;
- MessageSource_en.properties
name=kaitoshy
- SpringI18nTest
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SpringI18nTest.class)
public class SpringI18nTest {
// org.springframework.context.support.AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME
@Bean(MESSAGE_SOURCE_BEAN_NAME)
public static MessageSource messageSource(ConfigurableEnvironment environment) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.addBasenames("bar/MessageSource");
return messageSource;
}
@Before
public void before() {
LocaleContextHolder.resetLocaleContext();
LocaleContextHolder.setDefaultLocale(Locale.CHINA);
}
@Autowired
private MessageSource messageSource;
@Test
public void testMessageSource() throws InterruptedException {
String name = messageSource.getMessage("name", new Object[0], Locale.ENGLISH);
// 输出:kaitoshy
System.out.println(name);
String cnName = messageSource.getMessage("name", new Object[0], LocaleContextHolder.getLocale());
// 输出: 老帅
System.out.println(cnName);
}
}
以上你就可以体验到 SpringBoot 对于国际化的处理了。
核心讲解
由上可知,要使用 Spring Framework 的国际化,需要注册一个 MessageSource 的 bean,这一点我们从官方文档上也可以得知:
If the ApplicationContext cannot find any source for messages, an empty DelegatingMessageSource is instantiated in order to be able to accept calls to the methods defined above.
如果 ApplicationContext 无法找到任何消息源,则会实例化一个空的 DelegatingMessageSource,以便能够接受对上述方法的调用。针对于 MessageSource 官网也给出了 3 种实现,
Spring provides three MessageSource implementations, ResourceBundleMessageSource, ReloadableResourceBundleMessageSource and StaticMessageSource.
ResourceBundleMessageSourceReloadableResourceBundleMessageSourceStaticMessageSource
调试上述代码,从 messageSource.getMessagegetMessage 进入
AbstractMessageSource 类
@Override
public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
String msg = getMessageInternal(code, args, locale);
...
}
protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {
...
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);
}
}
}
...
}
上述代码中的 resolveCodeWithoutArguments 会调用 resolveCode, 而各自的子类会实现resolveCode, 当然了各自的子类也可以覆盖父类的
resolveCodeWithoutArguments。
毕竟在父类的实现中使用了锁机制。
@Nullable
protected abstract MessageFormat resolveCode(String code, Locale locale);
以 ResourceBundleMessageSource 中的实现为例:
@Override
@Nullable
protected MessageFormat resolveCode(String code, Locale locale) {
// baseName 定义了国际化文件的文件路径
Set<String> basenames = getBasenameSet();
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
}
...
return null;
}
@Nullable
protected ResourceBundle getResourceBundle(String basename, Locale locale) {
if (getCacheMillis() >= 0) {
...
return doGetBundle(basename, locale);
}
...
}
// 这里又使用了 JDK 的 ResourceBundle.getBundle 来获取相应的内容
protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
...
return ResourceBundle.getBundle(basename, locale, classLoader);
}
最终由回到了 JDK 的实现。
总结
MessageSource 中几个核心的要素
国家语言
- java.util.Locale
表示了国家,语言,方言的本地化对象
- org.springframework.context.i18n.LocaleContextHolder
ThreadLocal 实现的本地化上下文
文本存储:
- properties, xml, java 代码
- 可以拓展:MySQL, Nacos 等媒介
文本替换:
- java.text.MessageFormat#format
- java.util.Formatter#format
- org.slf4j.helpers.MessageFormatter#arrayFormat