级别: 中级 Elliotte Rusty Harold (elharo@metalab.unc.edu), 副教授, Polytechnic University
2007 年 2 月 02 日 Xalan XSLT 处理器几乎可以调用类路径中任何 Java™ 类的方法。这样做可以提高性能,提供 XSLT 中不可用的三角函数、执行文件 I/O、与数据库和网络服务器会话,或实现以 Java 语言可轻松编写但在 XSLT 中难以编写的算法。学习从 Xalan 中调用 Java 代码的基础知识。
Extensible Stylesheet Language Transformations(XSLT)是一种图灵完备型(Turing complete)编程语言。这也就意味着,给定足够的内存,它可以计算以其他任何编程语言编写的程序能够计算的内容。但是,这种理论上的能力通常是不切实际的。在某些情况下,您需要以更为传统的语言而非 XSLT 来编写代码:
- 外部 I/O —— 例如,文件、数据库或网络连接。XSLT 读或写这些东西的能力非常有限。
- 外部设备 —— 例如,通用串行总线(USB)端口或系统时钟。
- 高等数学 —— XSLT 在基本数学方面游刃有余,但它不支持三角学、指数函数、对数或其他更为高级的数学运算符及函数。尽管您可使用 XSLT 支持的基本运算实现所有这一切,但那将是一个拙劣、缓慢的程序。
使用一种专为此类运算设计的语言将大大提性能和易读性。
本文介绍如何将 Java™ 类链接到 XSLT,以执行此类运算。对于每一个 XSLT 处理器来说,XSLT 调用 Java 类的方式都是不同的。本文主要探讨 Apache Foundation 的比较流行的 Xalan XSLT 处理器。
静态方法
从 Xalan 调用时,最简单的 Java 方法就是简单的静态方法。例如,假设您有一个角度表,如 清单 1 所示。
清单 1. 一个包含角度的 XML 文档
<angles units="degrees">
<data>30<data>
<data>45<data>
<data>115<data>
<angles> |
现在,假设您想进行一些简单的处理,比如写出这些角度的正弦值。XSLT 没有正弦函数。您要为正弦函数编码泰勒级数(Taylor series)(参考资料 中提供了关于泰勒级数的更多内容),但调用 Java 方法 Math.sin() 显然要更简单。用 Xalan 从 XSLT 调用一个已有的静态 Java 方法只需三步:
- 将一个名称空间前缀(如
math)映射为形如 xalan://fully.package.qualified.ClassName(如 xalan://java.lang.Math)的 Uniform Resource Identifier(URL)。
- 在根元素
xsl:stylesheet 的 extension-element-prefixes 属性中列出此前缀。
- 使用
prefix:methodName() 这样的形式调用函数,例如 math:sin(.)。
清单 2 描述了此过程。
清单 2. 一个查找清单 1 中角度的正弦值的 Xalan XSLT 样式表
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:math="xalan://java.lang.Math"
extension-element-prefixes="math"
>
<xsl:template match="data" >
<The Math.sin() function expects input in radians
so you have to convert from degrees before calling it. -->
<xsl:value-of select="math:sin(3.1415292 * number(.) div 180.0)" />
<xsl:template>
</xsl:stylesheet> |
在同一个类中,相同的前缀可以引用多个不同的静态方法。例如,清单 3 展示了一个样式表,它生成角度的正弦、余弦和正切值。
清单 3. 一个查找清单 1 中角度的正弦、余弦和正切值的 Xalan XSLT 样式表
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:math="xalan://java.lang.Math"
extension-element-prefixes="math"
>
<xsl:template match="angles" >
<table>
<th><td>Angle<td><td>Sine</td><td>Cosine<td><td>Tangent</td></th>
<xsl:apply-templates select="data"/>
<table>
<xsl:template>
<xsl:template match="data" >
<tr>
<xsl:variable name="radians" select="3.1415292 * number(.) div 180.0"/>
<td><xsl:value-of select="." /><td>
<td><xsl:value-of select="math:sin($radians)" /><td>
<td><xsl:value-of select="math:cos($radians)" /><td>
<td><xsl:value-of select="math:tan($radians)" /></td>
</tr>
<xsl:template>
<xsl:stylesheet>
|
要调用其他类中的静态方法,可将完全限定名称映射到另外一个前缀。然后,在 extension-element-prefixes 属性中列出各前缀,以空格分隔。
这种技术绝非仅限于核心 Java 包中定义的方法。您可通过完全相同的方式调用您自己的类中的静态方法;这些方法可以实现一个 Java 静态方法能实现的一切功能。只要在运行 Xalan 时将类放在您所使用的类路径中即可。
告诫
在开始编写 XSLT 扩展函数之前,您需要了解 Java 编程语言与 XSLT 之间的一个重要差别。XSLT 是一种功能 语言。这有着多方面的含义,其中之一就是它没有全局变量,不会在函数调用间共享状态,也不会导致任何副作用。这些特征使编译器和解释器得以执行在其他情况下无法实现的某些优化。例如,由于函数的输出完全 由其输入和代码决定,因此您无需使用相同的实参多次计算一个函数。如果在您第一次调用 foo(2) 时,它返回了 7,那么第二次、第三次……第三十三次调用它时,依然会返回 7。因此,中间结果可以缓存并重用,而不必重新计算。
Java 语言是命令式的,而非功能语言:它在函数外部缓存状态,函数可能具有副作用。在 Java 代码中,每次调用 foo(2) 时都可能会返回不同的值。
 | |
目前的 Xalan(2.8)通常会如您所愿很好地工作。方法会被重复调用,甚至会使用相同的实参,而且通常是采用最显而易见的顺序。但 Xalan 开发人员计划在将来更进一步地优化转换,因此目前的情况很有可能会发生变化。
|
|
在 XSLT 这样的功能语言中混用非功能性的 Java 代码可能会有违背 XSLT 处理器假设的风险。例如,处理器可能不会在需要时重新计算一个值。它调用方法时的顺序可能与您期望的不同。甚至还可能会超过或少于您期望的调用次数。重申一下,它可能会如您所愿地工作。但无法保证它一定会这样做,尽管如今扩展函数可以解决这方面问题,但在下一个优化模式发生些许变化的发行版中,它可能也会失效。
为了安全地从 XSLT 调用 Java 方法,应使方法尽可能地功能化。您所调用的方法不应依赖于任何对象的易变状态,而应返回可再生产、可预测的结果。仅接受一两个实参、执行计算,并仅依据那些实参返回值的静态方法是理想的。例如,您可安全地从 XSLT 调用 java.lang.Math 类中的大多数方法,例如 Math.sin() 和 Math.exp()。此类中值得注意的特例就是 Math.random(),它会刻意地在您每次调用它时返回一个不同的值。
当然,以 Java 代码编写扩展函数的理由之一就是添加一些特性,像随机数、日期、时间和其他 XSLT 不提供的内容。从 XSLT 调用这些方法是完全可能的。但如果您那样做了,就会注意到,这些方法未能在您期望的时间调用,或者未能按照您期望的次数被调用。
务必谨慎,避免依赖于副作用。一个方法如果实现了未在其返回值中表达出来的功能 —— 例如,写文件,那是非常危险的。如果您这样做了,请设计一个幂等(idempotent)方法:也就是说,无论这个方式是被调用一次还是十次,确保它总是实现相同的功能。还要确保方法返回一个样式表将会用到的值。否则,Xalan 可能会优化掉扩展函数的调用,根本不会调用这个函数。
调用实例方法
根据其定义,实例方法涉及对象状态。而它们时常也会涉及易变的对象状态。因此在编写一个全新的扩展函数时,实例方法并非首选。大多数扩展函数都采用了更好的方式 —— 被编写为静态方法。但有时直接从 XSLT 调用一个已有的实例函数比较方便。
调用一个实例函数与调用一个静态方法类似:将完全限定类名映射到名称空间前缀,然后使用标识类的前缀和标识方法的本地名称来调用方法。但实例方法还需要另外一个数据:在其上调用方法的类的特殊实例。为获得此数据,您必须首先创建一个该类的对象,将其存储在一个变量中。为此,调用 prefix:new() 函数。例如,清单 4 中的代码片段创建了一个 java.awt.Color 对象,并将其存储在了一个名为 blue 的 XSLT 变量中。
清单 4. 从 XSLT 检索一个 Color 对象
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:color="xalan://java.awt.Color"
extension-element-prefixes="color"
>
<xsl:template match="/" >
<xsl:variable name="blue" select="color:new(20, 10, 160)"/>
<... -->
<xsl:template>
</xsl:stylesheet> |
然后,在这个 Color 对象上调用方法时,您将变量引用 $blue 作为第一个实参来传递。例如,清单 5 首先调用 brighter() 方法,创建了一个更浅的蓝色,然后打印其红、绿、蓝分量。
清单 5. 在 Color 对象上调用实例方法
<xsl:template match="/" >
<xsl:variable name="blue"
select="color:new(20 div 256, 10 div 256, 160 div 256)"/>
<xsl:variable name="brighterblue" select="color:brighter($blue)"/>
<red><xsl:value-of select="color:getRed($brighterblue)"/></red>
<green><xsl:value-of select="color:getGreen($brighterblue)"/></green>
<blue><xsl:value-of select="color:getBlue($brighterblue)"/><blue>
</xsl:template>
|
如果该类具有一个无参构造函数,您想使用这个构造函数,然后立即在其上调用一个实例方法,那么有一种捷径。只需像处理静态方法那样调用 prefix:methodName 即可。不必首先显式创建一个对象,再将它存储到一个变量中。
类型匹配
XSLT 使用 XPath 数据模型,具有 4 种基本类型:数字(Java 双精度)、字符串、布尔和节点集(node-set)。(它还添加了第五种类型 —— 结果树片段,但在简单的扩展函数中很少用到。)在 Xalan 向 Java 函数传递实参,并转换 Java 函数返回的结果值时,它会将数字转换为双精度数据、将字符串转换为字符串、将布尔转换为布尔。如果有必要匹配方法签名,那么它还会将这些 XPath 类型转换成相关类型。
例如,遇到 foo(2) 时,Xalan 倾向于调用一个名为 foo 并接受双精度类型实参的方法。但若这样的方法不存在,它就会转而调用 foo(java.lang.Double)、foo(float)、foo(long) 或 foo(int)(按优先选择顺序列出)。如果全部失败,它会查找 foo(short)、foo(char) 和 foo(byte)。如果均未找到,它甚至会尝试将数字转换成字符串,并调用 foo(String)。最后的手段就是查找 foo(Object)。
通常,您不必担心此过程 —— 默认情况下会进行正确的处理。惟一一种常见的问题就是在类同时具有 foo(double) 和 foo(int) 这两种变体时,而且您希望调用的是整型版本。切记,Xalan 总是优先选择双精度类型,只有在无法找到范围更大的类型时,它才会使用接受整型的方法。举个例子,您是否对 清单 5 中的这条指令感到疑惑?
<xsl:variable name="blue"
select="color:new(20 div 256, 10 div 256, 160 div 256)"/> |
或许您会想到,这应该比像下面这样传递整型代码简单:
<xsl:variable name="blue"
select="color:new(20, 10, 160)"/> |
问题在于,尽管 20、10 和 160 在 Java 语言中都是整型值,但在 XSLT 中都是双精度值。这条指令不会调用 Color(int, int, int)。而是调用 Color(float, float, float),它接受的值是 0.0 到 1.0,而不是 0 到 255。在调用此方法之前,您必须重新将颜色值调整到此范围内。
在编写扩展函数时,应谨慎处理根据实参类型进行的方法重载。完全不重载方法要简单得多。
处理节点集的扩展函数编写起来比那些处理简单值的函数更要求技巧,但不是非常困难。Xalan 将节点集转换为 Document Object Model(DOM)NodeIterator、NodeList 和 Node 对象,列出的顺序即优先选择的顺序。如果没有任何函数匹配,它将尝试 String,最终尝试 Object。
其他对象类型无法在 XSLT 中轻松处理。您可从 Java 方法返回这些类型,并将其存储在变量中。但能对此类 Java 对象进行的惟一处理就是将其回传或用它来调用另外一个扩展函数。尽量尝试将您的扩展函数设计为仅使用双精度、字符串、布尔和节点集作为实参和返回值的类型。
如果您希望调用处理其他类型的 Java 方法,请尝试在 Java 程序中编写至少一个额外的适配器函数。这个适配器方法可接受 XPath 类型,将其转换为 Java 类型,然后再调用您实际上要调用的那个方法。随后,方法返回时,适配器方法可将返回值转换为四种 XPath 类型之一,然后再返回转换后的值。在 Java 代码中管理转换要比在 XSLT 中简单得多。
异常处理
XSLT 没有任何类型的异常处理。如果您的扩展函数抛出了一个异常,Xalan 将关闭。例如,清单 6 展示了 清单 5 所示代码的一个有 bug 的版本。
清单 6. 扩展函数抛出异常时
$ java org.apache.xalan.xslt.Process
-IN http://www.cafeaulait.org/ -XSL colors.xsl
ERROR: 'Color parameter outside of expected range: Red Green Blue'
(Location of error unknown)XSLT Error (javax.xml.transform.TransformerException):
java.lang.IllegalArgumentException: Color parameter outside of expected range:
Red Green Blue |
因而,您需要在 Java 方法内实现所有异常处理逻辑。这里的异常是因我的代码中的一个 bug 引起的,修复起来非常容易。如果您调用一个可能抛出检查异常的已有函数,您应编写一个包装器函数,捕获并处理此类异常,然后在 XSLT 中调用包装器方法。
结束语
在转换文档时,XSLT 是一种极其高效的语言,但对于更为传统的任务,比如说在求微分方程的积分或与数据库通信时。幸运的是,您可以在 Java 语言中为此类任务编码,然后使用 Xalan 在您的 XSLT 样式表中调用它们。
参考资料 学习
获得产品和技术
- Xalan 2:下载本文讨论的 XSLT 引擎。
- IBM 试用版软件:使用 IBM 试用版软件构建您的下一个开发项目,这些软件可直接从 developerWorks 下载获得。
讨论
关于作者
对本文的评价
|