Spring MessageSource 源码剖析从 refresh 初始化到国际化消息解析的完整链路【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunterMessageSource是 Spring 框架实现国际化的核心接口负责根据code、Locale与参数动态解析本地化消息。本文以 Spring-MessageSource.md 为骨架沿着AbstractApplicationContext.refresh()→initMessageSource()→getMessage()的调用链逐段剖析 Spring 源码中消息源的初始化、默认实现、三级查找与资源包加载机制并结合仓库中 IoC 初始化与 Spring Boot 自动配置的相关文档帮助读者彻底理解 Spring 国际化的底层实现。一、MessageSource 在 Spring 整体脉络中的定位在 16张图解锁Spring的整体脉络 中MessageSource 被明确标注为容器的国际化功能负责获取某个国际化资源。在AbstractApplicationContext.refresh()这一 IoC 容器初始化的模板方法中initMessageSource()与initApplicationEventMulticaster()等并列属于容器启动阶段初始化容器内部基础组件的环节之一。在 1、BeanDefinition的资源定位过程 中可以看到 refresh() 的完整流程public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // 调用容器准备刷新 prepareRefresh(); // BeanDefinition 资源文件的载入 ConfigurableListableBeanFactory beanFactory obtainFreshBeanFactory(); // 为 BeanFactory 配置容器特性 prepareBeanFactory(beanFactory); try { postProcessBeanFactory(beanFactory); invokeBeanFactoryPostProcessors(beanFactory); registerBeanPostProcessors(beanFactory); // 初始化信息源和国际化相关. initMessageSource(); // 初始化容器事件传播器 initApplicationEventMulticaster(); onRefresh(); registerListeners(); // 初始化 Bean并对 lazy-init 属性进行处理 finishBeanFactoryInitialization(beanFactory); finishRefresh(); } catch (BeansException ex) { destroyBeans(); cancelRefresh(ex); throw ex; } } }同样的流程在 4、依赖注入(DI).md) 中也有完整的对照展示读者可以交叉阅读理解initMessageSource()在整个 IoC 容器启动时序中的确切位置——它发生在BeanFactoryPostProcessor与BeanPostProcessor注册之后、事件传播器初始化之前。二、初始化入口initMessageSource()initMessageSource()是org.springframework.context.support.AbstractApplicationContext的受保护方法也是 MessageSource 生命周期的起点。它的职责是从容器中查找名称为messageSource的 Bean若存在则使用并挂接父消息源若不存在则注册一个空的DelegatingMessageSource兜底。protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory getBeanFactory(); // 判断是否含有 messageSource if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { // 读取xml配置文件中 idmessageSource的数据 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 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 ]); } } }2.1 两个分支的理解分支一容器中存在messageSourceBean通过beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class)取出用户配置的消息源实例典型实现是ResourceBundleMessageSource或ReloadableResourceBundleMessageSource。若当前 ApplicationContext 存在父容器this.parent ! null且该消息源实现了HierarchicalMessageSource接口并且尚未设置父消息源则把父容器的内部消息源挂接为当前消息源的parentMessageSource形成父子消息源链实现消息的逐级向上查找。分支二容器中不存在messageSourceBean使用DelegatingMessageSource作为空实现保证任何时刻调用getMessage都不会因空指针而失败并设置父消息源后通过beanFactory.registerSingleton把它注册为单例 Bean。下面的调试截图对应分支一的执行现场messageSource已被赋值为ResourceBundleMessageSource其basenameset中包含一个元素messages.messagedefaultEncoding为ISO-8859-1fallbackToSystemLocale为truecacheMillis为-1关闭缓存2.2 对应的 XML 配置从源码可以看出用户可以通过在 XML 配置文件中定义idmessageSource的 Bean 来注入自定义消息源例如bean idmessageSource classorg.springframework.context.support.ResourceBundleMessageSource property namebasenames list valuemessages/value valuemessages.message/value /list /property property namedefaultEncoding valueUTF-8/ property nameuseCodeAsDefaultMessage valuetrue/ /bean上述配置中的basenames即对应调试截图中basenameset集合的内容defaultEncoding对应defaultEncoding属性useCodeAsDefaultMessage则直接影响后续getDefaultMessage的行为。三、消息查找入口AbstractApplicationContext#getMessage用户代码如通过Autowired MessageSource或实现MessageSourceAware调用消息查找时首先进入的是AbstractApplicationContext#getMessageOverride public String getMessage(String code, Nullable Object[] args, Locale locale) throws NoSuchMessageException { return getMessageSource().getMessage(code, args, locale); }它只是把请求委托给内部持有的messageSource即initMessageSource阶段确定的实例真正的解析逻辑发生在AbstractMessageSource中。四、核心解析逻辑AbstractMessageSource#getMessageAbstractMessageSource的getMessage是final方法它固化了三级查找的骨架先查内部消息 → 再查默认消息 → 最后抛出NoSuchMessageException。Override public final String getMessage(String code, Nullable Object[] args, Locale locale) throws NoSuchMessageException { // 获取对应的信息 String msg getMessageInternal(code, args, locale); if (msg ! null) { return msg; } // 默认信息 null String fallback getDefaultMessage(code); if (fallback ! null) { return fallback; } throw new NoSuchMessageException(code, locale); }接下来重点分析其中的两个方法。4.1 getDefaultMessage代码即默认消息Nullable protected String getDefaultMessage(String code) { // 判断是否使用默认值 if (isUseCodeAsDefaultMessage()) { return code; } return null; }该方法的行为极其简单当配置了useCodeAsDefaultMessage true时如果内部解析不到消息就直接返回code本身把消息编码当作消息内容展示避免抛出异常否则返回null继续走抛异常的分支。这是生产环境调试国际化缺失问题时非常实用的开关——开启后未命中的消息会原样显示编码便于快速定位缺失条目。4.2 getMessageInternal核心查找流程Nullable protected String getMessageInternal(Nullable String code, Nullable Object[] args, Nullable Locale locale) { if (code null) { return null; } if (locale null) { // 获取语言默认值 locale Locale.getDefault(); } Object[] argsToUse args; if (!isAlwaysUseMessageFormat() ObjectUtils.isEmpty(args)) { // Optimized resolution: no arguments to apply, // therefore no MessageFormat needs to be involved. // Note that the default implementation still uses MessageFormat; // this can be overridden in specific subclasses. String message resolveCodeWithoutArguments(code, locale); if (message ! null) { return message; } } else { // Resolve arguments eagerly, for the case where the message // is defined in a parent MessageSource but resolvable arguments // are defined in the child MessageSource. argsToUse resolveArguments(args, locale); MessageFormat messageFormat resolveCode(code, locale); if (messageFormat ! null) { synchronized (messageFormat) { return messageFormat.format(argsToUse); } } } // Check locale-independent common messages for the given message code. Properties commonMessages getCommonMessages(); if (commonMessages ! null) { String commonMessage commonMessages.getProperty(code); if (commonMessage ! null) { return formatMessage(commonMessage, args, locale); } } // Not found - check parent, if any. return getMessageFromParent(code, argsToUse, locale); }逐段拆解该方法的执行路径空值保护code null直接返回nulllocale null时取Locale.getDefault()即 JVM 默认语言环境。无参数快速路径当isAlwaysUseMessageFormat()为 false 且参数数组为空时不需要MessageFormat参与格式化直接调用resolveCodeWithoutArguments(code, locale)做轻量查找命中即返回。这是最常见的调用场景如getMessage(user.name, null, locale)。有参数完整路径当传入参数时先resolveArguments(args, locale)解析参数如LocaleContextHolder中的语言环境再resolveCode(code, locale)得到MessageFormat在synchronized块内执行messageFormat.format(argsToUse)完成占位符替换如{0}、{1}。公共消息兜底通过getCommonMessages()获取与语言无关的公共消息属性集若命中则formatMessage格式化返回。父级查找若以上均未命中调用getMessageFromParent(code, argsToUse, locale)沿父子消息源链向上查找这正对应 2.1 节中父子消息源的挂接设计。五、无参数路径的落点ResourceBundleMessageSource#resolveCodeWithoutArgumentsAbstractMessageSource定义了resolveCodeWithoutArguments与resolveCode抽象方法具体实现交由子类。最常见的子类ResourceBundleMessageSource基于 JDK 的ResourceBundle机制加载.properties资源文件Override protected String resolveCodeWithoutArguments(String code, Locale locale) { SetString basenames getBasenameSet(); for (String basename : basenames) { // 加载 basename ResourceBundle bundle getResourceBundle(basename, locale); if (bundle ! null) { // 从basename对应的文件中获取对应的值 String result getStringOrNull(bundle, code); if (result ! null) { return result; } } } return null; }其逻辑为获取全部basename对应配置中的basenames属性内部去重后存放于basenameset逐个按basename locale组合加载ResourceBundle通过getStringOrNull(bundle, code)从 bundle 中取消息值本质就是一次 Map 的get操作第一个命中即返回全部未命中返回null。下面两张调试截图完整展示了这一过程。先看加载前basenames集合的迭代状态当前 basename 为messages.message即将调用getResourceBundle再看资源加载成功后的结果bundle是PropertyResourceBundle其内部lookup是一个 HashMap存放了键值对codeenemen即资源文件中配置的消息条目locale为enname为messages.message。此时getStringOrNull直接从这个 Map 中取出消息值5.1 资源文件命名与目录约定ResourceBundle的命名遵循basename _ 语言 _ 国家/地区的约定并支持逐级回退messages.properties // 默认语言 messages_zh_CN.properties // 简体中文 messages_en.properties // 英文 messages_en_US.properties // 美式英语当请求locale en_US时查找顺序为messages_en_US → messages_en → messages命中即止。fallbackToSystemLocale调试截图中为true控制是否在未命中指定语言时回退到系统默认语言。一个典型的消息文件内容示例# messages.properties user.nameName welcomeWelcome, {0}! # messages_zh_CN.properties user.name姓名 welcome欢迎, {0}!5.2 没有配置文件时会发生什么当资源文件缺失、且所有 basename 均未命中时resolveCodeWithoutArguments返回null控制权回到AbstractMessageSource#getMessage进入getDefaultMessage分支。调试截图展示的正是getDefaultMessage(code)的执行现场——isUseCodeAsDefaultMessage()为 false 时直接返回null最终抛出NoSuchMessageException若开启了useCodeAsDefaultMessagetrue返回code本身消息编码原样呈现便于发现问题若未开启抛NoSuchMessageException(code, locale)由上层如MessageSourceAware的调用方或RequestMappingHandlerAdapter的异常处理决定如何兜底。六、Spring Boot 场景下的自动配置在 Spring Boot 项目中MessageSource 无需手动声明 Bean而是由自动配置类接管。仓库中的 SpringBoot-ConditionalOnBean.md 对org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration有完整的源码级分析Configuration(proxyBeanMethods false) ConditionalOnMissingBean(name AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search SearchStrategy.CURRENT) AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) Conditional(ResourceBundleCondition.class) EnableConfigurationProperties public class MessageSourceAutoConfiguration {}关键点在于ConditionalOnMissingBean(name AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, ...)仅当容器中尚不存在名为messageSource的 Bean 时才生效与initMessageSource()中的containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)判断逻辑遥相呼应ResourceBundleCondition重写了SpringBootCondition#getMatchOutcome从环境变量读取消息资源基名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; }也就是说Spring Boot 默认基名为messages可以通过spring.messages.basename修改。在application.properties中的典型配置为spring.messages.basenamemessages,messages.message spring.messages.encodingUTF-8 spring.messages.fallback-to-system-localetrue spring.messages.use-code-as-default-messagetrue其中use-code-as-default-message对应 4.1 节的isUseCodeAsDefaultMessage()fallback-to-system-locale对应 5.1 节的回退开关与框架级属性一一对应方便在 Spring Boot 项目中等价落地。七、调用链全景总结综合以上分析一次getMessage调用的完整链路如下AbstractApplicationContext#getMessage委托给内部messageSource由refresh()中的initMessageSource()初始化优先使用用户定义的messageSourceBean否则用DelegatingMessageSource兜底并注册单例AbstractMessageSource#getMessagefinal执行三级查找getMessageInternal→getDefaultMessage→ 抛出NoSuchMessageExceptiongetMessageInternal根据是否有参数分流无参走resolveCodeWithoutArguments有参走resolveArguments resolveCode MessageFormat.format另有公共消息与父消息源两级兜底ResourceBundleMessageSource#resolveCodeWithoutArguments遍历basenames按 locale 逐级加载ResourceBundle对应messages_zh_CN.properties等资源文件用getStringOrNull完成 Map 取值全部未命中时依据useCodeAsDefaultMessage决定返回编码本身还是抛出NoSuchMessageException。通过这条链路Spring 用模板方法 责任链 策略的组合设计将国际化消息的加载ResourceBundle、解析MessageFormat与回退父子消息源、默认消息优雅地解耦。深入理解 Spring-MessageSource.md 及本文的源码走读即可掌握在 Spring/Spring Boot 项目中配置多语言资源、处理参数占位符与排查消息缺失问题的完整方法论。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考