从源码上理解spring国际化的原理

来源:互联网 发布:ssh上传图片到数据库 编辑:程序博客网 时间:2024/06/05 14:11

        前段时间做公司系统的国际化,使用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的获取都执行这些操作影响就不能忽略了;除非你的资源文件频繁变动,每次变动后不想重启服务器,设置了这个值,你要尽可能减少资源文件的个数,减少循环次数,最理想的就是只有一组资源文件;

如果不设置这个值,那么除了第一次回去遍历读取配置文件外,以后都会从缓存里面直接读取,所以效率很高!





1 0
原创粉丝点击