前段时间做公司系统的国际化,使用spring国际化框架实现,后来发现有个别的页面,浏览器语言设置为中文时,界面偶尔夹杂着一两个英文的翻译,以为国际化中文写为了英文,同事检查后发现一个奇怪的问题:
- 显示为英文的这个key在资源文件a_i18n_zh_CN.properties和a_i18n_en_US.properties里面都有这个key,而且都做了相应语言的翻译,不存在中文翻译为英文的情况;
- .显示为英文的这个key 与其它的key不同之处在于在资源文件 b_i18n_en_US.properties 里也有这个key的英文翻译,但是b_i18n_zh_CN.properties却没有这个key的中文翻译(哎,n手代码,历史遗留问题太多,我们资源文件有好几个,不同资源文件中,存在一些重复的key,);
经测试在浏览器设置为中文的时候,加载了b_i18n_en_US.properties里面的英文key,所以很奇怪为什么没有加载a_i18n_zh_CN.properties里面的中文key,而选择了去加载一个只有英文key存在的b_i18n_en_US.properties;
同事的推测是资源文件加载的顺序是随机的,还有的同事说,配置文件中后面加载的文件会覆盖掉前面加载的文件中相同的key,但是经过推敲,很明显这些说法都站不住脚;
为了彻底的解决这个问题,只好祭出终极必杀技:读源码;
ps:读源码其实并不是一个很困难很高大上的事情,只要
while(不懂){
读源码;
}
一切问题都会迎刃而解;
下面进入正题,首先要加载源码,我们使用maven自动下载源码,运行mvn dependency:sources即可!
从spring国际化资源文件的配置类作为入口
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:i18n/report1_i18n</value>
......
</list>
</property>
<property name="cacheSeconds" value="5"/>
<property name="defaultEncoding" value="UTF-8"></property>
<property name="fallbackToSystemLocale" value="false"></property>
</bean>
打开org.springframework.context.support.ReloadableResourceBundleMessageSource这个类
public class ReloadableResourceBundleMessageSource extends AbstractMessageSource implements ResourceLoaderAware {
//配置文件中配置的资源文件数组
private String[] basenames = new String[0];
//是否启用如果查找资源失败则使用本地服务器的语言设置进行查询
private boolean fallbackToSystemLocale = true;
//缓存时间
private long cacheMillis = -1;
这个类中的三个最重要的成员变量,我已经加入了注释说明(为了避免长篇贴源码给大家造成的不适,仅贴出关键代码部分)。
我们先来看看basenames变量
这个变量保存了我们配置文件中配置的资源列表,是通过类里面的setBasenames方法设置的:
public void setBasenames(String... basenames) {
if (basenames != null) {
this.basenames = new String[basenames.length];
for (int i = 0; i < basenames.length; i++) {
String basename = basenames[i];
Assert.hasText(basename, "Basename must not be empty");
this.basenames[i] = basename.trim();
}
}
else {
this.basenames = new String[0];
}
}
如果我们的配置文件如下:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:i18n/report1_i18n</value>
<value>classpath:i18n/report2_i18n</value>
<value>classpath:i18n/report3_i18n</value>
<list>
<property name="cacheSeconds" value="5"/>
<property name="defaultEncoding" value="UTF-8"></property>
<property name="fallbackToSystemLocale" value="false"></property>
</property>
</bean>
那么执行过该方法后,basenames的取值如下:
basenames={
"classpath:i18n/report1_i18n",
"classpath:i18n/report2_i18n",
"classpath:i18n/report3_i18n"
}
数组顺序与我们 配置的list顺序是一样的。
明白了basenames成员变量的作用,我们继续看该类另外一个重要的方法,nresolveCodeWithoutArguments,这个方法就是根据key与locale通过一系列操作,返回最终国际化的内容,在这个方法里面我们会看到上面提到的成员变量cacheMillis:
protected String resolveCodeWithoutArguments(String code, Locale locale) {
//cacheMillis默认为-1,如果我们设置这个值小于0,则将资源文件永久进行缓存
if (this.cacheMillis < 0) {
// PropertiesHolder是一个内部类,资源文件读取进来后被封装为PropertiesHolder对象;
PropertiesHolder propHolder = getMergedProperties(locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
} else {//如果设置了缓存时间则执行else分支,因为配置文件中配置了cacheSeconds为5秒,所以cacheMillis=5000毫秒,所以会执行以下代码
for (String basename : this.basenames) {
//calculateAllFilenames这个方法很重要,它决定了要去什么文件去查找资源文件
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
}
}
return null;
}
因为在配置文件中我们配置了cacheSecond属性为5秒,这个值会被转换为毫秒赋值给cacheMillis=5000,
public void setCacheSeconds(int cacheSeconds) {
this.cacheMillis = (cacheSeconds * 1000);
}
它的默认值为-1,如果我们没有在配置文件中设置这个值,则只会加载一次配置文件,所以走的分支为if条件为true的分支,但是这里为5000,所以会走else分支分支里面是一个for循环来遍历成员变量basenames,先来看for循环里的第一行代码
List<String> filenames = calculateAllFilenames(basename, locale);
这个calculateAllFilenames方法的作用就是根据从basenames中取出的一个配置(如果for循环是第一次执行那么传入的就是basenames[0]="classpath:i18n/report1_i18n")和locale(这里的locale一般是用户浏览器端发起请求时浏览器指定的语言,一般我们都是中文)来计算出我们真正要读取的资源文件的数组(为什么是数组?看源码分析),首先贴该方法的源码!
protected List<String> calculateAllFilenames(String basename, Locale locale) {
//如果之前已经查询过这个文件,则会将结果放入缓存,先从缓存里面查询,查询到直接返回;
Map<Locale, List<String>> localeMap = this.cachedFilenames.get(basename);
if (localeMap != null) {
List<String> filenames = localeMap.get(locale);
if (filenames != null) {
return filenames;
}
}
//以下处理是缓存没有命中的处理
List<String> filenames = new ArrayList<String>(7);
//根据filename和locale信息生成完整文件名列表
filenames.addAll(calculateFilenamesForLocale(basename, locale));
//如果fallbackToSystemLocale为true(默认为true),并且服务器本地语言与用户请求语言不一致,则再根据本地语言生成完整文件名列表
if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) {
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault());
for (String fallbackFilename : fallbackFilenames) {
if (!filenames.contains(fallbackFilename)) {
// Entry for fallback locale that isn't already in filenames list.
filenames.add(fallbackFilename);
}
}
}
filenames.add(basename);
//将结果存入缓存
if (localeMap == null) {
localeMap = new ConcurrentHashMap<Locale, List<String>>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
}
}
localeMap.put(locale, filenames);
return filenames;
}
先看第13行代码filenames.addAll(calculateFilenamesForLocale(basename, locale));
calculateFilenamesForLocale(basename,locale)这个方法就是根据从配置文件取出的配置来计算出资源文件名列表,并将其放到filename列表里,看看它的源码实现
protected List<String> calculateFilenamesForLocale(String basename, Locale locale) {
List<String> result = new ArrayList<String>(3);
String language = locale.getLanguage();
String country = locale.getCountry();
String variant = locale.getVariant();
StringBuilder temp = new StringBuilder(basename);
temp.append('_');
if (language.length() > 0) {
temp.append(language);
result.add(0, temp.toString());
}
temp.append('_');
if (country.length() > 0) {
temp.append(country);
result.add(0, temp.toString());
}
if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) {
temp.append('_').append(variant);
result.add(0, temp.toString());
}
return result;
}
如果上面方法中
basename="classpath:i18n/report1_i18n" //配置文件列表中的第一个配置文件
locale = new Locale("zh","CN") //中文-中国
那么最终result的返回值为
result={
"classpath:i18n/report1_i18n_zh_CN",
"classpath:i18n/report1_i18n_zh",
}
看完了calculateFilenamesForLocale方法,我们再返回calculateAllFilenames方法的第15行接着分析,
if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) {
这条判断就是本文开头提到的问题的原因!
这条判断就是本文开头提到的问题的原因!
这条判断就是本文开头提到的问题的原因!
重要的事情说三遍!
这是一个条件判断,这里我们看到了我们开头说的最重要的三个变量中的最后一个变量fallbackToSystemLocale,它的默认值为true,然后看另外一个条件!locale.equals(Locale.getDefault()),如果用户的locale与服务器端的locale不一致,则执行if内的方法;
假设用户浏览器端的语言为中国中文,也就是new Locale("zh","Cn"),服务器通设置的语言为美国英语也就是new Locale("en","US"),那么在第16行再次执行calculateFilenamesForLocale方法
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault());
执行过该方法后又计算出来了两个文件
fallbackFilenames ={
"classpath:i18n/report1_i18n_en_US",
"classpath:i18n/report1_i18n_en",
}
所以最终calculateAllFilenames返回的
filenames={
"classpath:i18n/report1_i18n_zh_CN",
"classpath:i18n/report1_i18n_zh",
"classpath:i18n/report1_i18n_en_US",
"classpath:i18n/report1_i18n_en",
"classpath:i18n/report1_i18n",
}
它会查找前缀为这五个的properties文件,将其封装为PropertiesHolder对象(PropertiesHolder是ReloadableResourceBundleMessageSource内部类,其实就是对Properties对象的封装,很简单,这里就不贴源码了,有兴趣大家自己去看),然后根据key依次查找这五个资源文件有没有这个key。
所以问题就来了,如果用户浏览器是中文,服务器本地语言设置的是英文,当用户请求“i18n_login”这个key时,在中文资源文件report1_i18n_zh_CN.properties中没有“i18n_login”这个key,而在英文资源文件report1_i18n_en.properties中有“i18n_login=login”这个key,那么最终返回给用户的是英文login!
到此为止找到了问题的原因所在,所以最后在配置文件中加上了我们上面配置文件中看到的:
<property name="fallbackToSystemLocale" value="false"></property>
问题解决!
小结:通过读取源码发现spring国际化也有一些效率需要提高的地方,推荐以下做法
1.
<property name="fallbackToSystemLocale" value="false"></property>
将这个值设置为false,不会出现一些奇怪的中英结合的问题;
2.
<property name="cacheSeconds" value="5"/>
这个值最好不要去动,默认为-1,如果设置了,每次都会执行for循环去遍历basename数组,根据计算出来的文件数组,去获取PropertiesHolder,然后从propertiesHolder中取国际化值,没取到就遍历下一个basename,直到找到key或遍历完成为止(当然它在过期时间内也会做一些缓存,但是这个for循环是一定会执行的),虽然效率影响不大,但是想想国际化的每一个key的获取都执行这些操作影响就不能忽略了;除非你的资源文件频繁变动,每次变动后不想重启服务器,设置了这个值,你要尽可能减少资源文件的个数,减少循环次数,最理想的就是只有一组资源文件;
如果不设置这个值,那么除了第一次回去遍历读取配置文件外,以后都会从缓存里面直接读取,所以效率很高!