本文对应源码地址: https://github.com/nieandsun/NRSC-STUDY/tree/master/i18n-study
1 引子
1.1 国际化简单概述
作为一个服务端开发人员,这里我想先站在自己的角度对国际化(internationalization,因在i和n之间共有18个字母,所以国际化也称为i18n)所要做的事做一个简单的概述:
国际化在实际项目中所要承担的职责是按照客户指定的语言让服务端返回相应语言的内容。
1.2 spring/springboot工程中国际化玩法概述
在spring/springboot的世界里,国际化的玩法是基于如下接口的: org.springframework.context.MessageSource
。 在该接口里主要定义了如下三个方法:
public ?interface ?MessageSource ? { ???? ????@Nullable ????String?getMessage (String?var1,?@Nullable?Object[]?var2,?@Nullable?String?var3,?Locale?var4) ; ? ????String?getMessage (String?var1,?@Nullable?Object[]?var2,?Locale?var3) ?throws ?NoSuchMessageException ; ? ????String?getMessage (MessageSourceResolvable?var1,?Locale?var2) ?throws ?NoSuchMessageException ; }
该接口比较重要的三个实现类如下:
ResourceBundleMessageSource
ReloadableResourceBundleMessageSource
它们与MessageSource间的继承关系如下: 接下来将对这个三个类的玩法进行具体地介绍。
2 ResourceBundleMessageSource的玩法(默认)
首先来看一下在springboot项目中国际化最基础的玩法:
(1)搭建一个springboot项目(至少添加web依赖)。
(2)创建国际化配置文件,如下图所示:
messages.properties(默认配置)
?user.name=yoyo
messages_en_US.properties
user.name=yoyo-EN user.name1=nrsc user.name2=nrsc{0 }-{1 }
messages_zh_CN.properties
user.name1=章尔 user.name2=章尔{0 }-{1 }
(3)创建controller测试类
@RestController public ?class ?I18nDemoController ? { ????@Autowired ????private ?MessageSource?messageSource; ????@GetMapping ("/hello" ) ????public ?String?hello () ? { ????????String?defaultM?=?messageSource ????????????????.getMessage("user.name" ,?null ,?LocaleContextHolder.getLocale()); ????????String?message1?=?messageSource ????????????????.getMessage("user.name1" ,?null ,?LocaleContextHolder.getLocale()); ????????String?message2?=?messageSource ????????????????.getMessage("user.name2" ,?new ?String[]{"WW" ,?"MM" },?LocaleContextHolder.getLocale()); ????????String?message3?=?messageSource ????????????????.getMessage("user.nameXX" ,?null ,?"defaultName" ,?LocaleContextHolder.getLocale()); ????????return ?defaultM?+?"<->" ?+?message1?+?"--" ?+?message2?+?"##" ?+?message3; ????} }
tips:
① 直接可以从spring容器里通过@Autowired拿到MessageSource的原因是如果没有配置该类型的bean时,spring容器会默认初始化一个该类型的bean-ResourceBundleMessageSource
放到spring容器里。答案在如下源码文件中: org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
。
②LocaleContextHolder.getLocale()可以拿到本次请求header里的Accept-Language对应的语言环境 。
(4)简单测试一种场景,结果如下(其他场景留给读者):
3 ReloadableResourceBundleMessageSource的玩法
ReloadableResourceBundleMessageSource
相比于ResourceBundleMessageSource
而言最重要的区别在于
前者可以读取.properties和.xml结尾的国际化映射文件,后者只可以读取.properties结尾的文件
前者可以指定映射文件在内存中缓存的时间,后者不可以
ReloadableResourceBundleMessageSource的具体玩法如下:
(1)创建配置化文件,如下图:
(2)创建ReloadableResourceBundleMessageSource,将其注入到spring容器,代码如下:
@Bean ("reloadableResourceBundleMessageSource" )public ?MessageSource?initReloadableResourceBundleMessageSource () ? { ????ReloadableResourceBundleMessageSource?messageSource?=?new ?ReloadableResourceBundleMessageSource(); ???? ????messageSource.setBasename(ResourceUtils.CLASSPATH_URL_PREFIX?+?"i18n/messages" ); ???? ????messageSource.setDefaultEncoding("UTF-8" ); ???? ????messageSource.setCacheSeconds(60 ); ????return ?messageSource; }
(3)指定注入的MessageSource为ReloadableResourceBundleMessageSource
@Autowired @Qualifier ("reloadableResourceBundleMessageSource" )private ?MessageSource?messageSource;
(4)测试留给读者
4 StaticMessageSource的玩法
ReloadableResourceBundleMessageSource
和ResourceBundleMessageSource
都是基于本地文件的,而StaticMessageSource就比较简单了,它的基本玩法如下:
(1)创建StaticMessageSource
,同时指定国际化映射内容,然后将其放入spring容器,代码如下:
@Bean ("staticMessageSource" )public ?MessageSource?initStaticMessageSource () ? { ????StaticMessageSource?messageSource?=?new ?StaticMessageSource(); ????messageSource.addMessage("user.name" ,?Locale.US,?"yoyo-EN" ); ????messageSource.addMessage("user.name1" ,?Locale.US,?"nrsc" ); ????messageSource.addMessage("user.name2" ,?Locale.US,?"nrsc{0}-{1}" ); ????messageSource.addMessage("user.name" ,?Locale.CHINA,?"章尔" ); ????messageSource.addMessage("user.name1" ,?Locale.CHINA,?"章尔1" ); ????messageSource.addMessage("user.name2" ,?Locale.CHINA,?"章尔{0}-{1}" ); ????return ?messageSource; }
(2)指定注入的MessageSource为StaticMessageSource
@Autowired @Qualifier ("staticMessageSource" )private ?MessageSource?messageSource;
(3)测试留给读者
5 DIY
5.1 WHY DIY?
首先假设你的项目里国际化实现方案上有如下两个技术需求:
需要进行翻译的内容较多,要进行结构化的存储与管理(比如存放到mysql数据库)
这时你会发现, spring/springboot提供的上面三种玩法貌似都不得行,这时我们就要考虑DIY了。
5.2 从StaticMessageSource源码找寻DIY灵感
依我的经验来看,要进行DIY最好的途径是站在源码的基础上进行模仿和改造。而上面介绍的三种玩法中,StaticMessageSource对应的玩法应该是最简单,也是最好入手的,这里简单撸一下它的源码:
public ?class ?StaticMessageSource ?extends ?AbstractMessageSource ? { ? ? ?private ?final ?Map<String,?String>?messages?=?new ?HashMap<>(); ? ? ?private ?final ?Map<String,?MessageFormat>?cachedMessageFormats?=?new ?HashMap<>(); ? ?@Override ?protected ?String?resolveCodeWithoutArguments (String?code,?Locale?locale) ? { ??return ?this .messages.get(code?+?'_' ?+?locale.toString()); ?} ? ? ?@Override ?@Nullable ?protected ?MessageFormat?resolveCode (String?code,?Locale?locale) ? { ??String?key?=?code?+?'_' ?+?locale.toString(); ??String?msg?=?this .messages.get(key); ??if ?(msg?==?null )?{ ???return ?null ; ??} ?? ?? ?? ??synchronized ?(this .cachedMessageFormats)?{ ???MessageFormat?messageFormat?=?this .cachedMessageFormats.get(key); ???if ?(messageFormat?==?null )?{ ????messageFormat?=?createMessageFormat(msg,?locale); ????this .cachedMessageFormats.put(key,?messageFormat); ???} ???return ?messageFormat; ??} ?} ? ? ?? ?public ?void ?addMessage (String?code,?Locale?locale,?String?msg) ? { ??Assert.notNull(code,?"Code?must?not?be?null" ); ??Assert.notNull(locale,?"Locale?must?not?be?null" ); ??Assert.notNull(msg,?"Message?must?not?be?null" ); ??this .messages.put(code?+?'_' ?+?locale.toString(),?msg); ??if ?(logger.isDebugEnabled())?{ ???logger.debug("Added?message?[" ?+?msg?+?"]?for?code?[" ?+?code?+?"]?and?Locale?[" ?+?locale?+?"]" ); ??} ?} ? ?? ?public ?void ?addMessages (Map<String,?String>?messages,?Locale?locale) ? { ??Assert.notNull(messages,?"Messages?Map?must?not?be?null" ); ??messages.forEach((code,?msg)?->?addMessage(code,?locale,?msg)); ?} ?@Override ?public ?String?toString () ? { ??return ?getClass().getName()?+?":?" ?+?this .messages; ?} }
从上面的源码来看,其实非常简单。
5.3 DO DIY -- 借助redis进行缓存
这里给出一个简单的借助redis进行缓存的DIY方案 (1)向redis里存储数据
@Autowired private ?RedisTemplate<String,?Object>?redisTemplate;@Test public ?void ?initData () ? { ????List<MessageInfo>?messageInfos?=?Arrays.asList( ????????????new ?MessageInfo("user.name" ,?Locale.US.toString(),?"yoyo-EN" ), ????????????new ?MessageInfo("user.name1" ,?Locale.US.toString(),?"nrsc" ), ????????????new ?MessageInfo("user.name2" ,?Locale.US.toString(),?"nrsc{0}-{1}" ), ????????????new ?MessageInfo("user.name" ,?Locale.CHINA.toString(),?"章尔" ), ????????????new ?MessageInfo("user.name1" ,?Locale.CHINA.toString(),?"章尔1" ), ????????????new ?MessageInfo("user.name2" ,?Locale.CHINA.toString(),?"章尔{0}-{1}" ) ????); ????redisTemplate.opsForValue().set("userInfo" ,?messageInfos); }
(2)仿照StaticMessageSource自定义MessageSource
@Component ("myMessageSource" )public ?class ?MyMessageSource ?extends ?AbstractMessageSource ? { ????private ?final ?Map<String,?MessageFormat>?cachedMessageFormats?=?new ?HashMap<>(); ????@Autowired ????private ?RedisTemplate<String,?Object>?redisTemplate; ????@Override ????protected ?String?resolveCodeWithoutArguments (String?code,?Locale?locale) ? { ????????Map<String,?String>?map?=?getMessagesMap(); ????????return ?map.get(code?+?'_' ?+?locale.toString()); ????} ????private ?Map<String,?String>?getMessagesMap () ? { ????????Object?userInfoList?=?redisTemplate.opsForValue().get("userInfo" ); ????????List<MessageInfo>?messageInfoList?=?(List<MessageInfo>)?userInfoList; ????????Map<String,?String>?map?=?new ?HashMap<>(); ????????for ?(MessageInfo?messageInfo?:?messageInfoList)?{ ????????????String?key?=?messageInfo.getCode()?+?'_' ?+?messageInfo.getLocale(); ????????????map.computeIfAbsent(key,?k?->?messageInfo.getMessage()); ????????} ????????return ?map; ????} ????@Override ????@Nullable ????protected ?MessageFormat?resolveCode (String?code,?Locale?locale) ? { ????????String?key?=?code?+?'_' ?+?locale.toString(); ????????String?msg?=?getMessagesMap().get(key); ????????if ?(msg?==?null )?{ ????????????return ?null ; ????????} ????????synchronized ?(this .cachedMessageFormats)?{ ????????????MessageFormat?messageFormat?=?this .cachedMessageFormats.get(key); ????????????if ?(messageFormat?==?null )?{ ????????????????messageFormat?=?createMessageFormat(msg,?locale); ????????????????this .cachedMessageFormats.put(key,?messageFormat); ????????????} ????????????return ?messageFormat; ????????} ????} }
(3)指定注入的MessageSource为我自定义的MyMessageSource
@Autowired @Qualifier ("myMessageSource" )private ?MessageSource?messageSource;
(4)测试留给读者