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

2 篇文章 0 订阅

        前段时间做公司系统的国际化,使用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
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值