国际化
国际化也称作i18n,其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数。由于软件发行可能面向多个国家,对于不同国家的用户,软件显示不同语言的过程就是国际化(举个例子,人们玩的电子游戏,通常可以选择多个语言版本,适应于多个国家的玩家)。通常来讲,软件中的国际化是通过配置文件来实现的,假设某个软件要支撑两种语言,那么就需要两个版本的配置文件。
Java国际化
Java自身是支持国际化的,java.util.Locale用于指定当前用户所属的语言环境等信息,java.util.ResourceBundle用于查找绑定对应的资源文件。
Locale包含了language信息和country信息,Locale创建默认locale对象时使用的静态方法:
/**
* This method must be called only for creating the Locale.*
* constants due to making shortcuts.
*/
private static Locale createConstant(String lang, String country) {
BaseLocale base = BaseLocale.createInstance(lang, country);
return getInstance(base, null);
}
配置文件命名规则:
basename_language_country.properties
必须遵循以上的命名规则,java才会识别。其中,basename是必须的,语言和国家是可选的。这里存在一个优先级概念,如果同时提供了messages.properties和messages_zh_CN.propertes两个配置文件,如果提供的locale符合en_CN,那么优先查找messages_en_CN.propertes配置文件,如果没查找到,再查找messages.properties配置文件。最后,提示下,所有的配置文件必须放在classpath中,一般放在resource目录下。
举个例子,两个配置文件内容分别如下:
#messages.properties
test=hello1
#messages_en_CN.propertes
test=hello2
代码:
//ResourceBundle.getBundle接受两个参数:basename,locale
System.out.println(ResourceBundle.getBundle("messages",new Locale("en","CN")).getString("test"));
打印结果:
hello2
以下命名的配置文件优先级从低到高:
messages.properties
messages_en.properties
messages_en_CN.properties
通过配置不同的Locale相关的资源文件,我们可以通过key值取到相应环境的value值,这样便做到了国际化,这种方式是非侵入式的,让我们编写代码时可以不需要考虑国际化的问题,只需要根据配置规则配置相关资源文件,在实际读取资源配置时,指定相应的locale即可。这也是约定优于配置的体现。
Spring国际化
spring使用MessageSource接口实现国际化。两个实现类为:
ResourceBundleMessageSource:基于java的ResourceBundle实现了国际化,配置文件必须放在classpath下。
ReloadableResourceBundleMessageSource:直接使用读取文件的方式实现国际化,规则跟java的相同,支持动态修改后刷新配置,避免在业务不能中断的情况下重启进程。配置文件可以放在任意目录下,指定目录后,该类会去指定目录中加载配置文件。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.stereotype.Component;
import java.util.Locale;
@Component
public class MessageService {
@Autowired
private MessageSource messageSource;
private Locale currentLocale = new Locale("en");
@Bean
public static MessageSource getMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setCacheSeconds(5);
return messageSource;
}
public String getMessage(String key) {
return messageSource.getMessage(key, null, key, currentLocale);
}
}
ReloadableResourceBundleMessageSource的优点在于可以不重启进程动态刷新配置,以及指定资源目录,不需要强制放在classpath下面,如果需要放在classpath中,那么只需要在basename中的资源路径上添加"classpath:",便可以在classpath中查找配置。messageSource.setCacheSeconds用于设置配置的过期时间,单位为秒,messageSource.setCacheSeconds(5)代表每5秒配置文件就会过期,再重新查询时,就会重新加载配置。
spring的默认配置文件命名规则跟java是相同的,也都遵循约定优于配置的思路。
Locale信息的获取
一般来说,典型的B/S架构,用户可能在任何地点使用任何语言登陆网站,但是网站后台是部署在固定的服务器或者是云服务上的,那么如何让后端获取客户端(浏览器端)的locale信息,从而做到针对不同的客户端进行国际化呢?spring使用LocaleResolver解析用户的http请求来获取对应的locale信息。下面的代码是典型的controller,通过解析HttpServletRequest请求来获取locale信息。
@Controller
public class HomeController extends BaseController {
@RequestMapping(value = "", method = GET)
public String homeDefault() {
return homePageUrl();
}
@RequestMapping(value = "/locales", method = GET)
public ResponseEntity<OpenLmisResponse> getLocales(HttpServletRequest request) {
messageService.setCurrentLocale(RequestContextUtils.getLocale(request));
return response("locales", messageService.getLocales());
}
@RequestMapping(value = "/changeLocale", method = PUT, headers = ACCEPT_JSON)
public void changeLocale(HttpServletRequest request) {
messageService.setCurrentLocale(RequestContextUtils.getLocale(request));
}
}
RequestContextUtils.getLocale(request)实现如下。RequestContextUtils是spring提供的工具类。可以看出LocaleResolver从设计上支持多种策略。
/**
* Retrieve the current locale from the given request, using the
* LocaleResolver bound to the request by the DispatcherServlet
* (if available), falling back to the request's accept-header Locale.
* <p>This method serves as a straightforward alternative to the standard
* Servlet {@link javax.servlet.http.HttpServletRequest#getLocale()} method,
* falling back to the latter if no more specific locale has been found.
* <p>Consider using {@link org.springframework.context.i18n.LocaleContextHolder#getLocale()}
* which will normally be populated with the same Locale.
* @param request current HTTP request
* @return the current locale for the given request, either from the
* LocaleResolver or from the plain request itself
* @see #getLocaleResolver
* @see org.springframework.context.i18n.LocaleContextHolder#getLocale()
*/
public static Locale getLocale(HttpServletRequest request) {
LocaleResolver localeResolver = getLocaleResolver(request);
return (localeResolver != null ? localeResolver.resolveLocale(request) : request.getLocale());
}
LocaleResolver包含以下四种策略实现。AcceptHeaderLocaleResolver,CookieLocaleResolver,FixedLocaleResolver和SessionLocaleResolver。
下面的代码是DispatcherServlet中初始化LocaleResolver的过程,可以看到LocaleResolver实际是支持用户进行配置的,如果没有配置,那么使用getDefaultStrategy方法获取默认策略。
/**
* Initialize the LocaleResolver used by this class.
* <p>If no bean is defined with the given name in the BeanFactory for this namespace,
* we default to AcceptHeaderLocaleResolver.
*/
private void initLocaleResolver(ApplicationContext context) {
try {
this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
if (logger.isDebugEnabled()) {
logger.debug("Using LocaleResolver [" + this.localeResolver + "]");
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate LocaleResolver with name '" + LOCALE_RESOLVER_BEAN_NAME +
"': using default [" + this.localeResolver + "]");
}
}
}
查看项目工程代码,发现项目中配置的是CookieLocaleResolver。
几种LocaleResolver的用法不同,不过从名字便可以看出一二。
AcceptHeaderLocaleResolver:直接从Http请求的Header中通过accept-language获取locale信息
CookieLocaleResolver:从cookie中获取,如果获取不到,也是通过accept-language获取locale信息
SessionLocaleResolver:从session中获取,如果获取不到,也是通过accept-language获取locale信息
FixedLocaleResolver:固定locale,基本没啥用
至此,整个后端的国际化过程已经比较清楚:后端通过解析客户端(浏览器端)传递的locale信息,将locale拿出,然后再通过spring的MessageSource类传入locale信息,取出相应的配置文件,解析出相应配置,从而便完成了国际化。