一. 概述
在 spring 中有这么一个基础组件 MessageSource,可以存放信息地方。但我们最常用的场景是用来国际化处理;我们通过对其提供的方法来看,可以大体知道其是功能,代码如下:
public interface MessageSource { /** * 通过code找到对应的描述信息,接着描述中的占位符替换入参中的参数信息;如果没有找到对应的描述信息,则使用默认的信息 */ String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale); /** * 通过code以及对应的国际码找到对应的描述信息,接着描述中的占位符替换入参中的参数信息;如果没有找到对应的描述信息,则抛出异常 */ String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException; /** * resolvable实际是封装了code、args、 defaultMessage三个信息的对象。所以其介绍与第二个方法有点类似; */ String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException; }
复制代码
知道了其大体的功能;那么我们带着以下问题来讨论其运作原理;
二. 原理
类图如下:
上面的类图信息,根据其属性名可以大体猜测出相关的用途;重点介绍几个关键的属性
commonMessages , 是存放通用的映射信息表,其优先级最低;
useCodeAsDefaultMessage 当找到对应的 message 信息时,则使用 code 作为 message.
messageFormatsPerMessage 存放 commonMessages 对应的 MessageFormat 对象;
alwaysUseMessageFormat 是否允许一直使用 MessageFormat,进行格式化;
basenameSet 文件名集合,一般是去掉国际化的文件简称;例如: message_zh_CN.properties, 则 basename 为 message.
cacheMillis 文件缓冲时间;
ResourceBundleMessageSource 是通过 JDK 提供的 ResourceBundle 去加载资源文件的;同时也是运用 ResourceBundle 的刷新特性;
ReloadableResourceBundleMessageSource 是通过其属性 PropertiesPersister 去加载资源,支持 xml、properties 两个格式的文件。
cachedFilenames 缓存 basename 所对应的全文件名称集合;
cachedProperties 缓存全文件所对应的 PropertiesHolder;
cachedMergedProperties 缓存中 Locale 对应的 PropertiesHolder 信息;
在 ReloadableResourceBundleMessageSource 对象中有使用 PropertiesHolder 对象进行保存映射关系
至于 StaticMessageSource 对象呢,其是允许手动添加信息,这里不在过多讨论其细节;
我们重点其 ReloadableResourceBundleMessageSource 与 ResourceBundleMessageSource 的流程图;
ReloadableResourceBundleMessageSource 根据 code 返回 message 信息的流程图,如下:
ResourceBundleMessageSource 根据 code 返回的 message 信息的流程图,如下:
从上面流程图来看,有关资源加载,是采用懒加载方式,需要的时候再加载;
同时,里面格式化 MessageFormat,具体可以可以在网上查阅其用法,这里列一下语法规则:
MessageFormatPattern: String MessageFormatPattern FormatElement String FormatElement: { ArgumentIndex } { ArgumentIndex , FormatType } { ArgumentIndex , FormatType , FormatStyle } FormatType: one of number date time choice FormatStyle: short medium long full integer currency percent SubformatPattern
复制代码
这里不过多阐述;
至于文件资源加载,ResourceBundleMessageSource 采用 JDK 提供的 ResourceBundle 去解析文件,在 spring 中提供了 MessageSourceControl 类去加载文件,其中加载文件方式是通过 ClassLoader 方式去加载;
而 ReloadableResourceBundleMessageSource 是自实现了 DefaultResourceLoader 去加载文件资源,用 DefaultPropertiesPersister 去解析文件;
有兴趣的可以自行阅读其源码;
三. 实战
1. 初始化
在 spring 中,refresh 会初始化其组件,代码如下:
public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { //... try { // 初始化messageSource initMessageSource(); } catch (BeansException ex) { //..... } finally { //... } }}protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class); // Make MessageSource aware of parent MessageSource. if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) { HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource; if (hms.getParentMessageSource() == null) { // Only set parent context as parent MessageSource if no parent MessageSource // registered already. hms.setParentMessageSource(getInternalParentMessageSource()); } } if (logger.isTraceEnabled()) { logger.trace("Using MessageSource [" + this.messageSource + "]"); } } else { // Use empty MessageSource to be able to accept getMessage calls. DelegatingMessageSource dms = new DelegatingMessageSource(); dms.setParentMessageSource(getInternalParentMessageSource()); this.messageSource = dms; beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); if (logger.isTraceEnabled()) { logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]"); } }}
复制代码
从 initMessageSource 方法中可以看出,spring 会从容器中检查是否有注入 MessageSource 容器,如果有,则默认使用该 MessageSource 对象,如果没有,是使用 DelegatingMessageSource 对象;然而 DelegatingMessageSource 只是代理,其只是一个空壳;
而在我们 spring boot 框架中,默认使用的是 ResourceBundleMessageSource。具体代码如下:
@Configuration(proxyBeanMethods = false)@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@Conditional(ResourceBundleCondition.class)@EnableConfigurationPropertiespublic class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
@Bean @ConfigurationProperties(prefix = "spring.messages") public MessageSourceProperties messageSourceProperties() { return new MessageSourceProperties(); }
@Bean public MessageSource messageSource(MessageSourceProperties properties) { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); if (StringUtils.hasText(properties.getBasename())) { messageSource.setBasenames(StringUtils .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename()))); } if (properties.getEncoding() != null) { messageSource.setDefaultEncoding(properties.getEncoding().name()); } messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale()); Duration cacheDuration = properties.getCacheDuration(); if (cacheDuration != null) { messageSource.setCacheMillis(cacheDuration.toMillis()); } messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat()); messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage()); return messageSource; }
protected static class ResourceBundleCondition extends SpringBootCondition {
private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();
@Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages"); ConditionOutcome outcome = cache.get(basename); if (outcome == null) { outcome = getMatchOutcomeForBasename(context, basename); cache.put(basename, outcome); } return outcome; }
private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) { ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle"); for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) { for (Resource resource : getResources(context.getClassLoader(), name)) { if (resource.exists()) { return ConditionOutcome.match(message.found("bundle").items(resource)); } } } return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll()); }
private Resource[] getResources(ClassLoader classLoader, String name) { String target = name.replace('.', '/'); try { return new PathMatchingResourcePatternResolver(classLoader) .getResources("classpath*:" + target + ".properties"); } catch (Exception ex) { return NO_RESOURCES; } }
}
}
复制代码
我们可以通过 MessageSourceProperties 的配置,从而指定加载哪些文件;默认情况下,是加载 resource 根目录下的 messages 开头的资源文件。如图:
2. 使用
在 spring 中,我们是如何拿到 messageSource 对象的;我们通过在类中实现该 MessageSourceAware 接口,在 spring 容器中会自动调用该类的 setMessageSource 方法,传递 MessageSource 对象进来;至此,我们可以拿到该对象,从而对其进行操作;
评论