网站国际化实现(2)—Spring MVC国际化实现及原理

一、背景

很多网站的用户分布在世界各地,因此网站需要针对不同国家的用户展示不同语言的内容,因此就有了国际化实现的需求,大多数网站都会在网站的头部或尾部设置语言切换链接,这样就可以直接切换成相应的内容。其中有些网站是通过网站地址或参数进行区分,有些是通过设置cookie值进行进行区分。

二、解决思路

前面已经写过一篇JDK的国际化支持,讲解了JDK实现国际化的具体实现。那么网站的国际化实现具体如何做呢?

其实网站的国际化实现与前面介绍的JDK实现思路类似,只是本地化信息的获取需要从页面得到而已。得到了页面信息,再获取对应的数据并进行格式化处理,最后渲染到页面即可。这里主要说明后端的处理思路,前端的处理思路其实也类似,只是实现方式有区别而已。

那如何从页面获取本地化信息呢?这个是所有处理的首要环节,常用的几种方式有:
(1)直接根据Request.getLocale()方法得到本地化信息,实际就是从Http Request Headers里面取“accept-language”对应的值,该值拥有浏览器端的语言信息;
(2)在浏览器端保存一个自定义名字的cookie,默认情况下指定一个值,对应的切换通过语言切换链接的点击修改对应的值;
(3)在请求URL上面添加带本地化信息的参数或者地址里面包含本地化信息。

通过上面几种方式,在web程序中就可以直接从request中得到了本地化信息,然后根据本地化信息从相应的properties文件中获取数据(比如可以通过JDK的ResourceBundle类),得到数据后如果需要的化再对数据进行格式化处理(比如可以通过JDK的MessageFormat类),最后将处理过的数据展示到前台即完成了整个国际化操作。

思路已经有了,那么具体如何实现呢?下面以Spring MVC的实现为例,因为该框架做了很好的抽象和封装,是个非常好的参考例子。

三、Spring MVC实现及原理

3.1 本地化信息获取

3.1.1 概述

Spring MVC的DispatcherServlet类会在initLocaleResolver方法中查找一个locale resolver,如果没有找到就会用默认的AcceptHeaderLocaleResolver类。locale resolver会去根据请求Request设置当前的locale信息。

除了resolver类,还可以定义拦截器去设置locale信息,比如通过请求参数去设置,具体下面细讲。

Spring MVC相关的处理类都在org.springframework.web.servlet.i18n包下。而本地化信息的获取可以通过RequestContext.getLocale()方法得到。另外,RequestContext.getTimeZone()方法还可以得到时区信息。

3.1.2 AcceptHeaderLocaleResolver

这个从名字也能看出大概来,这个类是解析request的header中的accept-language值,这个值通常包含客户端支持的本地化信息,所以通过这个值可以获取本地化信息。不过这个类拿不到时区信息。这个类是默认配置的,所以使用的话不用额外配置。

3.1.3 CookieLocaleResolver

这个类是通过cookie去存取本地化信息,客户端可以在cookie中存储一个指定名字的值代表本地化信息,然后这个类获取后做相应的解析即可。具体的配置如下:
<code><bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<property name="cookieMaxAge" value="100000"/>
<property name="cookiePath" value="/"/>
</bean></code>
这里对几个配置的属性做下说明:

属性 默认值 说明
cookieName classname + LOCALE cookie名字
cookieMaxAge Servlet容器默认值 这个值为cookie在客户端保留的时间,如果值为-1,则不保留;这个值会在关闭浏览器后无效。
cookiePath / 这个值设置cookie的适用路径,如果这个值设置了,那么就表示cookie只对当前目录及其子目录可见。

3.1.4 SessionLocaleResolver

这个类是通过request获取本地化信息的,然后存在HttpSession中,所以本地化信息存取依赖于session的生命周期。具体配置如下:
<code><bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
</bean></code>

3.1.5 LocaleChangeInterceptor

这个拦截器会拦截请求中的参数,然后根据参数去调用LocaleResolver的setLocale()方法,改变当前的locale值。下面举个例子,有这个地址http://www.sf.net/home.view?siteLanguage=nl,参数siteLanguage代表locale信息,配置拦截修改locale值:
<code><bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/></bean>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/></code>
这里配置CookieLocaleResolver是因为LocaleChangeInterceptor需要调用LocaleResolver的setLocale()方法,这个例子里面用到了CookieLocaleResolver,当然也可以用其他的LocaleResolver实现类。

3.2 数据获取与格式化

Spring MVC的数据处理定义了一个接口MessageSource,该接口定义了数据获取的方法。方法如下:

  1. String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
    这里code为属性文件中的key值,args是文件中需要替换的参数值,defaultMessage是找不到内容时的默认内容,locale为本地化信息。
  2. String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
    这个方法与上面的方法类似,只是没有了默认内容,而是找不到内容时抛出异常。
  3. String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
    这里参数中有个新接口MessageSourceResolvable,对前面的参数进行了封装,locale为本地化信息。

对于MessageSource接口,Spring MVC的ApplicationContext和HierarchicalMessageSource都有继承,ApplicationContext在加载的时候,它会先去上下文里面查找bean名为messageSource的实现,找到后上面MessageSource方法的调用就用这个实现类; 如果找不到就会找包含MessageSource bean的类去使用; 再找不到就用DelegatingMessageSource去执行方法调用了。

MessageSource常见的实现主要有如下几个:

  1. ResourceBundleMessageSource类:这个类实际是依赖的JDK的ResourceBundle类获取数据、MessageFormat去做格式化。
  2. ReloadableResourceBundleMessageSource类:这个与上面的比较就多了可重新加载,即可以在不重新启动应用的情况下重新读取新的内容。具体实现方式也有区别,这个类是通过Spring的PropertiesPersister策略加载,依赖的是JDK的Properties类读取内容。
  3. StaticMessageSource类:这个类提供了简单的实现,内容是需要先配置好的。使用比较少,适合在内容较少较简单情况下使用。

下面以最常用的ResourceBundleMessageSource类做个简单示例:
<code><bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>format</value>
<value>exceptions</value>
</list>
</property>
</bean></code>
format与exceptions为文件基础名,对应内容配置在具体locale相应的文件名中即可。详细示例见下面部分。

3.3 小结与示例

上面两节主要讲了本地化信息获取,数据获取与格式化两部分,这两部分其实也是整个国际化过程最核心的两个部分,至于请求的匹配与接收,返回结果的页面渲染这个就不展开讲,与国际化不直接相关,属于Spring MVC的基础内容。

这里对整个Spring MVC的国际化过程做个大概的梳理,整个过程大概是这样:接收请求——>LocaleResolver获取/设置locale信息——>MessageSource获取数据并格式化——>内容展示到页面。

讲了半天,还是有点抽象,下面直接来个详细示例:

<code>@Controller
public class I18nController {
@Autowired
private MessageSource messageSource;
@RequestMapping("i18n")
public String i18n(Model model){
//获取本地化信息,从LocaleContext中得到
Locale locale = LocaleContextHolder.getLocale();
//初始化参数,这里简便演示,真实参数可能是从数据库查询处理的。这里的参数是与i18n目录下的配置文件需要替换的内容对应的
Object [] objArr = new Object[4];
objArr[0] = new Date();
objArr[1] = messageSource.getMessage("goods", null, locale);//这个具体商品从配置中读取
objArr[2] = "taobao";
objArr[3] = new BigDecimal("39.20");
//获取格式化后的内容
String content = messageSource.getMessage("template", objArr, locale);
model.addAttribute("content", content);
return "/i18n/show";
}
}
</code>

LocaleResolver配置,这里以Cookie为例:
<code><bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<property name="cookieMaxAge" value="100000"/>
<property name="cookiePath" value="/"/>
</bean></code>

MessageSource配置,这里以ResourceBundleMessageSource为例:
<code> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>/i18n/message</value>
</list>
</property>
</bean></code>

Properties配置,这里统一放在/i18n目录下,message名字开头:

文件配置.png

更详细的代码可以查看我的Github项目

四、拓展介绍

4.1 LocaleResolver对应Bean是如何初始化的?

初始化工作是在DispatcherServlet类初始化时调用initLocaleResolver方法执行的。
<code>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 + "]");
}
}
}</code>
从代码里面可以看到处理化过程分为两步:(1)先从当前上下文环境中取名字为localeResolver的bean; (2)如果找不到就根据默认策略去取LocaleResolver这个Class名字的bean,即执行getDefaultStrategy方法,该方法实际是取DispatcherServlet.properties文件中的org.springframework.web.servlet.LocaleResolver对应的值,即默认的AcceptHeaderLocaleResolver类,再创建对应的bean。
所以如果上下文中自定义了LocaleResolver就用自定义的,没有定义会用默认的AcceptHeaderLocaleResolver类。这种写法在写公共逻辑且提供多种策略时很实用。

五、参考资料

  1. Spring MVC using locales官方教程
  2. Spring MVC using MessageSource官方教程
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,773评论 6 342
  • 儿子,当你来到这个世上,发出第一声啼哭,我是多么激动,我有了一个兄弟!是的,我当时就是这么想的。那天,我就在想,怎...
    泰山月色阅读 256评论 0 0
  • 四万年前已经产生了智人,之前是没有人这的。那人这种与其他动物不同的动物是如何产生的呢?在千万里的环境变化中,各种动...
    我是小代同学阅读 330评论 0 2
  • 原标题:丽江游玩指南 改写标题: 1、如何用3000元玩遍丽江 2、你去丽江邂逅了吗? 3、别天天待家里了,看看外...
    凌霄霄阅读 291评论 1 0