一. 概述
在 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)
@EnableConfigurationProperties
public 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 对象进来;至此,我们可以拿到该对象,从而对其进行操作;
评论