写点什么

深入了解 Spring 之 MessageSource

用户头像
邱学喆
关注
发布于: 1 小时前
深入了解Spring之MessageSource

一. 概述

在 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;  }
复制代码

知道了其大体的功能;那么我们带着以下问题来讨论其运作原理;

  • 描述信息是如何初始化进去的?

  • 如何根据 code 找到对应的描述信息,其数据结构是什么样的?

  • 在 spring-boot 是如何使用的?

二. 原理

类图如下:


上面的类图信息,根据其属性名可以大体猜测出相关的用途;重点介绍几个关键的属性

  • commonMessages , 是存放通用的映射信息表,其优先级最低;

  • useCodeAsDefaultMessage 当找到对应的 message 信息时,则使用 code 作为 message.

  • messageFormatsPerMessage 存放 commonMessages 对应的 MessageFormat 对象;

  • alwaysUseMessageFormat 是否允许一直使用 MessageFormat,进行格式化;

  • basenameSet 文件名集合,一般是去掉国际化的文件简称;例如: message_zh_CN.properties, 则 basename 为 message.

  • cacheMillis 文件缓冲时间;


ResourceBundleMessageSource 是通过 JDK 提供的 ResourceBundle 去加载资源文件的;同时也是运用 ResourceBundle 的刷新特性;

  • cachedResourceBundles,缓存文件名所对应的 ResourceBundle 资源信息

  • cachedBundleMessageFormats 缓存 ResourceBundle 所对应的 MessageFormat 信息;


ReloadableResourceBundleMessageSource 是通过其属性 PropertiesPersister 去加载资源,支持 xml、properties 两个格式的文件。

  • cachedFilenames 缓存 basename 所对应的全文件名称集合;

  • cachedProperties 缓存全文件所对应的 PropertiesHolder;

  • cachedMergedProperties 缓存中 Locale 对应的 PropertiesHolder 信息;


在 ReloadableResourceBundleMessageSource 对象中有使用 PropertiesHolder 对象进行保存映射关系

  • cachedMessageFormats 保存着 code 对应的 messageFormat 信息

  • refreshTimestamp 刷新时间

  • fileTimestamp 文件时间戳

  • properties 缓存 code 与 message 映射关系


至于 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 对象进来;至此,我们可以拿到该对象,从而对其进行操作;


发布于: 1 小时前阅读数: 4
用户头像

邱学喆

关注

计算机原理的深度解读,源码分析。 2018.08.26 加入

在IT领域keep Learning。要知其然,也要知其所以然。原理的爱好,源码的阅读。输出我对原理以及源码解读的理解。个人的仓库:https://gitee.com/Michael_Chan

评论

发布
暂无评论
深入了解Spring之MessageSource