【源码】关于 Spring 国际化 MessageSource

前言

容器类顶层接口 ApplicationContext 同时也是一个 MessageSourceMessageSource 便是所谓的 国际化 接口,它的 getMessage 方法允许我们根据不同的 Locale 返回不同的信息结果

本章节集合部分源码解读其原理,并给出 SpringBoot 的实现和使用示例

版本

Spring Framework:5.2.x

MessageSource

public interface MessageSource {

	@Nullable
	String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);

	String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

	String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;

}

顶层接口,提供了 getMessage 的三个重载方法:

  • String code:待解析的字符串
  • Object[] args:待解析字符串的参数,可以基于 MessageFormat 解析带有 占位符 的字符串
  • String defaultMessage:读取不到 国际化 信息时依据此默认值进行处理
  • MessageSourceResolvable resolvable,对上述几个参数的封装,默认实现类 DefaultMessageSourceResolvable
  • Locale locale:国际化定位,如 Locale.CHINA 代表中国

HierarchicalMessageSource

public interface HierarchicalMessageSource extends MessageSource {

	void setParentMessageSource(@Nullable MessageSource parent);

	@Nullable
	MessageSource getParentMessageSource();

}

拓展了 MessageSource 的层级关系

DelegatingMessageSource

Spring 容器启动时,如果容器中不存在对应的 Bean 实例,则容器中默认创建的 MessageSource 实例类型为 DelegatingMessageSource

具体源码参考 AbstractApplicationContext#initMessageSource 方法

该类的主要作用是试图将 国际化 处理委托给父级 MessageSource,否则降级处理:处理成默认字符串或者抛出异常

public class DelegatingMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {

	@Nullable
	private MessageSource parentMessageSource;

	@Override
	public void setParentMessageSource(@Nullable MessageSource parent) {
		this.parentMessageSource = parent;
	}

	@Override
	@Nullable
	public MessageSource getParentMessageSource() {
		return this.parentMessageSource;
	}
	
	@Override
	@Nullable
	public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
		/**
		 * 由 parentMessageSource 处理
		 */
		if (this.parentMessageSource != null) {
			return this.parentMessageSource.getMessage(code, args, defaultMessage, locale);
		}
		
		// 依据 defaultMessage 继续处理
		else if (defaultMessage != null) {
			return renderDefaultMessage(defaultMessage, args, locale);
		}
		else {
			return null;
		}
	}

	@Override
	public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
		if (this.parentMessageSource != null) {
			return this.parentMessageSource.getMessage(code, args, locale);
		}
		// 抛出异常
		else {
			throw new NoSuchMessageException(code, locale);
		}
	}

	// ...

}

AbstractMessageSource

核心抽象类,实现 配置化解析国际化信息 的关键类,我们从 getMessage 方法的实现入手

getMessage

	@Override
	public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
		/**
		 * code 的国际化处理
		 */
		String msg = getMessageInternal(code, args, locale);
		if (msg != null) {
			return msg;
		}

		/**
		 * 如果处理结果为 null,且不提供 defaultMessage
		 * 		则根据属性 useCodeAsDefaultMessage
		 * 		决定原样返回,还是抛出异常,默认抛出异常
		 */
		if (defaultMessage == null) {
			return getDefaultMessage(code);
		}

		// 依据 defaultMessage 处理
		return renderDefaultMessage(defaultMessage, args, locale);
	}
  • getMessageInternal 方法处理信息解析
  • useCodeAsDefaultMessage 属性决定解析无结果的信息是原样返回还是抛出异常,默认抛出异常
  • renderDefaultMessage 即依据 defaultMessage 解析信息解析

重点关注 getMessageInternal 方法

getMessageInternal

	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)) {
			/**
			 * 无参信息的处理
			 * 本质是直接映射对应 Locale 的配置文件
			 * 方法由子类复写
			 */
			String message = resolveCodeWithoutArguments(code, locale);
			if (message != null) {
				return message;
			}
		}

		else {
			argsToUse = resolveArguments(args, locale);

			/**
			 * 有参信息的处理
			 * 本质是基于 MessageFormat 进行处理,
			 * 		同时缓存对应的 MessageFormat
			 * 方法由子类复写	
			 */
			MessageFormat messageFormat = resolveCode(code, locale);
			if (messageFormat != null) {
				synchronized (messageFormat) {
					return messageFormat.format(argsToUse);
				}
			}
		}

		// 基于 commonMessages 处理信息
		Properties commonMessages = getCommonMessages();
		if (commonMessages != null) {
			String commonMessage = commonMessages.getProperty(code);
			if (commonMessage != null) {
				return formatMessage(commonMessage, args, locale);
			}
		}

		// 最后,尝试交给父级处理
		return getMessageFromParent(code, argsToUse, locale);
	}
  • 对于无参信息交给 resolveCodeWithoutArguments 方法处理,其本质就是读取对应 Locale 的配置文件,具体由子类实现
  • 对于有参信息交给 resolveCode 方法处理,其本质是读取对应 Locale 的配置文件后,基于 MessageFormat 和对应的参数进行处理,同时进行缓存,具体由子类实现
  • 之后,会尝试基于 commonMessages 进行处理
  • 最后,尝试交给父级处理

重点关注子类对 resolveCodeWithoutArgumentsresolveCode 方法的实现

AbstractResourceBasedMessageSource

public abstract class AbstractResourceBasedMessageSource extends AbstractMessageSource {

	// 配置文件路径名
	private final Set<String> basenameSet = new LinkedHashSet<>(4);

	// 字符集
	@Nullable
	private String defaultEncoding;

	private boolean fallbackToSystemLocale = true;

	// 默认 Locale
	@Nullable
	private Locale defaultLocale;

	// 缓存时间,默认永久
	private long cacheMillis = -1;

	// ... getter & setter

}

AbstractMessageSource 的基础上,拓展了配置文件相关属性的管理

ResourceBundleMessageSource

核心实现类,也是 SpringBoot 提供的默认实现,我们重点关注其对上述提到的 resolveCodeWithoutArgumentsresolveCode 方法的实现

resolveCodeWithoutArguments

	@Override
	protected String resolveCodeWithoutArguments(String code, Locale locale) {
		/**
		 * 根据不同的 Locale 加载对应的 ResourceBundle
		 * 		因为没有参数,就相当于单纯的配置文件映射
		 * 基于 basenameSet 加载对应路径的配置文件
		 * 		基于 ResourceBundle 类来管理配置文件
		 */
		Set<String> basenames = getBasenameSet();
		for (String basename : basenames) {
			ResourceBundle bundle = getResourceBundle(basename, locale);
			if (bundle != null) {
				String result = getStringOrNull(bundle, code);
				if (result != null) {
					return result;
				}
			}
		}
		return null;
	}
  • 基于 basenameSet 属性和 Locale 定位对应的配置文件
  • 配置文件由 ResourceBundle 类管理
  • 直接读取对应的 key 即可
  • 因此使用 ResourceBundleMessageSource 时我们需要定义具体的 basenameSetSpringBoot 给出的默认实现中,basenameSet 默认为 messages,可以由 spring.messages.basename 属性自定义,后文会给出示例

resolveCode

	@Override
	@Nullable
	protected MessageFormat resolveCode(String code, Locale locale) {
		Set<String> basenames = getBasenameSet();
		for (String basename : basenames) {
			ResourceBundle bundle = getResourceBundle(basename, locale);
			if (bundle != null) {
				/**
				 * 由对应的 bundle code locale 创建对应的
				 * 		MessageFormat 缓存后返回
				 * 缓存数据结构 Map<String, Map<Locale, MessageFormat>>
				 */
				MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
				if (messageFormat != null) {
					return messageFormat;
				}
			}
		}
		return null;
	}
  • 同样的,基于 basenameSet 属性和 Locale 定位对应的配置文件
  • 配置文件由 ResourceBundle 类管理
  • 基于 bundle code locale 创建对应的 MessageFormat 并缓存起来,其数据结构为 Map<String, Map<Locale, MessageFormat>>
  • 默认创建的 MessageFormatnew MessageFormat(msg, locale),这基本就够用了,如果需要,也可以自行复写 MessageSourceSupport#createMessageFormat 方法

示例

此处我们给出 SpringBoot 中使用 ResourceBundleMessageSource 的示例,首先,SpringBoot 默认 自动装配MessageSource 即就是 ResourceBundleMessageSource

详情参考类 MessageSourceAutoConfiguration

因此,我们仅需要简单地配置即可使用:

application.yaml

spring:
  messages:
    basename: i18n/messages
    encoding: UTF-8

配置文件路径设置为 i18n/messages,编码为 UTF-8

i18n/messages

类路径下创建配置文件 messages.properties,同时再创建一个 messages_zh_CN.properties 用于匹配示例中的 LocaleCHINA,如图:
messages

demo

@SpringBootApplication
public class MsApplication {

	@Autowired
	MessageSource messageSource;

	public static void main(String[] args) {
		SpringApplication.run(MsApplication.class, args);
	}

	@Component
	class test implements CommandLineRunner {

		@Override
		public void run(String... args) throws Exception {
			// 无参信息处理
			System.out.println(
					messageSource.getMessage("hello", null, Locale.CHINA));

			// 有参信息处理
			System.out.println(
					messageSource.getMessage("time", new Object[]{ new Date() }, Locale.CHINA));
		}
	}

ReloadableResourceBundleMessageSource

还有另一个实现类 ReloadableResourceBundleMessageSource,同时支持对 xml 配置文件的解析,且它结合 basename 属性和 Locale 对配置文件的路径进行匹配

SpringBoot 中,它的使用与 ResourceBundleMessageSource 基本相似,但是如果要使用 ReloadableResourceBundleMessageSource,需要单独创建对应的 Bean实例,如下示例

示例

@SpringBootApplication
public class MsApplication {

	@Autowired
	MessageSource messageSource;

	public static void main(String[] args) {
		SpringApplication.run(MsApplication.class, args);
	}

	@Component
	class test implements CommandLineRunner {

		@Override
		public void run(String... args) throws Exception {
			// 无参解析
			System.out.println(
					messageSource.getMessage("hello", null, Locale.CHINA));

			// 有参解析
			System.out.println(
					messageSource.getMessage("time", new Object[]{ new Date() }, Locale.CHINA));

		}
	}

	/**
	 * 此处避免循环依赖,创建在静态内部类中
	 * 当然,一般情况单独由配置类管理
	 */
	@Configuration
	static class MessageSourceCofiguration {

		/**
		 * 声明 MessageSource 的 bean 实例为
		 * 		ReloadableResourceBundleMessageSource 
		 */
		@Bean
		public MessageSource messageSource() {
			ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource
					= new ReloadableResourceBundleMessageSource();
			reloadableResourceBundleMessageSource.setBasename("i18n/messages");
			reloadableResourceBundleMessageSource.setDefaultEncoding("UTF-8");
			reloadableResourceBundleMessageSource.setCacheMillis(2000);
			return reloadableResourceBundleMessageSource;
		}
	}
}

其他代码、配置文件如出一辙,无需改变

总结

以上,就是对 Spring 国际化的介绍,主要介绍了 MessageSource 体系下的各类,以及对 SpringBoot 中的使用方式进行了具体的示范

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值