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 }
首先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