DispatcherServlet使得开发人员可以通过客户端的Locale来自动转换消息进行国际化;在整个国际化的过程中,主要分成两步,一是解析客户端的Locale,一是查找国际化的消息;
1. 解析Locale
解析Locale是通过LocaleResolver来完成的。
当有请求时,DispatcherServlet查找LocaleResolver类型的Bean,如果找到,则使用它获取并设置Locale。在这之后,通过RequestContext.getLocale方法则可以获取到Locale。
Spring默认在i18n包中提供了几种实现,如通过Cookie、Header、Session等方式来获取Locale的实现等。
1.1 定义所使用的LocaleResolver:
Spring Boot中可以通过Bean注解来定义所使用的LocaleResolver
@Configuration
public class AdditionalBeanConfig {
@Bean
public LocaleResolver getLocalResolver() {
return new CookieLocaleResolver();
}
}
默认情况下,SpringBoot中是没有包含LocaleResolver对象的。
1.2 获取Locale
如果需要在应用中获取当前客户端所使在的Locale,在Controller中通过以下方式查找Locale:
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/locale")
public String locale(HttpServletRequest request) {
// 第一种方式,通过构建RequestContext来查找Locale
RequestContext requestContext = new RequestContext(request);
System.out.println(requestContext.getLocale().toLanguageTag());
// 第二种方式, 通过使用LocaleContextHolder来获取Locale
System.out.println(LocaleContextHolder.getLocale().toLanguageTag());
// 第二种方式,通过RequestContextUtils来获取Locale
return RequestContextUtils.getLocale(request).toLanguageTag();
}
}
打印及返回的结果为:zh-CN
RequestContext:用于存储请求相关的一些信息,如当前请求的Web上下文、Theme、Locale等;
RequestContextUtils: 使用RequestContext的一个辅助类;提供静态方法来使用RequestContext,这样就不需要手动创建RequestContext对象;
2. 查找消息
查找消息一般通过MessageSource接口进行,它可以根据Code查找消息,同时支持国际化及参数传递。
2.1 MessageSource接口
当不配置任何的MessageSource时,Spring默认使用DelegatingMessageSource,如以下控制器:
@GetMapping("/message")
public String message() {
return messageSource.toString();
}
请求返回后是:org.springframework.context.support.DelegatingMessageSource@76b6471e
这个时候,我们使用MessageSource来获取Message:
@GetMapping("/message")
public String message(HttpServletRequest request) {
Locale locale = RequestContextUtils.getLocale(request);
return messageSource.getMessage("test", null, locale);
}
此时会报错,错误信息:
There was an unexpected error (type=Internal Server Error, status=500).
No message found under code 'test' for locale 'zh_CN'.
经过跟踪,这个异常在DelegatingMessageSource的getMessage方法中处理:
@Override
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(code, args, locale);
}
else {
throw new NoSuchMessageException(code, locale);
}
}
由于parentMessageSource为空,直接抛出异常,即访问接口时看到的消息。
2.2 指定MessageSource
MessageSource包含有以下几个实现类:
- StaticMessageSource: 基于程序注册的方式来管理资源;一般用于测试;
- ResourceBundleMessageSource: 基于JDK中的ResourceBundle实现;
- ReloadableResourceBundleMessageSource:包含运行时动态加载机制的实现;
2.2.1 StaticMessageSource
先使用StaticMessageSource来进行实验:
@Bean
public MessageSource messageSource() {
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage("test", Locale.CHINA, "测试消息");
return messageSource;
}
仍旧使用之前的Controller,这时不会报错直接会返回“测试消息”这个串了。
2.2.2 ResourceBundleMessageSource
上面已经说过,StaticMessageSource这种方式一般用于测试当中,在生产中一般使用ResourceBundleMessageSource。
我们将上面使用staticMessageSource的地方进行替换:
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// 通过setBasenames来设置国际化文件
messageSource.setBasenames("messages");
// 也可以指定多个,如:
// messageSource.setBasenames("messages", "errors");
return messageSource;
}
它会去Classpath下的目录中查找名称为mesages开头的国际化文件并进行加载。
在resources目录下添加messages.properties及messages_zh_cn.properties两个文件,并在message_zh_cn.properties中添加test=测试消息 的内容。
执行前面的Controller,可以看到获取到的消息。也可以仅在messages.properties中添加;它会先去对应Locale的文件中查找,如果未找到则去默认的messages.properties中查找;如果仍旧未找到则抛出异常。
有时候resources目录东西过多需要分子目录,可以在定义MessageSource的时候进行指定,如下面在resources下增加子目录i18n:
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// 通过setBasenames来设置国际化文件
messageSource.setBasenames("i18n.messages");
// 也可以指定多个,如:
// messageSource.setBasenames("messages", "errors");
return messageSource;
}
3. 总结
综上,在项目国际化过程中,需要创建LocaleResolver及MessageSource对象并注入到容器中,然后定义相关的国际化属性文件;在文件中定义好相关信息即可在需要使用的地方通过Autowired注入MessageSource对象并使用它来解析国际化消息了。
注意到在每个使用的地方都要传入Locale到MessageSource的方法中,实际上,通过使用LocaleContextHolder可以进行进一步的简化:
@Component
public class MessageSourceProxy {
@Autowired
private MessageSource messageSource;
public String getMessage(String code, Object[] params) {
return messageSource.getMessage(code, params, LocaleContextHolder.getLocale());
}
}
然后改造前面的Controller:
@Autowired
private MessageSourceProxy messageSourceProxy;
@GetMapping("/message")
public String message(HttpServletRequest request) {
// Locale locale = RequestContextUtils.getLocale(request);
// return messageSource.getMessage("test", null, locale);
return messageSourceProxy.getMessage("test", null);
}
这样简化后,使用者甚至都不用手动去获取Locale了。
本文主要分析国际化的配置过程是怎样完成的,关于MessageSource对象的使用,可以参考具体的API。