本文项目已发布到github,后续学习项目也会添加到此工程下,欢迎fork点赞。https://github.com/wangyuheng/spring-boot-sample 
国际化 
简单来说,国际化就是让应用(app、web)适应不同的语言和地区的需要,比如根据地区选择页面展示语言。 
 
i18n =internationalization,首末字符i和n,18为中间的字符数
原理 基于传入语言or地区 标识进行判断,输出不同内容。伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 func hello (var  lang)   {    if  (lang == "にほんご" ) {         return  "おはよう" ;     } else  if  (lang == "English" ) {         return  "hello" ;     } else  {         return  “你好”     }      } 
原理简单,但是如何优雅的实现?spring是否已经提供了现成的轮子?答案是肯定的。基于原理可以认为,实现国际化主要分为2部分
输入语言or地区 标识 
输出不同语言or地区的内容文案 
 
输出 通过MessageSource实现不同语言输出。
在spring初始化refresh过程中,会初始化MessageSource。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public  abstract  class  AbstractApplicationContext  extends  DefaultResourceLoader 		implements  ConfigurableApplicationContext , DisposableBean   {...		 		@Override  	public  void  refresh ()  throws  BeansException, IllegalStateException  		synchronized  (this .startupShutdownMonitor) { 			 			prepareRefresh(); 			 			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); 			 			prepareBeanFactory(beanFactory); 			try  { 				 				postProcessBeanFactory(beanFactory); 				 				invokeBeanFactoryPostProcessors(beanFactory); 				 				registerBeanPostProcessors(beanFactory); 				 				initMessageSource(); 				 				initApplicationEventMulticaster(); 				 				onRefresh(); 				 				registerListeners(); 				 				finishBeanFactoryInitialization(beanFactory); 				 				finishRefresh(); 			} ...	 } 
而springboot会初始化ResourceBundleMessageSource实例作为MessageSource的默认实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Configuration @ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Conditional(ResourceBundleCondition.class) @EnableConfigurationProperties @ConfigurationProperties(prefix = "spring.messages") public  class  MessageSourceAutoConfiguration  ... 	@Bean  	public  MessageSource messageSource ()   		ResourceBundleMessageSource messageSource = new  ResourceBundleMessageSource(); 		if  (StringUtils.hasText(this .basename)) { 			messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray( 					StringUtils.trimAllWhitespace(this .basename))); 		} 		if  (this .encoding != null ) { 			messageSource.setDefaultEncoding(this .encoding.name()); 		} 		messageSource.setFallbackToSystemLocale(this .fallbackToSystemLocale); 		messageSource.setCacheSeconds(this .cacheSeconds); 		messageSource.setAlwaysUseMessageFormat(this .alwaysUseMessageFormat); 		return  messageSource; 	} ... } 
MessageSource内部则通过basename以及Locale定位到具体Resource Bundle文件,并基于code**(properties key)**读取对应的显示文本。
其中涉及到的三个概念
basename标识 
Locale 
Resource Bundle 
 
basename 用于指定Resource Bundle文件位置,可以通过配置文件配置, 默认为messages 
1 spring.messages.basename =messages 
Locale Locale对象代表具体的地理,政治或文化地区,用来指定语言及地区。构造函数如下
1 2 3 4 5 Locale(String language) Locale(String language, String country) Locale(String language, String country, String variant) 
variant 变体值,用于指示变化的任意值Locale。
1 2 3 4 5 6 7 static  public  final  Locale ENGLISH = createConstant("en" , "" );static  public  final  Locale CHINESE = createConstant("zh" , "" );static  public  final  Locale SIMPLIFIED_CHINESE = createConstant("zh" , "CN" );static  public  final  Locale TRADITIONAL_CHINESE = createConstant("zh" , "TW" );
Resource Bundle ResourceBundle类和Properties类似,都可以读取程序内的文件,不过ResourceBundle更强大,提供了诸如缓存、Locale区分一类的操作。
所以MessageSource 其实是对ResourceBundle的一种封装增加,优化了使用,并且托管与spring容器生命周期。这时就有一个很重要的选择:
如果通过静态类封装了restful的接口返回,可以自己扩展ResourceBundle类,而不是将MessageSource的spring实例放置在静态类中。 
而idea中提供了Resource Bundle资源束的支持,方便用户添加管理国际化文案。
输入 看完输出的形式,可以知道,我们只需要确认Locale就可以实现国际化。所以我们再找一下Locale的轮子。
accept-language servlet自带轮子,基于http协议,即通过header中的accept-language 报文头,实现Locale的自动识别。
代码见org.apache.catalina.connector.Request
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public  class  Request  implements  org .apache .catalina .servlet4preview .http .HttpServletRequest      @Override      public  Locale getLocale ()           if  (!localesParsed) {             parseLocales();         }         if  (locales.size() > 0 ) {             return  locales.get(0 );         }         return  defaultLocale;     }     protected  void  parseLocales ()           localesParsed = true ;                                             TreeMap<Double, ArrayList<Locale>> locales = new  TreeMap<>();         Enumeration<String> values = getHeaders("accept-language" );         while  (values.hasMoreElements()) {             String value = values.nextElement();             parseLocalesHeader(value, locales);         }                           for  (ArrayList<Locale> list : locales.values()) {             for  (Locale locale : list) {                 addLocale(locale);             }         }     }      } 
spring提供的轮子
LocaleResolver 实现次接口,用于自定义解析规则 
RequestContextUtils 基于request获取Locale,优先使用自定义LocaleResolver 
LocaleContextHolder通过ThreadLocal持有Locale对象,在FrameworkServlet支持servlet.service时执行初始化。 
LocaleChangeInterceptor通过Configuration配置Locale切换规则 
 
综上,只要请求方在header中增加了accept-language 报文头,即可在代码中通过LocaleContextHolder获取Locale对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import  org.springframework.beans.factory.annotation.Autowired;import  org.springframework.boot.SpringApplication;import  org.springframework.boot.autoconfigure.SpringBootApplication;import  org.springframework.context.MessageSource;import  org.springframework.context.i18n.LocaleContextHolder;import  org.springframework.web.bind.annotation.GetMapping;import  org.springframework.web.bind.annotation.RestController;import  javax.servlet.http.HttpServletRequest;@SpringBootApplication @RestController public  class  I18nApplication      public  static  void  main (String[] args)           SpringApplication.run(I18nApplication.class, args);     }     @Autowired      private  MessageSource messageSource;     @GetMapping("hello")      public  Object hello (HttpServletRequest request)           return  messageSource.getMessage("10000" , new  Object[]{}, LocaleContextHolder.getLocale());     } } 
lang 除了accept-language外,常见在url中增加了国家及地区参数,如: https://twitter.com/?lang=zh 
通过lang 配置国际化,需要通过LocaleChangeInterceptor 进行配置,此配置的优先级高于accept-language 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Configuration public  class  I18nConfig  extends  WebMvcConfigurerAdapter      @Bean      public  LocaleResolver localeResolver ()           SessionLocaleResolver sessionLocaleResolver = new  SessionLocaleResolver();         return  sessionLocaleResolver;     }     @Bean      public  LocaleChangeInterceptor localeChangeInterceptor ()           LocaleChangeInterceptor localeChangeInterceptor = new  LocaleChangeInterceptor();         localeChangeInterceptor.setParamName("lang" );         return  localeChangeInterceptor;     }     @Override      public  void  addInterceptors (InterceptorRegistry registry)           registry.addInterceptor(localeChangeInterceptor());     } } 
Test Case 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import  org.junit.Test;import  org.junit.runner.RunWith;import  org.springframework.boot.context.embedded.LocalServerPort;import  org.springframework.boot.test.context.SpringBootTest;import  org.springframework.boot.test.web.client.TestRestTemplate;import  org.springframework.context.i18n.LocaleContextHolder;import  org.springframework.http.HttpEntity;import  org.springframework.http.HttpHeaders;import  org.springframework.http.HttpMethod;import  org.springframework.http.ResponseEntity;import  org.springframework.test.context.junit4.SpringRunner;import  java.util.Locale;import  java.util.ResourceBundle;import  static  org.junit.Assert.assertEquals;@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT) public  class  I18nApiTest      @LocalServerPort      private  int  port;     private  TestRestTemplate restTemplate = new  TestRestTemplate();     @Test      public  void  should_return_message_by_different_accept_language ()  throws  Exception          HttpHeaders headers = new  HttpHeaders();         headers.add("accept-language" , "en" );         HttpEntity entity = new  HttpEntity(headers);         ResponseEntity<String> resultEn = restTemplate.exchange("http://localhost:" +port+"/hello" , HttpMethod.GET, entity, String.class);         assertEquals("hello" , resultEn.getBody());         headers.remove("accept-language" );         headers.add("accept-language" , "zh" );         entity = new  HttpEntity(headers);         ResponseEntity<String> resultCh = restTemplate.exchange("http://localhost:" +port+"/hello" , HttpMethod.GET, entity, String.class);         assertEquals("你好" , resultCh.getBody());     }     @Test      public  void  should_return_zh_message_by_accept_language_zh_locale ()          LocaleContextHolder.setLocale(Locale.CHINA);         assertEquals("你好" , ResourceBundle.getBundle("messages" , LocaleContextHolder.getLocale()).getString("10000" ));     }     @Test      public  void  should_return_zh_message_by_different_lang ()          String lang = "en" ;         ResponseEntity<String> resultEn = restTemplate.getForEntity("http://localhost:" +port+"/hello?lang=" +lang, String.class);         assertEquals("hello" , resultEn.getBody());         lang = "zh" ;         ResponseEntity<String> resultCh = restTemplate.getForEntity("http://localhost:" +port+"/hello?lang=" +lang, String.class);         assertEquals("你好" , resultCh.getBody());     }     @Test      public  void  should_return_by_lang_when_set_lang_and_accept_language ()          String lang = "zh" ;         HttpHeaders headers = new  HttpHeaders();         headers.add("accept-language" , "en" );         HttpEntity entity = new  HttpEntity(headers);         ResponseEntity<String> resultEn = restTemplate.exchange("http://localhost:" +port+"/hello?lang=" +lang, HttpMethod.GET, entity, String.class);         assertEquals("你好" , resultEn.getBody());     } } 
LanguageTagCompliant Locale的命名规则为 lang-country, 如: zh-CN ,有时会看到zh_CH 这种写法,这是另一种规范,可以在CookieLocaleResolver了解规范定义
1 2 3 4 5 6 7 8 9 10 11 12 13 @UsesJava7 protected  Locale parseLocaleValue (String locale)  	return  (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale)); } 
需要在中LocaleChangeInterceptor开启兼容模式
1 localeChangeInterceptor.setLanguageTagCompliant(true ); 
但是为了符合规范,不推荐zh_CH 这种写法。
倾向 按照《Http参数格式约定》 文中所述,通用&非业务参数,一般会选择放到header中,所以比较倾向于accept-language 这种定义方法。