spring学习4-国际化

国际化

国际化也称作i18n,其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数。由于软件发行可能面向多个国家,对于不同国家的用户,软件显示不同语言的过程就是国际化(举个例子,人们玩的电子游戏,通常可以选择多个语言版本,适应于多个国家的玩家)。通常来讲,软件中的国际化是通过配置文件来实现的,假设某个软件要支撑两种语言,那么就需要两个版本的配置文件。

Java国际化

Java自身是支持国际化的,java.util.Locale用于指定当前用户所属的语言环境等信息,java.util.ResourceBundle用于查找绑定对应的资源文件。
Locale包含了language信息和country信息,Locale创建默认locale对象时使用的静态方法:

    /**
     * This method must be called only for creating the Locale.*
     * constants due to making shortcuts.
     */
    private static Locale createConstant(String lang, String country) {
        BaseLocale base = BaseLocale.createInstance(lang, country);
        return getInstance(base, null);
    }

配置文件命名规则:
basename_language_country.properties
必须遵循以上的命名规则,java才会识别。其中,basename是必须的,语言和国家是可选的。这里存在一个优先级概念,如果同时提供了messages.properties和messages_zh_CN.propertes两个配置文件,如果提供的locale符合en_CN,那么优先查找messages_en_CN.propertes配置文件,如果没查找到,再查找messages.properties配置文件。最后,提示下,所有的配置文件必须放在classpath中,一般放在resource目录下。
举个例子,两个配置文件内容分别如下:

#messages.properties
test=hello1
#messages_en_CN.propertes
test=hello2

代码:

//ResourceBundle.getBundle接受两个参数:basename,locale
System.out.println(ResourceBundle.getBundle("messages",new Locale("en","CN")).getString("test"));

打印结果:

hello2

以下命名的配置文件优先级从低到高:

messages.properties
messages_en.properties
messages_en_CN.properties

通过配置不同的Locale相关的资源文件,我们可以通过key值取到相应环境的value值,这样便做到了国际化,这种方式是非侵入式的,让我们编写代码时可以不需要考虑国际化的问题,只需要根据配置规则配置相关资源文件,在实际读取资源配置时,指定相应的locale即可。这也是约定优于配置的体现。

Spring国际化

spring使用MessageSource接口实现国际化。两个实现类为:
ResourceBundleMessageSource:基于java的ResourceBundle实现了国际化,配置文件必须放在classpath下。
ReloadableResourceBundleMessageSource:直接使用读取文件的方式实现国际化,规则跟java的相同,支持动态修改后刷新配置,避免在业务不能中断的情况下重启进程。配置文件可以放在任意目录下,指定目录后,该类会去指定目录中加载配置文件。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.stereotype.Component;

import java.util.Locale;

@Component
public class MessageService {

    @Autowired
    private MessageSource messageSource;

    private Locale currentLocale = new Locale("en");

    @Bean
    public static MessageSource getMessageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages");
        messageSource.setCacheSeconds(5);
        return messageSource;
    }

    public String getMessage(String key) {
        return messageSource.getMessage(key, null, key, currentLocale);
    }
}

ReloadableResourceBundleMessageSource的优点在于可以不重启进程动态刷新配置,以及指定资源目录,不需要强制放在classpath下面,如果需要放在classpath中,那么只需要在basename中的资源路径上添加"classpath:",便可以在classpath中查找配置。messageSource.setCacheSeconds用于设置配置的过期时间,单位为秒,messageSource.setCacheSeconds(5)代表每5秒配置文件就会过期,再重新查询时,就会重新加载配置。
spring的默认配置文件命名规则跟java是相同的,也都遵循约定优于配置的思路。

Locale信息的获取

一般来说,典型的B/S架构,用户可能在任何地点使用任何语言登陆网站,但是网站后台是部署在固定的服务器或者是云服务上的,那么如何让后端获取客户端(浏览器端)的locale信息,从而做到针对不同的客户端进行国际化呢?spring使用LocaleResolver解析用户的http请求来获取对应的locale信息。下面的代码是典型的controller,通过解析HttpServletRequest请求来获取locale信息。

@Controller
public class HomeController extends BaseController {

  @RequestMapping(value = "", method = GET)
  public String homeDefault() {
    return homePageUrl();
  }

  @RequestMapping(value = "/locales", method = GET)
  public ResponseEntity<OpenLmisResponse> getLocales(HttpServletRequest request) {
    messageService.setCurrentLocale(RequestContextUtils.getLocale(request));
    return response("locales", messageService.getLocales());
  }

  @RequestMapping(value = "/changeLocale", method = PUT, headers = ACCEPT_JSON)
  public void changeLocale(HttpServletRequest request) {
    messageService.setCurrentLocale(RequestContextUtils.getLocale(request));
  }
}

RequestContextUtils.getLocale(request)实现如下。RequestContextUtils是spring提供的工具类。可以看出LocaleResolver从设计上支持多种策略。

    /**
     * Retrieve the current locale from the given request, using the
     * LocaleResolver bound to the request by the DispatcherServlet
     * (if available), falling back to the request's accept-header Locale.
     * <p>This method serves as a straightforward alternative to the standard
     * Servlet {@link javax.servlet.http.HttpServletRequest#getLocale()} method,
     * falling back to the latter if no more specific locale has been found.
     * <p>Consider using {@link org.springframework.context.i18n.LocaleContextHolder#getLocale()}
     * which will normally be populated with the same Locale.
     * @param request current HTTP request
     * @return the current locale for the given request, either from the
     * LocaleResolver or from the plain request itself
     * @see #getLocaleResolver
     * @see org.springframework.context.i18n.LocaleContextHolder#getLocale()
     */
    public static Locale getLocale(HttpServletRequest request) {
        LocaleResolver localeResolver = getLocaleResolver(request);
        return (localeResolver != null ? localeResolver.resolveLocale(request) : request.getLocale());
    }

LocaleResolver包含以下四种策略实现。AcceptHeaderLocaleResolver,CookieLocaleResolver,FixedLocaleResolver和SessionLocaleResolver。


LocaleResolver实现.png

下面的代码是DispatcherServlet中初始化LocaleResolver的过程,可以看到LocaleResolver实际是支持用户进行配置的,如果没有配置,那么使用getDefaultStrategy方法获取默认策略。

    /**
     * Initialize the LocaleResolver used by this class.
     * <p>If no bean is defined with the given name in the BeanFactory for this namespace,
     * we default to AcceptHeaderLocaleResolver.
     */
    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 + "]");
            }
        }
    }

查看项目工程代码,发现项目中配置的是CookieLocaleResolver。


LocaleResolver配置.png

几种LocaleResolver的用法不同,不过从名字便可以看出一二。


accept-language.png

AcceptHeaderLocaleResolver:直接从Http请求的Header中通过accept-language获取locale信息
CookieLocaleResolver:从cookie中获取,如果获取不到,也是通过accept-language获取locale信息
SessionLocaleResolver:从session中获取,如果获取不到,也是通过accept-language获取locale信息
FixedLocaleResolver:固定locale,基本没啥用

至此,整个后端的国际化过程已经比较清楚:后端通过解析客户端(浏览器端)传递的locale信息,将locale拿出,然后再通过spring的MessageSource类传入locale信息,取出相应的配置文件,解析出相应配置,从而便完成了国际化。

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