Spring国际化实现原理+源码解析

Spring在webmvc依赖包下提供了支持国际化的i18n包,支持根据请求中不同语言环境标志位来动态改变当前的语言环境,同时可以支持配置多个不同的语言资源配置,并自动根据当前的语言环境动态读取不同的资源配置值

代码如下:

i18包类结构:

由图可见,一共提供了四种支持国际化的实现类,分别是AcceptHeaderLocaleResolver、CookieLocaleResolver、FixedLocaleResolver和SessionLocaleResolver类,这些类都实现了LocaleContextResolver接口:

此接口提供了两个基本功能:

  • 解析请求中的语言环境
  • 设置本地上下文的语言环境

而此接口的父类接口为:

在它们的抽象子类实现中提供了默认的Locale对象(Locale是java.util包下提供了所有语言和国家地区映射关系的枚举对象)

在抽象子类AbstractLocaleContextResolver中,又新增了获取时区的方法,但是并没有提供解析语言环境和设置环境的具体实现

如果不额外配置LocaleResolver的bean,那么spring默认提供的是AcceptHeaderLocaleResolver类作为语言环境解析器

AcceptHeaderLocaleResolver类源码如下,可以看到提供了一个默认的Locale类型的变量和支持语言环境的集合supportedLocales

接下来看它重写的resolveLocale方法如何从http请求中解析语言环境

可以看到如果请求头中没有Accept-Language字段会用默认的语言环境,如果有,会从request对象中获取Locale对象,如果当前解析器不支持该类型的语言对象(即!supportedLocales.contains(requestLocale)),那么findSupportedLocale会在已经支持的Locale集合里和request.getLocales()里寻找到一对locale.getLanguage()相同的结果,并返回当前的Locale对象,这在两个集合直接相互查找的过程不同于常规的for循环遍历处理,可以参考:

需要注意的是,此对象不支持调用setLocale方法手动添加支持的Loclae

那么,现在的问题是,这个解析器什么时候负责解析请求中的语言环境,以及谁给它设置的Locale

同时,即便是不关心上面的问题,那么谁去负责读取国际化的资源配置,和提供对外的方法,它又怎么判断当前语言环境呢

在传统的spring mvc中,需要手动配置资源解析器MessageSource 的实例来完成对资源配置的读取:

或者说,使用@bean配置

那么知道由ReloadableResourceBundleMessageSource类负责读取配置文件后,又会有两个问题,就是它什么时候去读取资源文件,以及怎么识别不同语言地区的资源文件,所以可以看下ReloadableResourceBundleMessageSource类的源码会发现,并没有构造方法在此类实例化的时候去按照某种规则解析资源文件并存储,而只有在用户需要读取资源配置时才会去解析国家化资源文件:

在缓存不失效的情况下(getCacheMillis() > 0),会获取到basename的集合(这个是在它的父类AbstractResourceBasedMessageSource中维护的,由IOC容器构造这个实例的时候将配置的basename设置进去)

然后calculateAllFilenames方法负责根据用户指定的basename名称解析出绑定了具体语言、区域的资源配置文件:

这个this.cacheFilename是全局变量,保存了basename名称和对应的文件名

private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFilenames = new ConcurrentHashMap<>();

例如:{

message: {Locale.CHINESE: ['message.zh.ZN'],

        Locale.ENGLISH: ['message.en.US'],

       }

}

当根据basename找不到对应的Loadle映射的资源文件名时,calculateFilenamesForLocale方法会根据当前传入的Locale对象和basename解析出对应的资源文件名:

这个方法的意思是,计算给定 basename和Locale的资源文件名,附加语言代码、国家代码和变体代码。例如,basename为 "messages", Locale 为"de_AT_oo",那么对应的资源文件名列表就为 "messages_de_AT_OO", "messages_de_AT", "messages_de"。

所以我们在创建对应的语言环境资源文件时,必须按照这个规则去命名文件

解析出指定Locale对应的资源文件名后,会将这个映射关系保存到cachedFilenames中

再回到resolveCode方法,在获取了List<String>filenames后,getProperties方法会负责读取解析指定的filename,并返回一个PropertiesHolder对象,存放到全局变量cachedProperties中:

getProperties方法源码:

ReloadableResourceBundleMessageSource类支持定时缓存刷新功能,先默认根据filename从全局的cachedProperties获取PropertiesHolder对象,

然后判断当前时间与缓存的起始时间差是否超过配置的缓存时间,如果不超过则直接返回

相反,如果这些条件不满足,那么就会重新加载properties文件并解析成一个PropertiesHolder对象返回(refreshProperties()方法),同时对这个过程加锁

refreshProperties方法源码如下:

这个方法优先会去寻找文件名以.properties后缀结尾的文件,如果找不到,就会查找以文件名+.xml的文件

如果文件存在,它并不会直接load配置文件,而是先检查配置文件最后修改时间,因为存在缓存时间过期但是配置文件并没有被更新的情况,所以它这里会判断文件的最后修改时间是否等于原来PropertiesHolder类所保存的最后修改时间,如果相同则刷新一下PropertiesHolder的更新时间(即当前时间,后面要用来做时间差值判断是否缓存超时)

如果不相同,则才会去load配置文件,并返回一个Properties对象,再用这个properties对象构造成一个PropertiesHolder对象:

然后将PropertiesHolder对象和文件名的映射关系保存到全局变量cachedProperties中去:

这时候,读取资源配置文件就算加载完成了,而且仅仅是读取其中一种语言文件,当然同一个basename下的其它资源配置文件可能也会在遍历中被逐个加载(因为它只要找到codeLocale所对应的值就会return,剩下的就不会遍历了),然后释放锁

这时,回到resolveCode方法,在通过getProperties方法获取了PropertiesHolder对象后,直接getMessageFormat调用即可:

这里返回的是一个MessageFormat对象,所以再看一下PropertiesHolder的内部结构,它是作为ReloadableResourceBundleMessageSource的一个内部类存在:

可以看到PropertiesHolder的构造函数中并没有做别的事情,除此以外还有一个ConcurrentMap<String,Map<Locale,MessageFormat>>结构的map

然后重点看下getMessageFormat方法,因为国际化的功能都是通过此方法来获取资源值的:

这里就很清楚一目了然了,通过properties.getProperty获取到code对应的配置值msg,然后再将这个msg和Locale对象封装成一个MessageFormat对象result,又将result和locale再做一层映射关系保存到localeMap,再将这个localeMap和code做绑定存储到ConcurrentMap<String,Map<Locale,MessageFormat>>

这里可以梳理一下PropertiesHolder对象和资源配置文件的关系:

一个basename下的一种语言资源配置就会构造一个PropertiesHolder对象

以上就是ReloadableResourceBundleMessageSource类的源码解读,但是在spring boot中会自动装配一些bean,在

Org.springframework.boot:spring-boot-autoconfigure:2.1.1.RELEASE依赖下的METE-INF/spring.factoryies文件中配置了MessageSourceAutoConfiguration类的自动装配:

进而可以看下MessageSourceAutoConfiguration类:

可以看到messageSource方法配置了@Bean注解并返回了ResourceBundleMessageSource对象,所以在spring boot中资源文件是由ResourceBundleMessageSource类去解析的,ResourceBundleMessageSource和前面讲的ReloadableResourceBundleMessageSource类大同小异,这里就不解读了

这里需要注意的是,如果在自定义的@Configuration类中配置了MessageSource的bean,那么将会覆盖spring boot自动装配所提供的bean配置

如果使用spring boot做国际化,还需要注意的是,basename的名称应该放在applicatioin.properties中指定,同时为了防止乱码可以指定资源的编码格式:

知道了spring是如何调用资源解析器MessageSource解析国际化资源文件后,这里再回到之前的问题,谁负责在什么时候去调LocaleResolver.resolveLocale()方法解析语言环境,解析后的语言环境又会存放到哪里?

查看DispatcherServlet类的源码会发现,在它的初始化方法中,会拿到语言环境解析器的bean:

initLocaleResolver方法如下:

可以看到会从IOC容器获取解析器对象,如果找不到,会取一个默认的解析器,也就是AcceptHeaderLocaleResolver

DispatcherServlet类继承了父类FrameworkServlet,在父类实现中,完成了上下文和环境等一系列的初始化工作

同时重写了HTTP请求的八大方法,让请求调用统一的前置处理器,再进入到子类DispatcherServlet的doService中:

前置处理器processRequest如下:

在processRequest中可以看到,先是通过LocaleContextHolder获取了LocaleContext对象(其实就是内部维护了一个ThreadLocal<LocaleContext>)

然后通过buildLocaleContext方法获取Locale(已被子类Dispatcher重写)

在buildLocaleContext中可以看到从DispatcherServlet初始化时拿到的LocaleResolver去调用了resolveLocale方法获取Locale对象,如果没有的话就直接取HttpServletRequest中的Locale返回,然后this.initContextHolders方法将解析后的Locale对象设值到LocaleContextHolder中:

通常语言环境解析器会配合LocaleChangeInterceptor一起使用,LocaleChangeInterceptor是i18n包下提供的拦截器,可以通过setParamName方法设置该拦截器的拦截参数名,它的preHandle方法会从请求中获取设置的拦截参数值,并设置到LocaleResolver里去:

所以,可以通过LocaleChangeInterceptor实现请求传参数来改变语言环境的功能

需要特别注意的是,LocaleChangeInterceptor拦截器不能和AcceptHeaderLocaleResolver一起使用,因为在LocaleChangeInterceptor拦截器中会调用语言环境解析器的setLocale方法,而AcceptHeaderLocaleResolver的setLocale实现是抛了一个异常,会导致请求报错:

ERROR 11408 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.UnsupportedOperationException: Cannot change HTTP accept header - use a different locale resolution strategy] with root cause

 

java.lang.UnsupportedOperationException: Cannot change HTTP accept header - use a different locale resolution strategy

at org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver.setLocale(AcceptHeaderLocaleResolver.java:140) ~[spring-webmvc-5.1.3.RELEASE.jar:5.1.3.RELEASE]

at org.springframework.web.servlet.i18n.LocaleChangeInterceptor.preHandle(LocaleChangeInterceptor.java:153) ~[spring-webmvc-5.1.3.RELEASE.jar:5.1.3.RELEASE]

at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:136) ~[spring-webmvc-5.1.3.RELEASE.jar:5.1.3.RELEASE]

at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1033) ~[spring-webmvc-5.1.3.RELEASE.jar:5.1.3.RELEASE]

at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.3.RELEASE.jar:5.1.3.RELEASE]

at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005) ~[spring-webmvc-5.1.3.RELEASE.jar:5.1.3.RELEASE]

at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897) ~[spring-webmvc-5.1.3.RELEASE.jar:5.1.3.RELEASE]

at javax.servlet.http.HttpServlet.service(HttpServlet.java:634) ~[tomcat-embed-core-9.0.13.jar:9.0.13]

查看异常日志可以发现错误就是在LocaleChangeInterceptor类的preHandle调用导致的

所以,如果要实现通过请求参数动态改变语言环境,必须用LocaleChangeInterceptor搭配SessionLocaleResolver或者CookieLocaleResolver使用

通常拦截器搭配SessionLocaleResolver解析器一起使用,这需要我们在配置类中注册成一个bean:

而看SessionLocaleResolver的源码会发现,它的resolveLocale实现是优先从session中获取Locale,如果取不到则会取默认值,最后才从请求中获取:

determineDefaultLocale方法为:

至于setLocaleContext方法,则会从LocaleContext获取Locale对象,同时和时区信息一起保存到session

也就是当请求第一次进来时:

1、DispatcherServlet的前置处理器会将请求交给SessionLocaleResolver解析,但此时session并没有保存请求对应的Locale,所以取了默认值并被设置到LocaleContextHolder中

2、执行doService,通过HandlerAdapter分发请求

3、拦截器前置处理,将LocaleContext的Locale保存到session

4、处理器通过MessageSource.getMessage(code,null,LocaleContextHolder.getLocale())获取语言环境配置

当下一次请求进来时,DispatcherServlet的前置处理器就会通过SessionLocaleResolver直接从session拿到Locale对象了,并设置到LocaleContextHolder

除此之外i18n包还提供了CookieLocaleResolver和FixedLocaleResolver 两种解析器,CookieLocaleResolver和SessionLocaleResolver类似,只是基于Cookie对象来获取和保存Locale,如果cookie不存在,则从请求头解析;FixedLocaleResolver则是一直使用固定的Local,同样改变Local 是不支持的 

总结,AcceptHeaderLocaleResolver和FixedLocaleResolver解析器解析语言环境是基于请求头来判断,而SessionLocaleResolver和CookieLocaleResolver是优先基于请求参数来解析语言环境,兜底方案才是请求头解析

后面再写怎么拓展自定义的国际化资源,比如支持数据库和枚举类的等等

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值