Java —— MessageFormat类 处理国际化
一、MessageFormat 概览
java.text包中的 Fomart 接口是所有处理格式的基础接口,有三个子类:DateFormat、MessageFormat、NumberFormat。
MessageFormat 是专门处理文本格式的类,且没有子类。
二、MessageFormat 细节
1、构造函数:
MessageFormat(String pattern); //pattern为字符串模式;使用默认的Locale.Category常量对应的语言格式 MessageFormat(String pattern,Locale locale); //使用指定的locale对应的语言
2、静态方法:
MessageFormat.format(String pattern,Ojbect... arguments); //创建一次性使用的格式字符串
3、实例方法:
主要涉及三个功能:设置或返回pattern、设置或返回locale、设置或返回Format实例。
//设置与返回当前的pattern public void applyPattern(String pattern); public String toPattern(); //设置与返回当前的locale public void setLocale(Locale locale); public Locale getLocale(); //一次设置单个格式 public void setFormat(int formatElementIndex, Format newFormat); public void setFormatByArgumentIndex(int argumentIndex, Format newFormat); //一次设置多个格式 public void setFormats(Format[] newFormats); public void setFormatsByArgumentIndex(Format[] newFormats); //获取设置的格式 public Format[] getFormats(); public Format[] getFormatsByArgumentIndex();//不推荐。如果一个ArgumantsInex没有用于任何格式元素,则返回null。 //format。后两个参数是可选的 public String format(Object[] arguments[,StringBuffer result,FieldPosition position]);
三、基础实践
1、使用构造方法指定pattern与locale
public MessageFormat(String pattern,Locale local);
模式参数 pattern 的语法:
语法即:真个表达式为一个字符串,需要替换的参数信息放在花括号{}中。
FormatType 与 FormatStyle 说明:
指定date与time要求传入的arguments 中对应对象为 Date类型,number 为数字类型。
Locale 类:
管理语言,用其常量可实现国际化。
实践:
Calendar calendar= Calendar.getInstance(); calendar.setTime(new Date());//calendar 只是管理时间,所以需要传入时间 String date=String.valueOf(calendar.get(Calendar.YEAR))+"." +String.valueOf(calendar.get(Calendar.MONTH)+1)+"." +String.valueOf(calendar.get(Calendar.DATE)); Object[] objects={"贵阳",date,"晴朗"}; //只指定应用对象:objects MessageFormat mf= new MessageFormat("当前时间:{1},地点:{0},天气:{2}");//索引对应于objects元素索引 String result=mf.format(objects); System.out.println(result);
注意:参数对准!上面只是将字符串填入对应参数位置,而没有用到FormatType、FormatStyle。获取美国时间:
四、Spring 中国际化实现
Spring 中国际化是通过实现MessageSource 根接口实现的,继承关系如下:
说明:图中除了标记的接口下面的类中没实现MessageSource 接口中的方法外,其它类都实现或通过继承实现了。
com.springframemork.context.MessageSource 接口中定义的方法:
String getMessage(String code, Object[] args, String defaultMessage, Locale locale);//code即要国际化的信息,args为对象参数数组 String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException; String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
该接口的具体实现细节是在com.springframework.context.support 包中的 MessageSourceSupport 抽象类中实现的。关键代码如下:
package org.springframework.context.support; ... import org.springframework.util.ObjectUtils; public abstract class MessageSourceSupport { private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat(""); protected final Log logger = LogFactory.getLog(getClass()); private boolean alwaysUseMessageFormat = false; private final Map<String, Map<Locale, MessageFormat>> messageFormatsPerMessage = new HashMap<String, Map<Locale, MessageFormat>>(); //标记1:定义存储国际化文本信息的结构 ... protected String formatMessage(String msg, Object[] args, Locale locale) { //国际化真正实现函数,被getMessage方法调用 if (msg == null || (!this.alwaysUseMessageFormat && ObjectUtils.isEmpty(args))) { return msg; } MessageFormat messageFormat = null; synchronized (this.messageFormatsPerMessage) { //标记2:MessageFormat 类不是同步的,如果多个线程 //访问同一个Format,要求必须同步。 Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg); if (messageFormatsPerLocale != null) { //标记3:当前msg的国际化信息Set集合还未初始化 messageFormat = messageFormatsPerLocale.get(locale); } else { //标记4:国际化信息Set集合中装入local为键,Message //-Format为值的Map 集合。 messageFormatsPerLocale = new HashMap<Locale, MessageFormat>(); this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale); } if (messageFormat == null) { try { messageFormat = createMessageFormat(msg, locale); //标记5:上面创建结构,此处创建MessageFormat实例 } catch (IllegalArgumentException ex) { // invalid message format - probably not intended for formatting, // rather using a message structure with no arguments involved if (this.alwaysUseMessageFormat) { throw ex; } // silently proceed with raw message if format not enforced messageFormat = INVALID_MESSAGE_FORMAT; } messageFormatsPerLocale.put(locale, messageFormat); } } if (messageFormat == INVALID_MESSAGE_FORMAT) { return msg; } synchronized (messageFormat) { return messageFormat.format(resolveArguments(args, locale)); //标记7:调用locale对应的MessageFormat 的format } } protected MessageFormat createMessageFormat(String msg, Locale locale) { //标记6:返回locale对应的MessageFormat return new MessageFormat((msg != null ? msg : ""), locale); } protected Object[] resolveArguments(Object[] args, Locale locale) { return args; } }
简单实践:
第一步:
配置资源操作类(org.springframework.context.support.ResourceBundleMessageSource或ReloadableResourceBundleMessageSource)bean,指定properties属性配置源文件(即namebases属性)
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>com/milan/mymain/myProperties</value>
</list>
</property>
<property name="useCodeAsDefaultMessage" value="false"/>
<property name="defaultEncoding" value="UTF-8"/>
<property name="cacheSeconds" value="60"/>
</bean>
第二步:
编辑myProperties 中程序要访问的属性,其实是MessageFormat 类的参数pattern ,以key=value形式存放:
HelloWorld=Time:{0,date} Hello {1}
第三步:
主函数中使用并运行:
ApplicationContext appContext=new ClassPathXmlApplicationContext("com/milan/mymain/applicationContext.xml"); Object[] object={new Date(),"小明"}; String msg=appContext.getMessage("HelloWorld", object, Locale.US);//HellowWorld 即属性文件中pattern 对应的key。以美国方式显示日期 System.out.println(msg);
结果:
五、Spring 中国际化报错
1、报错日志:
org.springframework.context.NoSuchMessageException: No message found under ...
简单归纳了出现情况:
第一种:
配置 ResourceBundleMessageSource 类的bean 时id 没有设成"messageSource"。可通过更前面的日志记录可知,没有找到id为messageSource的bean时,会使用默认的ReourceBundleMessageSource 对象,我们没有对他进行配置,当然找不到properties 文件。(可以打开ResoureBundleMessageSource类 源码,会发现bean工厂会使用默认的值为messageSource的字符串来查找该类的bean)
第二种:
.properties 文件路径配置出错。查找properties文件默认路径为src 文件夹下,所以如果文件放在包中,要加上包路径(如上面实践)。此外,有的说要在前面加"classpath:",但我加了反而提示错误,估计与spring版本有关。
第三种:
.properties 文件中确实没有定义所要查找的模式字符串pattern 对应的key。
2、学习如何定位错误位置:
简单的错误信息,如缺少包等可直接解决,如果不知错误原因,可仔细查看上面的日志记录,找到与错误相关的日志,查看来源,打开相关类的源码分析。
如上面因为我的bean的id没设成messageSource 而报的错误,可在日志记录找到一条:
[DEBUG] 2017-04-16 19:50:30,005 method:org.springframework.context.support.AbstractApplicationContext.initMessageSource(AbstractApplicationContext.java:807) Unable to locate MessageSource with name 'messageSource': using default [org.springframework.context.support.DelegatingMessageSource@2093decc]
分析日志:
提示无法定位messageSource 的bean,所以使用了默认的同样用于国际化的DelegateingMessageSource 类的实例,且能查看到日志来源。打开来源函数,如下:
protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { //MESSAGE_SOURCE_BEAN_NAME为方法所属类定义的常量,为messageSource //标记2:以MessageSource类形式获取id为messageSource的bean 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.isDebugEnabled()) { logger.debug("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.isDebugEnabled()) { //标记1:这就是打印日志的位置 logger.debug("Unable to locate MessageSource with name '" + MESSAGE_SOURCE_BEAN_NAME + "': using default [" + this.messageSource + "]"); } } }
知道了错误原因,ok,改ResourceBundleMessageSource bean的id 为messageSource,之后运行正确!
六、Sping 中实现国际化的各类 接口或类的关系梳理
学习框架,查看源码很有必要,能加深对框架各部分协作关系的理解。自己开始学习spring 时,看到的全是接口,竟然找不到接口中的方法是在哪实现的,如何调用的!
那么Spring 中实现国际化主要涉及到哪些接口或类,它们又是怎么协作的呢?
1、涉及接口或类:(spring-context-***.jar 中)
第一类:
是 com.springframework.context 包中的MessageSource 接口及其子接口或类:MessageSource 中定义了三个getMessage 方法
说明:图中可见,DelegatingMessageSource 实现了MessageSource 接口,找不到我们定义的id应该取名为messageSource 的ResourceBundleMessageSource的bean 时,默认使用的便是该类的实例。
ApplicationContext 实例可以直接调用 getMessage 方法获得国际化后的字符串,但我们可能会更关注getMessage 方法的实现细节。通过eclipse 可发现,getMessage 是在图所示的AbstractMessageSource 抽象类中实现的,但它实际上却是依赖了继承于MessageSourceSupport 类的方法。AbstractMessageSource 声明头:
<span style="font-size:12px;">public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource</span>
第二类:
同一jar包的 com.springframework.context.support包中的 MessageSourceSupport 类及其子类:定义了getMessage 方法所依赖的国际化真正实现方法
说明:MssageSourceSupport 关键代码已经在上面 四、Spring 中国际化实现 部分说明了。
2、各部分协作关系
原来画一张清晰的图也好难,随便画画:
说明:getMessage 方法的真正实现是在右边第一、三个框中,所以作为AbstractMessageSource 的子类的 ResourceBundleMessageSource 类也具有了国际化功能。左边的ApplicationContext 的子类就是通过操作 具有国际化功能的这些类而实现国际化的。
那么有个问题,为什么ApplicationContext 还要实现MessageSource 接口呢,它根本没实现真正实现MessageSource 中的getMessage 方法啊。原因是我们并不是直接操作ApplicationContext 的实例,而是将它的子类如 FileSystemXmlApplicationContext、ClassPathXmlApplicationContext 等的实例上转型给ApplicationContext 对象。为了让上转型得到的 ApplicationContext 对象能直接调用getMessage 方法,那么前提是ApplicationContext 接口自身必须要有这个方法,所以必须继承。
Spring 的核心思想就是依赖注入,通过注入接口同时利用接口的多态性来使各部分只关注自己的业务,同时降低耦合。
浙公网安备 33010602011771号