引言
随着公司海外业务发展,对系统的国际化改造已经势在必行。为了不影响国内业务,我们的做法是基于现有系统,采用国际化的思路搭建一套新的系统。我有幸被指定负责这次国际化基础架构设计和开发工作。
本人没有做过国际化相关项目,但以前在学习Spring MVC时隐约记得看到过相关章节。于是这几天又把 Spring MVC “国际化”相关章节又复习了一遍,并查阅其他一些相关资料,完成了一个简单的Spring MVC国际化demo搭建。
初步准备把这次的搭建经历分成三部分进行总结:1、java国际化之---jdk相关api(一);2、java国际化之---springMVC(二);3、java国际化之---demo讲解(三)。并计划在后续项目进行过程中,对实际遇到的问题再进行补充。
本章是第一部分:jdk相关api,主要讲解java.util.Locale类,以及java.text中的三大格式化类NumberFormat、DateFormat、MessageFormat的用法。另外 ResourceBundle类准备放到第二部分springMVC中进行分析。
名词解释
国际化:是开发支持多语言和数据格式的应用程序的技术,无需重新编写程序逻辑。简写为:i18n(Internationalization)。
本地化:是将国际化应用程序改成支持特定语言区域的技术。简写为:l10n(localization)。
个人理解为:在开发阶段,基于国际化技术,对系统程序进行开发和设计;在部署阶段,对于不同的国家采用对应本地化语言配置,但部署的代码是同一套。虽然程序在不同的国家分别单独进行部署,但对于开发人员来说代码是同一套,这样可以大大的降低代码维护成本。
java.util.Locale类
java.util.Locale类表示一个语言区域,是java国际化的核心,可以说对java程序的国际化设计,就是基于该类进行的。构造该类的实例,可以通过三个重要的变量完成:language、country、variant。对应的三个公有的构造方法分别为:
public Locale(String language)//指定语言 public Locale(String language, String country)//指定语言、国家 public Locale(String language, String country, String variant)//指定语言、国家、自定义参数
再来分别看下三个参数的含义:
language:语言代号,对于不同的语言有不同的语言代号。比如:zh(汉语)、en(英语)、de(德语) 等等。要想知道某个指定语言的代号,可以通过查询《ISO 639语言代码表》获得。另外可以通过查看LocaleISOData类的isoLanguageTable字段,这个字段定义了所有java支持的”语言代号”。
country:国家码,对于不同的国家有不同的国家码。比如:CN(中国)、US(美国)、DE(德国)等等。要想知道某个指定国家的国家码,可以通过查询《ISO 3166国家码表》。主要应用场景:有时候只是根据语言还是无法区分语言区域的,比如 美国和英国都说英语,但他们之间还是有区别,这时可以通过添加country字段进行区分。另外可以通过查看LocaleISOData类的isoCountryTable字段,这个字段定义了所有java支持的”国家码”。
variant:自定义变量,是自己任意指定。比如:可以是省份名称,比如有些省份有方言;也可以使是电脑操作系统等等。个人感觉用得较少,但也有特殊的场景会用到。
另外对于主流的语言区域,Locale类还提供了static final的常量,比如:
Locale locale = Locale.CHINA;
但一共只提供了10几个这样的常量,对于其他语言只能通过调用上述三个构造方法实现实例创建。
三大格式化类之NumberFormat
基于国际化技术的程序设计,最首要的工作就是对不同国家的不同数字表现形式进行处理,主要有三类数字类型需要处理:数字(整数、小数),金额(带符号)、百分数,不同语言区域有不同的表现形式。比如:德国的小数点是“逗号”,而大多数国家是“点”;中国的货币符号是“¥”,而美国的是“$”等等。
在java的国际化程序设计中,可以通过创建NumberFormat的不同实例,并结合不同的Locale参数来实现,如下(locale为传入的区域语言对象):
NumberFormat.getInstance(locale);//处理普通Number型的格式 NumberFormat.getCurrencyInstance(locale);//处理货币型的格式 NumberFormat.getPercentInstance(locale);//处理百分数的格式 NumberFormat.getIntegerInstance(locale);//处理整数的格式
通过查看源码,可以发现这些方法本质上是通过创建其子类DecimalFormat的实例,来实现的。通过调用NumberFormat实例的下列两个方法完成转换:
parse()方法,把字符串转换为指定的对象。
Format()方法,把对象转换成指定语言的字符串表现形式。
以下是我写的一个NumberFormat 国际化处理的工具类,由于NumberFormat是线程安全的,可以在程序启动时调用initLocal初始化方法,对指定locale的NumberFormat对象只初始化一次:
package com.sky.locale.web.I18nUtil; import org.apache.commons.lang.StringUtils; import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; /** * NumberFormat国际化工具类 NumberFormat是线程安全的,可以提前初始化 * Created by gantianxing on 2017/6/8. */ public class NumberFormatUtil { public static NumberFormat numF; //number型格式 public static NumberFormat curF; //货币型格式 public static NumberFormat perF; //百分比型格式 public static NumberFormat intF; //整型格式 /** * 程序启动时,或者切换语言时调用 * @param localeStr 如:en */ public static void initLocal(String localeStr){ Locale locale = null; if(StringUtils.isBlank(localeStr)){ return; } String [] a = localeStr.split("_"); if(a.length>1){ locale = new Locale(a[0],a[1]); } else { locale = new Locale(localeStr); } numF = NumberFormat.getInstance(locale); curF = NumberFormat.getCurrencyInstance(locale); perF = NumberFormat.getPercentInstance(locale); intF = NumberFormat.getIntegerInstance(locale); } /** * 字符串类型 转换为 数字类型 * @param numberStr * @return * @throws ParseException */ public static Number getNumber(String numberStr) throws ParseException{ return numF.parse(numberStr); } /** * 数字类型 转换为 字符串类型 * @param number * @return */ public static String getStringNumber(Number number){ numF.setMinimumFractionDigits(4);//最多保留4个小数 return numF.format(number); } /** * 把价格字符串转换为 double型 * 用于从页面读取字符串,保存到数据库 * @param priceStr 如:$100.00 * @return 如:100.00 */ public static double getNumberCur(String priceStr) throws ParseException { return curF.parse(priceStr).longValue(); } /** * 把double型转换为指定国家的 价格字符串 * 用户从数据库读取,输出到页面 * @param price 如:100.0012 * @return 如:$100.00 (自动四舍五入 保留两位小数) */ public static String getStringCur(double price){ return curF.format(price); } /** * 百分比字符串传 double * @param perStr * @return * @throws ParseException */ public static double getNumberPer(String perStr) throws ParseException{ return perF.parse(perStr).longValue(); } /** * 字double 转 百分比字符串 * @param per * @return */ public static String getStringPer(double per){ perF.setMinimumFractionDigits(2); //保留两位小数 return perF.format(per); } /** * 字符串 转 number * @param intStr * @return * @throws ParseException */ public static int getNumberInt(String intStr) throws ParseException{ return intF.parse(intStr).intValue(); } /** * int 转 字符串 * @param i * @return */ public static String getStringInt (int i){ return intF.format(i); } public static void main(String[] args) throws Exception{ double price = 111110.0058; //initLocal("de_DE"); //德国 //initLocal("th_TH"); //泰国 initLocal("zh_CN"); //中国 System.out.println(getStringNumber(price)); System.out.println(getStringCur(price)); System.out.println(getStringPer(0.87344)); System.out.println(getStringInt(1111111)); System.out.println("-------分界线------"); System.out.println(getNumber(getStringNumber(price))); System.out.println(getNumberCur(getStringCur(price))); System.out.println(getNumberPer(getStringPer(0.87344))); System.out.println(getNumberInt(getStringInt(1111111))); } }
在mian方法中首先调用initLocal方法,参数为指定某个语言区域。转入不同的locale,执行main方法可以看到不同的数据格式。
三大格式化类之DateFormat
基于国际化技术的程序设计,其另一个主要工作是对不同国家的不同日期、时间表现形式进行处理。在java可以通过创建不同的DateFormat实例实现。
对于日期格式,可以调用下列方法进行处理:
public final static DateFormat getDateInstance(int style, Locale aLocale)
两个参数,第二参数是语言区域对象。第一个参数,可以确定日期格式的显示完整性,从源码看可以选择如下5种类型:
DateFormat.DEFAULT //默认格式 DateFormat.SHORT //最短格式 DateFormat.MEDIUM //中等格式 DateFormat.LONG //长格式 DateFormat.FULL //最完整格式
对于时间格式,可以调用下列方法进行处理:
public final static DateFormat getTimeInstance(int style, Locale aLocale)
两个参数的含义,与日期方法getDateInstance的两个参数完全相同。
对于日期+时间格式,可以调用下列方法进行处理:
public final static DateFormat getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale)
这里有三个参数,第一个是日期的格式,第二个是时间的格式,第三个是语言区域对象
对于日期的格式化转换是通过调用DateFormat的format和parse方法完成的:
format()方法 将日期(时间)对象转换为指定国家的字符串表现形式
parse()方法 将指定国家的字符串表现形式转换为日期(时间)对象。
来看下示例代码:
/** * Created by gantianxing on 2017/6/8. */ public class DateFormatUtil { public static void main(String[] args) throws Exception{ Date date = new Date(); String dateStr = date.toString(); System.out.println("获取时间:" +dateStr); Locale locale = Locale.GERMANY;//德国 DateFormat defStyleD = DateFormat.getDateInstance(DateFormat.DEFAULT, locale); DateFormat shortStyleD = DateFormat.getDateInstance(DateFormat.SHORT, locale); DateFormat mediumStyleD = DateFormat.getDateInstance(DateFormat.MEDIUM, locale); DateFormat longStyleD = DateFormat.getDateInstance(DateFormat.LONG, locale); DateFormat fullStyleD = DateFormat.getDateInstance(DateFormat.FULL, locale); System.out.println(defStyleD.format(date)); System.out.println(shortStyleD.format(date)); System.out.println(mediumStyleD.format(date)); System.out.println(longStyleD.format(date)); System.out.println(fullStyleD.format(date)); System.out.println("-----分割线 ------"); DateFormat defStyleT = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale); DateFormat shortStyleT = DateFormat.getTimeInstance(DateFormat.SHORT, locale); DateFormat mediumStyleT = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale); DateFormat longStyleT = DateFormat.getTimeInstance(DateFormat.LONG, locale); DateFormat fullStyleT = DateFormat.getTimeInstance(DateFormat.FULL, locale); System.out.println(defStyleT.format(date)); System.out.println(shortStyleT.format(date)); System.out.println(mediumStyleT.format(date)); System.out.println(longStyleT.format(date)); System.out.println(fullStyleT.format(date)); System.out.println("-----分割线 parse方法------"); System.out.println(fullStyleD.parse("Donnerstag, 8. Juni 2017")); System.out.println(fullStyleT.parse("21:33 Uhr CST")); System.out.println("-----分割线 日期时间格式化处理------"); DateFormat shortDf = DateFormat.getDateTimeInstance(DateFormat.SHORT,DateFormat.SHORT,Locale.CHINA); System.out.println(shortDf.format(date)); DateFormat longDF = DateFormat.getDateTimeInstance(DateFormat.LONG,DateFormat.LONG,Locale.CHINA); System.out.println(longDF.format(date)); //String 转日期 String dStr = "2017年6月7日 上午10时13分01秒"; DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, Locale.CHINA); String dateS = df.parse(dStr).toString(); System.out.println("反向解析出的结果:" + dateS); } }
这里是以德国语言区域为例,执行main方法,可以看到德国的日期(时间)格式。也可以改变不同的Locale,实现国际化。
另外需要注意的是这里调用的getXXXInstance()方法,最终会创建其子类SimpleDateFormat的实例进行处理。并且是线程不安全的,与上述NumberFormat的处理方法不同,这里在每次使用时都需要创建一个实例,防止高并发情况下出现异常。
三大格式化类之MessageFormat
MessageFormat是对消息的格式化处理,同时该类也提供对国际化的支持。先看例子,如下:
public static void main(String[] args) { msgf(); msgNum(); } public static void msgf(){ Object[] objects={new Date(),"中国","晴朗"};//指定date或time,传入Date 实例 //只指定应用对象:objects MessageFormat mf= new MessageFormat("当前时间:{0,date,full},地点:{1},天气:{2}",Locale.CHINA); String result=mf.format(objects); System.out.println(result); } public static void msgNum() { Object[] objects={1111.11,2111.11};//指定date或time,传入Date 实例 MessageFormat mf= new MessageFormat("当前价格:{0,number,percent},原价:{1,number,currency}",Locale.US);
String result=mf.format(objects); System.out.println(result); }
通过查看源码,可以发现MessageFormat对国际化的支持,其底层其实是调用的NumberFormat和DateFormat进行处理的。
如上示例代码中:
日期处理:{0,date,full} 0表示取数组中下标为0的值进行替换,date表示是日期型,full表示完整的日期显示。
数字处理:{0,number,percent} 0表示取数组中下标为0的值进行替换,number表示是数字类型,percent表示是百分数。
执行main方法,打印信息如下:
当前时间:2017年6月8日 星期四,地点:中国,天气:晴朗 当前价格:111,111%,原价:$2,111.11
其他组合方式,不再一一列举,结合以上关于NumberFormat和DateFormat的描述,替换相关参数即可完成不同的消息格式处理。
总结
总之java.util.Locale的实例对象是实现国际化的关键,相对于key。贯穿在这个国际化的数字、日期(时间)、消息根式化处理过程中。
关于MessageFormat的一个典型的应用场景是,从配置文件中取出指定个格式消息体,再进行相关占位符的替换,然后返回给前端页面。这个场景我会在下一章springMVC的demo中进行讲解。这里不再累述。
另外MessageFormat也是线程不安全的,使用的时候需要注意。也就是说java中的三大格式化处理,只有NumberFormat是线程安全的,DateFormat和MessageFormat都不是。
最后,关于创建NumberFormat和DateFormat的实例的getXXXInstance()方法,是静态工厂方法模式,与通过构造方法创建实例想必,在《effective java》书中跟推崇的静态工厂方法模式,在我们的日常开发中也可以借鉴这种方式。而创建MessageFormat的实例采用的是构造方法。