级别: 中级 Ting Wei Feng, 软件工程师, IBM Xiao Chuan Cai, 软件工程师, IBM
2009 年 9 月 29 日 了解如何使用 IBM Rational Application Developer 来实现 JavaServer Faces Web 程序的全球化。本文描述了开发全球市场所面临的挑战,并介绍了怎样使用 JavaServer Faces Widget Library(JWL)来处理这个问题。
从版本 7 开始,IBM®Rational®Application Developer 包含了 JavaServer Faces Widget Library(JWL),它是一个 Java™Server Faces (JSF)- 以及用于快速开发网络程序的基于 JavaScript 的库。
JWL,hxclient 的 JavaScript 库,实施了对 JWL 构件的客户端支持。它还包含了一系列所谓的“JSF 转化器”,可以帮助开发员分析和格式化日期,时间以及特定位置模式的来回号码,更特别的是,这些工具就是 JavaSimpleDateFormat 和 DecimalFormat 的 JavaScript 实施。这些工具对于设计成支持多种语言的程序来说十分有用,因为它们帮助您处理来自客户端位置敏感数据输入和输出的挑战。
本篇文章还解释了与 JavaServer Faces 程序中多线程相关的全球化挑战问题,并提供了一个解决方案。本文作者假设您有关于 JSF 和 JWL 的基础知识。
全球化基础知识
在网络程序中,输出语言是由 HTTP 请求报头的 Accept-Language 区域所决定。用户可以指定喜好的语言和带有浏览器设置的场所。
JSF 框架分析 HTTP 请求报头。您可以通过使用如列表 1 所示的报头来获取该值。
列表 1. 获取关于语言和场所的请求
Locale locale = FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
|
场所值用于决定用于显示的预言。
使用 JWL 来处理场所敏感输出与输入
在快速引入全球化之后,现在我们已经做好准备,讨论全球化 JSF 网络程序中面临的两个挑战:
- 使用本地格式显示客户日期和时间
- 显示和结束本地数字格式
hxclient 在页面中是怎样初始化的
只要您在使用页面中的 JWL 标签,您就必须确保页面中安装有库,并得到了合适的初始化。如果您设置了浏览器场所请求为“ja”(日语),并查看使用 JWL 调用的 HTML 页面的源代码,您将会发现如列表 2 所示的代码。
列表 2. 使用 JWL 的网络页面的结构
<script type="text/JavaScript" language="JavaScript"
src="/sample/.ibmjsfres/hxclient_core_v3_0_8.js"></script>
<script type="text/JavaScript" language="JavaScript"
src="/sample/.ibmjsfres/hxclient_S_v3_0_8_ja.js?viewLocale=ja">
</script>
<script type="text/JavaScript" language="JavaScript">
if (hX_5) hX_5.setResourceServer("/sample/.ibmjsfres");
if(hX_5 && hX_5.setLocale) hX_5.setLocale("ja");
</script>
|
该代码可以完成三件事:
- 包含页面上的 hxclient 内核脚本库
- 包含页面上的 hxclient 场所特定的脚本库
- 创建当前的页面场所
正如您所看到的那样,您不需要手动创建场所,因为 JWL 会通过阅读场所请求来自动决定场所。对于 hxclient 的自动初始化,您已经做好准备将其用于日期,时间和数字格式了。
使用 JWL 的本地格式来显示客户日期和时间
对于网络程序,开发员想要显示页面上的最新请求时间。在全球化的程序中,时间必须是当地格式的。
例如,一个美国的用户可能会想要按以下方式查看日期时间格式 :
Last Refresh: Friday, May 8, 2009 1:35:07 PM GMT+08:00
但是一个日本的用户也许会看到如下所示的日期时间格式:
前回の最新表示: 2009 年 5 月 8 日金曜日 13 時 41 分 07 秒 GMT+08:00
因为时间是在客户端计算的,所以 JavaScript 并没有通过内置 API 来提供一个方案:Date.toLocaleString(). 这是因为:
- 返回值的场所并不是由浏览器的场所设置所决定的,而是由客户操作系统的场所决定。
- 开发员没有机会指定日期的格式。
日期时间转换器是 JWL 客户脚本库的一个工具。它是 Java SimpleDateFormat 类的 JavaScript 实施,它可以很好的支持 ICU4J(Unicode Java 库的国际构件 Library)。它使得客户端的日期/时间格式变得像处理 Java™一样容易。在本例中,我们使用 DateTimeConverter 和 ICU4J 来生成客户端的本地日期/时间。
为了快点开始,让我们来看客户端的脚步是什么样的:
列表 3. JavaScript 的日期/时间格式
function getLocalizedCurrentTime()
{
var converter = hX.getConverterById("date_converter");
if(null == converter)
{
//construct a new DateTimeConverter and add it to converter set
hX.addConverter("date_converter", new hX.DateTimeConverter(
"format:EEEE, MMMM d, yyyy h:mm:ss a z",
"ICU4J:true"));
}
converter = hX.getConverterById("date_converter");
var date = new Date();
//format client date and return
return converter.valueToString(date);
}
|
您所要做的只是定位日期/时间。但是这些参数会传递给 DateTimeConverter 构建器:
-
“ICU4J:true” 允许 DateTimeConverter 接受模式的特定 ICU 特征。
-
“format:EEEE, MMMM d, yyyy h:mm:ss a z” 意味着 DateTimeConverter 用于格式化日期/时间的模式。
到目前为止,我们已经向您展示了,您需要什么客户代码来格式化日期/时间。但这并不足够。考虑一下模式。您知道对于不同的场所模式会是什么样的吗?
例如,对于美国用户的模式是这样的:
EEEE,MMMM d,yyyy h:mm:ss a z
而日本客户的模式是这样的:
yyyy'年'M'月'd'日'EEEE H'時'mm'分'ss'秒'z
还好模式并不是硬代码的,因为开发员并不可能知道不同场所的所有模式。像 “yyyy mm dd”这样的模式对于某个开发员来说可能是合理的,但是对用户看来就是很古怪的 。
因此,答案就是从服务器端获取模式,因为 ICU4J 已经为所有开发员的使用准备好了模式。列表 4 中的代码展示了,我们怎样根据本地的请求获取一个模式:
列表 4. 通过使用 ICU4J 来生成日期/时间模式
public class FormatterUtils
{
public static String getDateTimePattern(int dateFormat)
{
Locale locale =
FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
CalendarData calData = new
CalendarData(ULocale.forLocale(locale), null);
String[] dateTimePatterns = calData.getStringArray("DateTimePatterns");
return dateTimePatterns[dateFormat + 4] + " " + dateTimePatterns[dateFormat];
}
}
|
参数 dateFormat 显示了格式模式使用的方法。基本上,在 ICU com.ibm.icu.text.DateFormat 类中有四种预定义的类型:
对于不同格式的参数,方法 getDateTimePattern 的返回值也有所不同。例如,对于 en-us 场所,有四种类型的返回值:
- EEEE,MMMM d,yyyy h:mm:ss a z
- MMMM d,yyyy h:mm:ss a z
- MMM d,yyyy h:mm:ss a
- M/d/yy h:mm a
所以通过从四个类型中选择一个,您已经准备好了格式模式,让我们假设您使用的是 Full 格式。下一步是将该格式模式应用到客户端,以使用 hxclient。在 Java™Server Pages(JSP™)脚本中,这很容易做到。对于客户端的模式,JavaScript 代码应该像列表 5 所示。
列表 5. 将格式模式捆绑到客户代码
<script>
var datetimeFormatPattern = "<%=FormatterUtils.getDateTimePattern()%>";
function getLocalizedCurrentTime()
{
var converter = hX.getConverterById("date_converter");
if(null == converter)
{
//construct a new DateTimeConverter and add it to converter set
hX.addConverter("date_converter", new hX.DateTimeConverter(
"format:" + datetimeFormatPattern, "ICU4J:true"));
}
converter = hX.getConverterById("date_converter");
var date = new Date();
//format client date and return
return converter.valueToString(date);
}
</script>
|
现在已经完成了。通过调用 JavaScriptgetLocalizedCurrentTime 方法,您可以得到客户端的日期和时间文本(例如,图 1 显示是英语,图 2 显示的是日语)。
图 1. 显示 en-us 的日期和时间
图 2. 显示 ja-jp 的日期和时间
显示和接受本地 JWL 的数字格式
显示带有本地格式的数字,与显示日期/时间相类似。像 1000.1 这样的十进制数字在美国应该显示成 1,000.1,而在德国会显示成 1.000,1。
不像日期和时间,这些数字应该从服务器端获得,在这里 ICUDecimalFormat 可以完成这一点。但是有些情况下,您可能想要格式化客户端的数字(例如,接受用户输入并显示值)。在 JWLhxclient 库中, NumberConverter 实施了客户端上 DecimalFormat 的逻辑。
同样,让我们首先直接跳到完整的客户代码:
列表 6. 格式数字的 JavaScript
<script>
var decimalFormatPattern = "<%= FormatterUtils.getDecimalFormatPattern() %>";
var decimalFormatSymbols = "<%= FormatterUtils.getDecimalFormatSymbols() %>";
function formatDecimal(value)
{
var converter = hX.getConverterById("number_converter");
if(null == converter)
{
hX.addConverter("number_converter",
new hX.NumberConverter("pattern:" + decimalFormatPattern,
"locale:" + decimalFormatSymbols, "ICU4J:true"));
}
converter = hX.getConverterById("number_converter");
var output = cvt.valueToString(value);
return output;
}
</script>
|
有三种参数会传递给 NumberConverter 构建器:
-
“ICU4J:true”:它允许 NumberConverter 接受在模式中 ICU 特定的特征。
-
“pattern:”+ decimalFormatPattern: 这是在转化值的时候会用到的数字模式。
-
“locale:” + decimalFormatSymbols: 这是转化值时会用到的场所信息。它包含了十进制分隔符,百分比字符等等之类的符号。
至于 DateTimeConverter,模式和场所信息应该从服务器端获得:
列表 7. 使用 ICU4J 来生成数字模式和符号
public class FormatterUtils
{
private static DecimalFormat getDecimalFormatter()
{
Locale locale =
FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
return (DecimalFormat)NumberFormat.getInstance(locale);
}
public static String getDecimalFormatPattern()
{
return getDecimalFormatter().toPattern();
}
public static String getDecimalFormatSymbols()
{
Locale locale =
FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
//The information is provided as a string of 6 characters with fixed format:
StringBuilder sb = new StringBuilder();
sb.append(symbols.getGroupingSeparator());
sb.append(symbols.getDecimalSeparator());
sb.append(symbols.getPercent());
sb.append(symbols.getPerMill());
sb.append(symbols.getMinusSign());
sb.append(symbols.getCurrencySymbol());
return sb.toString();
}
}
|
正如您在列表 6 中看到的那样,getDecimalFormatPattern 和 getDecimalFormatSymbols 用于传递页面中的模式和场所信息。对于服务器端的协助,您可以使用 JavaScriptformatDecimal() 功能,来格式化 JavaScriptNumber 类型变量。列表 8 向您展示了一个这样的例子。
列表 8. 使用客户代码来格式化数字
<script>
//Suppose current locale is "de"
var value = 1000.1; //type of value is Number
var formatted = formatDecimal(value); //the formatted value is "1.000,1" in Germany
</script>
|
页面中的数字通常会像图 3 那样格式化(该例展示了德语中的汇容量统计):
图 3. 页面中的格式化数据:
在处理数字时,不但要注意输出还要注意输入。输入随着用户的习惯而不同。一个德国的用户可能会输入 1.000,1 或者 1000,1。但是这两种格式的数据都应该识别为十进制的数据 1000.1。对于开发员来说,这是一个艰难的任务,因为他们需要写上千行的代码以识别输入。
好的消息是 JWLhxclient 可以转换数字。您可以使用该功能来将用户输入转化为 JavaScript Number 对象。该对象通过自动执行这些步骤,来将显示的数字和值区别开来:
- 接受用户输入。
- 通过使用
NumberConverter,来分析 String 对象的输入到 Number 对象。
- 使用转化值以进行计算。
- 通过再次使用
NumberConverter,来将计算结果格式化回至 String 对象。
- 使用格式化的值以进行显示。
列表 9 中的代码举了一个例子,展示了怎样分析用户输入(对于 Deutsch 或者 German,场所是“de”):
列表 9. JavaScript 分析的输入数字
<script>
var decimalFormatPattern = "<%= FormatterUtils.getDecimalFormatPattern() %>";
var decimalFormatSymbols = "<%= FormatterUtils.getDecimalFormatSymbols() %>";
function formatDecimal(input)
{
var converter = hX.getConverterById("number_converter");
if(null == converter)
{
hX.addConverter("number_converter",
new hX.NumberConverter("pattern:" + decimalFormatPattern,
"locale:" + decimalFormatSymbols, "ICU4J:true"));
}
converter = hX.getConverterById("number_converter");
var output = cvt.stringToValue(input);
return output;
}
var parsedValue = formatDecimal("1.000,1"); //the parsed value is 1000.1
parsedValue = formatDecimal("1000,1"); //the parsed value is 1000.1
parsedValue = formatDecimal("oops"); //parsing fails, null is returned
</script>
|
列表 9 中的代码在以下方面与列表 6 十分相似:从服务器端获取模式和场所信息,创建一个 NumberConverter 的范例,然后执行该任务。唯一的区别是调用的方法:stringToValue(). 方法的名字是不言而喻的:它分析一个 String 对象,并试着将其转化为 Number 对象。如果在转化期间发生了什么错误,那么该方法将会返回 null。因此,NumberConverter 也可以用于识别用户输入。
到目前为止,我们已经介绍了 JWLhxclient 脚本是怎样帮助您处理客户端的数字分析和格式问题。在例子中的代码中,我们总是需要得到服务器端的场所信息,以生成格式模式。因此,在接下来的章节中,我们将会讨论更高级的话题,就是场所信息是怎样传递的,以及在 JSF 网络程序中是怎样使用这些信息的。
在多线程 JSF 程序中全球化道路的风险
使用 JSF 进行全球化很容易,但是并不是极简单的。特别是在多线程的程序中,如果设计缺乏完善的考虑,错误的假设将会使得您的全球化支持,变得更像是应用一系列的补丁。接下来我们将要介绍的技术,将会使得您的多线程 JSF 程序变得更加强壮。
决定显示的语言
全球化通常构建于场所的基础之上。因此,怎样实现全球化归根结底就是怎样处理场所。在获取场所信息之后,您已经可以决定基于场所用户界面中显示的语言。在大多数情况下,JSF 框架会关注带有 <loadBundle> 标签的语言包,它根据场所请求获取包,而不需要额外的编码。但是如果您需要使用 Java 代码中的语言包内容,那么您就需要使用场所信息来获取包的路径,然后自己格式化信息。列表 10 给出了范例代码。
列表 10. 一个简单的信息格式化范例
public class MessageFormatter {
private static final String MESSAGE_BUNDLE_NAME = "com.ibm.sample.messages";
private static String formatMessage(String msgKey, Object[] args,Locale locale) {
ResourceBundle messageBundle = ResourceBundle.getBundle(MESSAGE_BUNDLE_NAME,locale);
String message = messageBundle.getString(msgKey);
if (message != null) {
if (args == null) {
return message;
} else {
return MessageFormat.format(message, args);
}
} else {
return msgKey;
}
}
}
|
得到请求的场所
我们可以看到文本文件中列表 10 所示的代码,它解释了怎样使用 Java 方法来实现全球化。但是在实际操作时当您看到大量 getRequestLocale 存在时是很痛苦的,如列表 1 所示,叫做格式化信息之前。
提示:
查看 IBM Java 技术库以得到关于 轻松使用线程:不共享有时是最好的变量的更多信息。
接下来,我们将会向您展示怎样让工作变得更加完美。
您已经知道,JSFFacesContext 是作为服务线程中的一个 ThreadLocal 变量保存的。因为所有的 Faces backend 豆是在服务线程中运行的,所以您可以在任意时刻获取 FacesContext 对象。对于场所对象 T 也是真实的,因为它是从 FacesContext 获取的。这样您就可以将场所获取方法放在工具类中,来重写列表 10 中的代码,如列表 11 所示。
列表 11. 获取信息格式化器的最佳方法
public class LocaleUtils(){
public class LocaleUtils{} {
public static Locale getRequestLocale() {
return FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
}
}
public class MessageFormatter {
private static final String MESSAGE_BUNDLE_NAME = "com.ibm.sample.messages";
private static String formatMessage(String msgKey, Object[] args) {
Locale locale = LocaleUtils.getRequestLocale();
ResourceBundle messageBundle = ResourceBundle.getBundle(MESSAGE_BUNDLE_NAME, locale);
String message = messageBundle.getString(msgKey);
if (message != null) {
if (args == null) {
return message;
} else {
return MessageFormat.format(message, args);
}
} else {
return msgKey;
}
}
}
|
然后 MessageFormatter.formatMessage 就成为从语言包中获取信息的唯一方法了,它会返回基于请求的信息。现在您可以忽略场所了,因为在 LocaleUtils 的支持下它现在透明地为您工作了。
将请求场所传递给用户线程
如果您想让场所自动地工作,那么现在您做的还不够。
当您在处理多线程程序时,还有一个例外。在多线程程序中,用户定义的线程并不是由服务线程初始化的,因此它们并没有将 FacesContext 初始化为一个 ThreadLocal 变量。结果,在该线程中运行的代码在格式化信息时,就不能访问场所信息了。
对于以上问题通用的解决方法,是通过在构造变量期间将其传递给用户线程,来让每一个用户都有一个场所变量。当变量在线程中准备好时,您可以使用列表 10 中所示的范例代码,来在用户线程中运行代码以实现全球化。但是,这需要编辑用户线程构造器及其调用的代码。有一种方法可以让它更加完美地工作。
我们知道至少有一个用户线程(或者是它的上级线程)会在服务线程中实现。换句话说,至少有一个用户线程的构造器在服务线程中得到访问。因为用户线程构造器中的代码仍然在访问线程的环境下运行,所以在构造用户线程时,我们还有机会从服务线程继承场所对象。这样我们将可以传递实现每一个子线程初始化的场所对象。这样我们就有了 LocaleUtils 的升级版本,这样通过实施 LocaleSensitive 界面,在用户线程成为场所敏感的线程之后,在多线程环境中它就可以更好地工作了,就像我们在 BaseThread 中所做的那样(见于列表 12)。
列表 12. 场所敏感的基底线程
public static Locale DEFAULT_LOCALE = Locale.ENGLISH;
public static Locale getCurrentRequestLocale() {
Locale locale = null;
try {
//try to retrieve locale information from FacesContext
locale = FacesContext.getCurrentInstance().
getExternalContext().getRequestLocale();
}
catch(Throwable e){
//Unable to reach FacesContext
//Therefore call getLocale to retrieve locale variable from current thread
Thread t = Thread.currentThread();
if (t instanceof LocaleSensitive) {
locale = ((LocaleSensitive) t).getLocale();
}
finally{
if(locale == null){
locale = DEFAULT_LOCALE;
}
}
return locale;
}
}
public interface LocaleSensitive {
public Locale getLocale();
}
public class BaseThread extends Thread implements LocaleSensitive {
protected Locale locale= null;
public Locale getLocale() {
return locale;
}
public BaseThread(String name) {
super(name);
//Save a copy of locale in thread instance
this.locale=LocaleUtils.getCurrentRequestLocale();
}
}
public class UserThread extends BaseThread{
public void run(){
…
String message = MessageFormatter.formatMessage(msgKey,msgParameters);
…
}
}
|
首先,您需要定义一个名为 LocaleSensitive 的界面。任何一个实施该节目的类都应该提供 getLocale() 方法中的场所信息。
然后您使用 BaseThread 来实施 LocaleSensitive 线程。BaseThread 构造器通过访问 LocaleUtils.getCurrentRequestLocale() 来得到场所对象,它从 FacesContext 中(在服务环境下),或者上级线程中得到场所信息(这就解释了场所信息是怎样传递的)。然后如果有用户线程成为 BaseThread 的子类,该线程可以使用 MessageFormatter,而不需升级版本的 LocaleUtils 类,就像我们在服务环境下所做的那样。
使用列表 12 中的代码,您就可以在任何线程中传递和得到场所信息了,而无需担心 FacesContext 是否可以访问。现在多线程 JSF 网络程序中的风险就不复存在了。
总结
IBM Rational Application Developer 中的 JavaServer Widgets 库,提供了脚本帮助开发员处理客户端的全球化问题,这使得全球化开发变得更加容易。在您处理多线程的网络程序时,要十分谨慎,因为您需要掌握一定的技巧,以确保在程序的任何地方都能够访问场所信息。
参考资料 学习
获得产品和技术
讨论
作者简介  | 
|  | Ting Wei Feng 是位于上海的 IBM China 系统和技术研究部的一名软件工程师。他于 2008 年加入 IBM,从事 IBM Systems Director 和 Storage Configuration Manager 项目工作。 |
 | 
|  | Xiao Chuan Cai 是位于上海的 IBM China 系统和技术研究部的一名软件工程师。他于 2007 年加入 IBM,从事 IBM Systems Director 和 Storage Configuration Manager 项目工作。 |
对本文的评价
|