1. 程式人生 > >Springboot國際化資訊(i18n)解析

Springboot國際化資訊(i18n)解析

國際化資訊理解

國際化資訊也稱為本地化資訊 。 Java 通過 java.util.Locale 類來表示本地化物件,它通過 “語言型別” 和 “國家/地區” 來建立一個確定的本地化物件 。舉個例子吧,比如在傳送一個具體的請求的時候,在header中設定一個鍵值對:"Accept-Language":"zh",通過Accept-Language對應值,伺服器就可以決定使用哪一個區域的語言,找到相應的資原始檔,格式化處理,然後返回給客戶端。

MessageSource

Spring 定義了 MessageSource 介面,用於訪問國際化資訊。

  • getMessage(String code, Object[] args, String defaultMessage, Locale locale)
  • getMessage(String code, Object[] args, Locale locale)
  • getMessage(MessageSourceResolvable resolvable, Locale locale)

 

 MessageSourceAutoConfiguration

 springboot提供了國際化資訊自動配置類,配置類中註冊了ResourceBundleMessageSource實現類。

 1 @Configuration
 2 @ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)
 3 @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
 4 @Conditional(ResourceBundleCondition.class)
 5 @EnableConfigurationProperties
 6 public class MessageSourceAutoConfiguration {
 7 
 8     private static final Resource[] NO_RESOURCES = {};
 9 
10     @Bean
11     @ConfigurationProperties(prefix = "spring.messages")
12     public MessageSourceProperties messageSourceProperties() {
13         return new MessageSourceProperties();
14     }
15 
16     @Bean
17     public MessageSource messageSource(MessageSourceProperties properties) {
18         ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
19         if (StringUtils.hasText(properties.getBasename())) {
20             messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(
21                     StringUtils.trimAllWhitespace(properties.getBasename())));
22         }
23         if (properties.getEncoding() != null) {
24             messageSource.setDefaultEncoding(properties.getEncoding().name());
25         }
26         messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
27         Duration cacheDuration = properties.getCacheDuration();
28         if (cacheDuration != null) {
29             messageSource.setCacheMillis(cacheDuration.toMillis());
30         }
31         messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
32         messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
33         return messageSource;
34     }
35 
36     protected static class ResourceBundleCondition extends SpringBootCondition {
37 
38         private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();
39 
40         @Override
41         public ConditionOutcome getMatchOutcome(ConditionContext context,
42                 AnnotatedTypeMetadata metadata) {
43             String basename = context.getEnvironment()
44                     .getProperty("spring.messages.basename", "messages");
45             ConditionOutcome outcome = cache.get(basename);
46             if (outcome == null) {
47                 outcome = getMatchOutcomeForBasename(context, basename);
48                 cache.put(basename, outcome);
49             }
50             return outcome;
51         }
52 
53         private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context,
54                 String basename) {
55             ConditionMessage.Builder message = ConditionMessage
56                     .forCondition("ResourceBundle");
57             for (String name : StringUtils.commaDelimitedListToStringArray(
58                     StringUtils.trimAllWhitespace(basename))) {
59                 for (Resource resource : getResources(context.getClassLoader(), name)) {
60                     if (resource.exists()) {
61                         return ConditionOutcome
62                                 .match(message.found("bundle").items(resource));
63                     }
64                 }
65             }
66             return ConditionOutcome.noMatch(
67                     message.didNotFind("bundle with basename " + basename).atAll());
68         }
69 
70         private Resource[] getResources(ClassLoader classLoader, String name) {
71             String target = name.replace('.', '/');
72             try {
73                 return new PathMatchingResourcePatternResolver(classLoader)
74                         .getResources("classpath*:" + target + ".properties");
75             }
76             catch (Exception ex) {
77                 return NO_RESOURCES;
78             }
79         }
80 
81     }
82 
83 }
View Code

首先MessageSource配置生效依靠一個ResourceBundleCondition條件,從環境變數中讀取spring.messages.basename對應的值,預設值是messages,這個值就是MessageSource對應的資原始檔名稱,資原始檔副檔名是.properties,然後通過PathMatchingResourcePatternResolver從“classpath*:”目錄下讀取對應的資原始檔,如果能正常讀取到資原始檔,則載入配置類。

 

 springmvc自動裝配配置類,註冊了一個RequestContextFilter過濾器。

 每一次請求,LocaleContextHolder都會儲存當前請求的本地化資訊。

 通過MessageSourceAccessor根據code獲取具體資訊時,如果預設配置的本地化物件為空,則通過LocaleContextHolder獲取。

 上圖的messageSource是應用程式上下文物件(本文建立的是GenericWebApplicationContext例項),該messageSource物件會呼叫ResourceBundleMessageSource例項獲取具體資訊。

ValidationAutoConfiguration

引數校驗hibernate-validator是通過這個自動裝配載入進來的。

 1 @Configuration
 2 @ConditionalOnClass(ExecutableValidator.class)
 3 @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
 4 @Import(PrimaryDefaultValidatorPostProcessor.class)
 5 public class ValidationAutoConfiguration {
 6 
 7     @Bean
 8     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 9     @ConditionalOnMissingBean(Validator.class)
10     public static LocalValidatorFactoryBean defaultValidator() {
11         LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
12         MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
13         factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
14         return factoryBean;
15     }
16 
17     @Bean
18     @ConditionalOnMissingBean
19     public static MethodValidationPostProcessor methodValidationPostProcessor(
20             Environment environment, @Lazy Validator validator) {
21         MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
22         boolean proxyTargetClass = environment
23                 .getProperty("spring.aop.proxy-target-class", Boolean.class, true);
24         processor.setProxyTargetClass(proxyTargetClass);
25         processor.setValidator(validator);
26         return processor;
27     }
28 
29 }
View Code

MethodValidationPostProcessor這個後置處理處理方法裡單個引數校驗的註解(JSR和Hibernate validator的校驗只能對Object的屬性(也就是Bean的域)進行校驗,不能對單個的引數進行校驗。)。

LocalValidatorFactoryBean實現了javax.validation.ValidatorFactory和javax.validation.Validator這兩個介面,以及Spring的org.springframework.validation.Validator介面,你可以將這些介面當中的任意一個注入到需要呼叫驗證邏輯的Bean裡。

 預設情況下,LocalValidatorFactoryBean建立的validator使用PlatformResourceBundleLocator獲取資源的繫結關係,獲取的資源名稱是:ValidationMessages

 使用者自定義的校驗資訊放在專案classpath目錄下。

另外hibernate-validator還會載入預設的校驗資原始檔,名稱是:org.hibernate.validator.ValidationMessages。可以看到,預設的校驗資源捆綁檔案包含了不同區域的資訊的配置。

通過LocalValidatorFactoryBean獲取的validator是如何根據不同的地區載入不同校驗資原始檔呢?hibernate-validator暴露了一個訊息插補器(MessageInterpolator),spring正是重新代理這個訊息插補器。

 通過LocaleContextMessageInterpolator原始碼,可以看到最終還是通過LocaleContextHolder獲取當前時區資訊。

 

是否可以自定義國際化校驗的資源資訊呢?當然是肯定的,我們只需要重寫LocalValidatorFactoryBean型別bean的建立過程,通過setValidationMessageSource方法指定自定義的資源資訊。

MessageSource測試

基礎測試

建立Resouce bundle messages

編寫message source測試方法,從request中獲取當前Locale值

 編寫測試類,指定當前請求的Locale值或者設定請求頭的header值:Accept-Language:zh

根據測試類中請求的Locale值不同,獲取到的文字也不同。

格式化測試

建立Resouce bundle messages

編寫message source測試方法,從request中獲取當前Locale值

 編寫測試類,指定當前請求的Locale值或者設定請求頭的header值:Accept-Language:zh

 

根據測試類中請求的Locale值不同,獲取到的格式化的文字也不同。

靜態message source測試

動態註冊message(可區分Locale),可用於自定義message source。

編寫測試的方法,通過MessageSourceAccessor訪問。

 編寫測試類,獲取自定義message source中的資訊。

根據測試類中請求的Locale值不同,獲取到的文字也不同。

&n