有一种方法给 XSLT 中变量进行赋值,是保持状态的那种

在 XSLT 中声明变量可以用 <xsl:variable> 和 <xsl:param>,它们的区别是可以通过 <xsl:param> 从外部向 XSLT 文件传参数,除此之外,在 xslt 内部使用时这两者的用法基本是一样的。下面只以 <xsl:variable> 为例子,例子中的 xsl:variable 替换成 xsl:param 也是能 run 的。


<xsl:variable> 的基本用法是:
1<xsl:variable name="username" select="'Initial'"/> <!-- 不写 select 则默认为 '' -->
2
3<xsl:variable name="username" select="'New Value'"/> <!-- 赋值 -->
4
5<xsl:value-of select="$username"/> <!-- 显示变量值,变量名前加上 $ 符号 -->

上面三行同时写在一个 <xsl:template/> 里是没问题的,最后显示出新的值为 'New Value',但是跨了多次模板调用就有问题了,即使是把第一行写在最外层看起来像个全局变量。等会例子会揭示出现像来,先记住一点,在 XSLT 中每次应用模板就像是一次方法调用一样,那好,看个例子吧,有三部分组成,XML、XSLT、XsltTransformer 类,这三个文件都放在 cc/unmi/xslt 包中:

1. test.xml 文件:
 1<?xml version="1.0" encoding="UTF-8"?>
 2<users>
 3    <user>
 4        <name>Unmi</name>
 5        <email>fantasia@sina.com</email>
 6    </user>
 7    <user>
 8        <name>Any</name>
 9        <email>master@unmi.cc</email>
10    </user>
11</users>

2. XsltTransformer.java 文件:
 1package cc.unmi.xslt;
 2
 3import javax.xml.transform.Result;
 4import javax.xml.transform.Source;
 5import javax.xml.transform.Transformer;
 6import javax.xml.transform.TransformerException;
 7import javax.xml.transform.TransformerFactory;
 8import javax.xml.transform.stream.StreamResult;
 9import javax.xml.transform.stream.StreamSource;
10
11/**
12 *
13 * @author Unmi
14 *
15 */
16public class XsltTransformer {
17
18    /**
19     * @param args
20     * @throws TransformerException
21     */
22    public static void main(String[] args) throws TransformerException {
23        TransformerFactory transformerFactory = TransformerFactory.newInstance();
24
25        Source xsltSource = new StreamSource(ClassLoader.getSystemResourceAsStream("cc/unmi/xslt/test_vars.xslt"));
26        Transformer transformer = transformerFactory.newTransformer(xsltSource);
27
28        Source xmlSource = new StreamSource(ClassLoader.getSystemResourceAsStream("cc/unmi/xslt/test.xml"));
29
30        Result outputResult = new StreamResult(System.out);
31
32        transformer.transform(xmlSource,outputResult);
33    }
34}

3. test_vars.xml 文件:
 1<xsl:stylesheet version="2.0"
 2    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 3
 4  <xsl:output encoding="utf-8" method="text"/>
 5
 6  <!-- define global variable -->
 7  <xsl:variable name="username" select="'Initial'"/>
 8
 9    <xsl:template match="/users" >
10        <xsl:apply-templates select="user"/>
11
12        <!-- show the last username after traversing user nodes -->
13        1, Last username: <xsl:value-of select="$username"/>
14    </xsl:template>
15
16    <xsl:template match="user">
17        <!-- show the last username -->
18        2, Last username: <xsl:value-of select="$username"/>
19
20        <!-- assign name to username -->
21        <xsl:variable name="username" select="name"/>
22        3, Current username: <xsl:value-of select="$username"/>
23    </xsl:template>
24
25</xsl:stylesheet>

执行 XsltTransformer 的输出结果是:

        2, Last username: Initial
     3, Current username: Unmi

        2, Last username: Initial
     3, Current username: Any 

        1, Last username: Initial

分析结果想原因:从 <xsl:apply-templates select="user"/>  到应用模板 <xsl:template match="user">,相当于是一次方法调用,username 像是方法参数,在模板中可以改变 username 的值,但是再次进入时又是初始值,当然在全部遍历完之后,username 还是它的初始值。也就是,尽管 username 声明在最外层,看起来像个全局变量,但是却无法在模板中改变它的值。所以你不能像写通常的 Java 程序那样,设个全局标志,在某个方法中进行赋值,最后作为条件来判断。

看著名的 W3 Schooll 中关于 XSLT <xsl:variable> 元素 的注释:  一旦您设置了变量的值,就无法改变或修改该值!

比如我们有碰到下面应用场景的时候:
 1<!-- 标识 'Unmi' 是否存在来做些什么 -->
 2<xsl:variable name="exists" select="false()"/>
 3
 4  <xsl:template match="/users" >
 5      <xsl:apply-templates select="user"/>
 6
 7      <xsl:if test="$exists">
 8          <!-- do something here -->
 9      </xsl:if>
10  </xsl:template>
11
12  <xsl:template match="user">
13      <xsl:if test="name='Unmi' and not($exists)">
14          <xsl:variable name="exists" select="true()"/>
15      </xsl:if>
16  </xsl:template>

显然,通过 <xsl:variable> 是做不到的,因为从模板中出来之后变量又变回去了。

要实现上面的功能,我们需要走别的路子了,试图找到一个真正全局的地方,能保存执行过程中的状态。想啊想啊,我们知道在 XSLT 中可以调用 JS/C#/Java 的方法,最简单的是调用静态方法,那么是否可以调用实例方法呢?如果可行的话,就可以在每次执行 XSLT 时绑定一个 Java 实例(JS/C# 的情况用到时再研究),以此 Java 实例作为数据容器,那么对它其中变量的改变就能够记录下来了。

这种方法确实是可行的,关于 XSLT 中调用 Java 方法的方式有好几种,具体步骤请参考:简单的 Xalan 扩展函数,这里直接用例子说明,如何用 Java 实例来保存 XSLT 所需的变量

这里我们仍然使用前面的 test.xml 和 XsltTransformer.java 文件,但是需要修改 test_vars.xstl 文件,以及创建一个 StatusHolder 来存放 XSLT 中的变量值。

StatusHolder.java:
 1package cc.unmi.xslt;
 2
 3/**
 4 * a variabe container
 5 * @author Unmi
 6 */
 7public class StatusHolder {
 8
 9    private String param1;
10    private String param2;
11
12    public String getParam1() {
13        return param1;
14    }
15    public void setParam1(String param1) {
16        this.param1 = param1;
17    }
18    public String getParam2() {
19        return param2;
20    }
21    public void setParam2(String param2) {
22        this.param2 = param2;
23    }
24}

修改后的 test_vars.xslt 文件:
 1<xsl:stylesheet version="1.0"
 2    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 3    xmlns:holder="xalan://cc.unmi.xslt.StatusHolder"
 4    extension-element-prefixes="holder">
 5
 6  <xsl:output encoding="utf-8" method="text"/>
 7
 8  <xsl:variable name="statusHolder" select="holder:new()"/>
 9
10  <!-- define global variable -->
11  <xsl:variable name="username" select="holder:getParam1($statusHolder)"/>
12
13    <xsl:template match="/users" >
14        <xsl:apply-templates select="user"/>
15
16        <!-- show the last username after traversing user nodes -->
17        1, Last username: <xsl:value-of select="holder:getParam1($statusHolder)"/>
18    </xsl:template>
19
20    <xsl:template match="user">
21        <!-- show the last username -->
22        2, Last username: <xsl:value-of select="holder:getParam1($statusHolder)"/>
23
24     <!-- use xsl:value-of to call method setParam1() -->
25     <xsl:value-of select="holder:setParam1($statusHolder,name)"/>
26     3, Current username: <xsl:value-of select="holder:getParam1($statusHolder)"/>
27    </xsl:template>
28
29</xsl:stylesheet>

再次执行 XsltTransformer,控制台输出是:

        2, Last username:
     3, Current username: Unmi

        2, Last username: Unmi
     3, Current username: Any 

        1, Last username: Any

我想这应该是我们所期待的结局。

关键性说明:

1. TransformerFactory.newInstance().newTransformer() 默认使用的就是 Xalan 的转换器
2. 调用 new() 来创建实例的,也可以 new('a', 'b') 来调用相应的有参构造函数
3. 调用实例方法时,第一个参数是实例本身,像调用谢方法一样
4. 可用 <xsl:value-of select="holder:setParam1($statusHolder,'abc')"/> 来调用 setParam1() 方法,尽管该方法的输出为 void。
5. 参数支持 4 种基本类型:数字(Java 双精度)、字符串、布尔和节点集(node-set);如 foo(2) 时,Xalan 倾向于调用方法的顺序是 foo(double)、foo(float)、foo(long) foo(int)、foo(short)、foo(char) 和 foo(byte)。
6. 当被调用方法产生异常时,直接引起 Xalan 的关闭,而且异常栈里有时很难发现问题,所以应该在被调用方法中处理好异常,如有异常时输出信息,不要往外抛。
7. 最后,可以设计一个更好的状态容器,或叫做变量容器,比如用 Map 来保存状态值,而不是一味的罗列 param1 ... paramN。

参考:1. 简单的 Xalan 扩展函数 永久链接 https://yanbin.blog/xslt-assign-variables/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。