Apache Xalan 快速入门与集成示例

Xalan-J 2.7.3 快速入门:Maven 工程 pom 全文、含变量循环条件的 XSLT 样例,命令行与 API 双路径转换、-PARAM 参数化、错误处理与 JVM 堆验证,附运行回显。

Apache XalanApache Xalan2026/10/12

Apache Xalan 快速入门与集成示例

目标:10 分钟跑通 Apache Xalan-J 2.7.3 的两条转换路径(命令行 Process 与 Java Transformer API),并掌握 -PARAM 参数化、输出格式/编码控制、错误处理与 JVM 堆验证。全部代码与回显已在 Ubuntu 22.04.5(OpenJDK 17.0.20.1)与 CentOS 8.5.2111(OpenJDK 17.0.1)双环境实测通过。

一、环境与目标

项目值
操作系统Ubuntu 22.04.5 LTS / CentOS Linux 8.5.2111
JDKOpenJDK 17(Xalan-J 2.7.3 兼容 JDK 8+)
Xalan-Jxalan:xalan:2.7.3 + xalan:serializer:2.7.3
工程目录/opt/xalan-demo
执行账号root(密码 q1w2e3r4)

完成后你将得到:一个可复用的 Maven 工程、两套转换路径的运行脚本、错误处理与堆参数验证样板。

二、完整工程搭建(pom 全文)

mkdir -p /opt/xalan-demo/src/main/java/com/example /opt/xalan-demo/data /opt/xalan-demo/out
cd /opt/xalan-demo

pom.xml 全文:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>xalan-demo</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>xalan</groupId>
      <artifactId>xalan</artifactId>
      <version>2.7.3</version>
    </dependency>
    <dependency>
      <groupId>xalan</groupId>
      <artifactId>serializer</artifactId>
      <version>2.7.3</version>
    </dependency>
  </dependencies>
</project>
mvn -B clean package

✅ 实测回显(dependency:tree 关键行):

[INFO] +- xalan:xalan:jar:2.7.3:compile
[INFO] \- xalan:serializer:jar:2.7.3:compile
[INFO] BUILD SUCCESS

⚠️ 实测:xalan:xalan:2.7.3 的 pom 不传递依赖 serializer,两个依赖必须显式声明,缺 serializer 运行时报 NoClassDefFoundError: org/apache/xml/serializer/SerializerTrace。

三、样例数据与样式表

3.1 样例 XML(data/books.xml)

<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
  <book category="编程">
    <title lang="zh">Java 核心技术</title>
    <author>张三</author>
    <price>99.50</price>
  </book>
  <book category="数据库">
    <title lang="zh">MySQL 调优实战</title>
    <author>李四</author>
    <price>79.00</price>
  </book>
  <book category="编程">
    <title lang="zh">Java 并发编程</title>
    <author>王五</author>
    <price>89.90</price>
  </book>
  <book category="网络">
    <title lang="zh">TCP/IP 详解</title>
    <author>赵六</author>
    <price>128.00</price>
  </book>
</bookstore>

3.2 样式表(data/books.xsl)

覆盖变量、循环、条件、排序与数值排版四个 XSLT 1.0 基础能力:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" indent="yes" encoding="UTF-8"/>

  <!-- 命令行 -PARAM keyword=xxx 可覆盖该默认值 -->
  <xsl:param name="keyword" select="'编程'"/>

  <!-- 变量:统计匹配书目数量与全部图书总价 -->
  <xsl:variable name="matched" select="count(/bookstore/book[contains(title, $keyword)])"/>
  <xsl:variable name="total" select="sum(/bookstore/book/price)"/>

  <xsl:template match="/">
    <html>
      <head><title>图书清单</title></head>
      <body>
        <h1>图书清单(关键字:<xsl:value-of select="$keyword"/>)</h1>
        <p>共 <xsl:value-of select="$matched"/> 本匹配,
           全部图书总价:<xsl:value-of select="format-number($total, '#,##0.00')"/> 元</p>
        <table border="1" cellpadding="4">
          <tr><th>书名</th><th>作者</th><th>分类</th><th>价格</th></tr>
          <xsl:for-each select="/bookstore/book[contains(title, $keyword)]">
            <xsl:sort select="price" data-type="number" order="descending"/>
            <tr>
              <td><xsl:value-of select="title"/></td>
              <td><xsl:value-of select="author"/></td>
              <td>
                <xsl:choose>
                  <xsl:when test="@category = '编程'"><b>编程类</b></xsl:when>
                  <xsl:otherwise><xsl:value-of select="@category"/></xsl:otherwise>
                </xsl:choose>
              </td>
              <td align="right"><xsl:value-of select="format-number(price, '0.00')"/></td>
            </tr>
          </xsl:for-each>
        </table>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

3.3 文本输出样式表(data/books-text.xsl)

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="UTF-8"/>
  <xsl:template match="/bookstore">
    <xsl:for-each select="book">
      <xsl:value-of select="title"/> | <xsl:value-of select="author"/> | <xsl:value-of select="price"/>
      <xsl:text>&#10;</xsl:text>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

四、路径一:命令行 Process 变换

cd /opt/xalan-demo
export CP=/root/.m2/repository/xalan/xalan/2.7.3/xalan-2.7.3.jar:/root/.m2/repository/xalan/serializer/2.7.3/serializer-2.7.3.jar
# out 目录必须预先存在(实测 Process 不自动创建输出目录)
java -cp $CP org.apache.xalan.xslt.Process -IN data/books.xml -XSL data/books.xsl -OUT out/cli.html
head -16 out/cli.html

✅ 实测回显:

<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>图书清单</title>
</head>
<body>
<h1>图书清单(关键字:编程)</h1>
<p>共 1 本匹配,
          全部图书总价:396.40 元</p>
<table cellpadding="4" border="1">
<tr>
<th>书名</th><th>作者</th><th>分类</th><th>价格</th>
</tr>
<tr>
<td>Java 并发编程</td><td>王五</td><td><b>编程类</b></td><td align="right">89.90</td>
</tr>

五、路径二:Java Transformer 代码与运行

5.1 完整代码(src/main/java/com/example/XalanDemo.java)

package com.example;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;

/**
 * Xalan-J 快速入门:TransformerFactory + Transformer 完成 XSLT 变换。
 * 用法: java com.example.XalanDemo <xml> <xsl> <out> [paramValue]
 */
public class XalanDemo {
    public static void main(String[] args) throws Exception {
        // 1. 打印当前 JAXP 实际加载的 TransformerFactory 实现类
        TransformerFactory factory = TransformerFactory.newInstance();
        System.out.println("TransformerFactory 实现类: " + factory.getClass().getName());

        // 2. 准备输入源与输出目标
        Source xmlSource = new StreamSource(new File(args[0]));
        Source xslSource = new StreamSource(new File(args[1]));
        Result result = new StreamResult(new File(args[2]));

        // 3. 用样式表编译 Transformer,并设置输出属性与外部参数
        Transformer transformer = factory.newTransformer(xslSource);
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        if (args.length > 3) {
            transformer.setParameter("keyword", args[3]);
        }

        // 4. 执行转换
        transformer.transform(xmlSource, result);
        System.out.println("转换完成, 输出文件: " + args[2]);
    }
}

5.2 编译与运行回显

mvn -B -q package
java -cp "target/classes:$CP" com.example.XalanDemo data/books.xml data/books.xsl out/api.html

✅ 实测回显:

TransformerFactory 实现类: org.apache.xalan.processor.TransformerFactoryImpl
转换完成, 输出文件: out/api.html

💡 默认实现是解释型引擎:xalan.jar 的 META-INF/services/javax.xml.transform.TransformerFactory 声明了 org.apache.xalan.processor.TransformerFactoryImpl。classpath 无 xalan.jar 时 JDK 会退回内置实现 com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl(实测)。

5.3 -PARAM 参数化变换

java -cp "target/classes:$CP" com.example.XalanDemo data/books.xml data/books.xsl out/api-java.html Java
grep -o "共 [0-9]* 本匹配" out/api-java.html

✅ 实测回显:

TransformerFactory 实现类: org.apache.xalan.processor.TransformerFactoryImpl
转换完成, 输出文件: out/api-java.html
共 2 本匹配

切换 XSLTC 编译型引擎(同一份业务代码,仅加 JVM 系统属性):

java -Djavax.xml.transform.TransformerFactory=org.apache.xalan.xsltc.trax.TransformerFactoryImpl \
     -cp "target/classes:$CP" com.example.XalanDemo data/books.xml data/books.xsl out/api-xsltc.html

✅ 实测回显:

TransformerFactory 实现类: org.apache.xalan.xsltc.trax.TransformerFactoryImpl
转换完成, 输出文件: out/api-xsltc.html

六、扩展实践

6.1 输出格式与编码控制

编程接口用 OutputKeys;命令行等价开关为 -XML/-HTML/-TEXT。实测三种格式:

java -cp $CP org.apache.xalan.xslt.Process -IN data/books.xml -XSL data/books.xsl -XML -OUT out/cli-xml.xml
head -4 out/cli-xml.xml
java -cp $CP org.apache.xalan.xslt.Process -IN data/books.xml -XSL data/books-text.xsl -TEXT -OUT out/cli.txt
cat out/cli.txt

✅ 实测回显(XML 格式头部):

<?xml version="1.0" encoding="UTF-8"?><html>
<head>
<title>图书清单</title>
</head>

✅ 实测回显(TEXT 格式全文):

Java 核心技术 | 张三 | 99.50
MySQL 调优实战 | 李四 | 79.00
Java 并发编程 | 王五 | 89.90
TCP/IP 详解 | 赵六 | 128.00

编码要点(实测):XML 源与样式表的解析编码由各自 XML 声明决定(UTF-8 声明 + UTF-8 文件即可,与系统 locale 无关);输出编码由 xsl:output/@encoding 或 OutputKeys.ENCODING 决定;但命令行参数与 stdout 的编码由系统 locale 决定——POSIX locale 下 -PARAM 传中文乱码、Java 程序打印中文变问号,解法 export LANG=C.UTF-8。

6.2 错误处理(try/catch + ErrorListener)

创建 src/main/java/com/example/XalanErrorDemo.java:

package com.example;

import javax.xml.transform.ErrorListener;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;

/**
 * Xalan-J 错误处理示例:try/catch TransformerException + 自定义 ErrorListener。
 * 用法: java com.example.XalanErrorDemo <xml> <xsl> <out>
 */
public class XalanErrorDemo {
    public static void main(String[] args) {
        try {
            TransformerFactory factory = TransformerFactory.newInstance();
            // 自定义错误监听器:把样式表编译告警/错误打上统一前缀
            factory.setErrorListener(new ErrorListener() {
                public void warning(TransformerException e) {
                    System.out.println("[样式表告警] " + e.getMessage());
                }
                public void error(TransformerException e) throws TransformerException {
                    System.out.println("[样式表错误] " + e.getMessage());
                    throw e;
                }
                public void fatalError(TransformerException e) throws TransformerException {
                    System.out.println("[样式表致命错误] " + e.getMessage());
                    throw e;
                }
            });

            Transformer transformer = factory.newTransformer(new StreamSource(new File(args[1])));
            transformer.transform(new StreamSource(new File(args[0])),
                                  new StreamResult(new File(args[2])));
            System.out.println("转换成功: " + args[2]);
        } catch (TransformerException e) {
            // 统一错误出口:定位到样式表行列号
            System.out.println("捕获 TransformerException: " + e.getMessage());
            if (e.getLocator() != null) {
                System.out.println("出错位置: " + e.getLocator().getSystemId()
                        + " 第 " + e.getLocator().getLineNumber() + " 行 第 "
                        + e.getLocator().getColumnNumber() + " 列");
            }
            Throwable cause = e.getCause();
            if (cause != null) {
                System.out.println("根因: " + cause);
            }
            System.exit(1);
        }
    }
}

构造一个不合法样式表(data/books-bad.xsl,第 6 行 xsl:if 未闭合):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <xsl:if test="count(book) > 0">
      <p>未闭合的 if</p>
  </xsl:template>
</xsl:stylesheet>

运行并观察错误链路:

javac -encoding UTF-8 -cp "$CP" -d target/classes src/main/java/com/example/XalanErrorDemo.java
java -Djavax.xml.transform.TransformerFactory=org.apache.xalan.processor.TransformerFactoryImpl \
     -cp "target/classes:$CP" com.example.XalanErrorDemo data/books.xml data/books-bad.xsl out/bad-api.html
echo "EXIT=$?"

✅ 实测回显:

[Fatal Error] books-bad.xsl:6:5: The element type "xsl:if" must be terminated by the matching end-tag "</xsl:if>".
[样式表致命错误] org.xml.sax.SAXParseException; systemId: file:/opt/xalan-demo/data/books-bad.xsl; lineNumber: 6; columnNumber: 5; The element type "xsl:if" must be terminated by the matching end-tag "</xsl:if>".
[样式表致命错误] javax.xml.transform.TransformerException: org.xml.sax.SAXParseException; systemId: file:/opt/xalan-demo/data/books-bad.xsl; lineNumber: 6; columnNumber: 5; The element type "xsl:if" must be terminated by the matching end-tag "</xsl:if>".
捕获 TransformerException: javax.xml.transform.TransformerException: org.xml.sax.SAXParseException; systemId: file:/opt/xalan-demo/data/books-bad.xsl; lineNumber: 6; columnNumber: 5; The element type "xsl:if" must be terminated by the matching end-tag "</xsl:if>".
根因: javax.xml.transform.TransformerException: org.xml.sax.SAXParseException; systemId: file:/opt/xalan-demo/data/books-bad.xsl; lineNumber: 6; columnNumber: 5; The element type "xsl:if" must be terminated by the matching end-tag "</xsl:if>".
EXIT=1

💡 实测要点:致命解析错误发生在样式表编译阶段,e.getLocator() 在包装异常里可能为空,但消息中携带 lineNumber: 6; columnNumber: 5,可直接定位;生产代码按本例统一在 catch 中收口并 exit(1),让调度系统感知失败。

七、JVM 堆设置与验证

XSLTC 编译大样式表、转换大文档时把堆控制在 256~512MB 起。验证程序 src/main/java/com/example/MemoryDemo.java(打印 maxMemory,转换后保持进程 20 秒供采样):

package com.example;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;

/** 验证 JVM 堆参数: 打印 maxMemory, 转换后保持进程存活 20 秒供 ps/jps 采样 */
public class MemoryDemo {
    public static void main(String[] args) throws Exception {
        long max = Runtime.getRuntime().maxMemory();
        System.out.println("JVM 最大堆(maxMemory): " + max / 1048576 + " MB");
        TransformerFactory factory = TransformerFactory.newInstance();
        Transformer t = factory.newTransformer(new StreamSource(new File(args[1])));
        t.transform(new StreamSource(new File(args[0])), new StreamResult(new File(args[2])));
        System.out.println("大文件转换完成, 进程保持 20 秒供 ps/jps 采样");
        Thread.sleep(20000);
    }
}
javac -encoding UTF-8 -cp "$CP" -d target/classes src/main/java/com/example/MemoryDemo.java
# 后台启动带堆参数的进程
nohup java -Xms256m -Xmx512m -cp "target/classes:$CP" com.example.MemoryDemo data/big.xml data/books.xsl out/mem.html > out/mem.log 2>&1 &
sleep 4
ps -ef | grep [j]ava
jps -v
wait %1 2>/dev/null
cat out/mem.log

✅ 实测回显(ps -ef | grep [j]ava,Ubuntu 22.04):

root      6757  6724 99 14:36 ?  00:00:04 java -Xms256m -Xmx512m -cp target/classes:/root/.m2/repository/xalan/xalan/2.7.3/xalan-2.7.3.jar:/root/.m2/repository/xalan/serializer/2.7.3/serializer-2.7.3.jar com.example.MemoryDemo data/big.xml data/books.xsl out/mem.html

✅ 实测回显(jps -v):

6757 MemoryDemo -Xms256m -Xmx512m

✅ 实测回显(out/mem.log):

JVM 最大堆(maxMemory): 512 MB
大文件转换完成, 进程保持 20 秒供 ps/jps 采样

Maven 构建 JVM 单独用 MAVEN_OPTS 控制(放环境变量、/etc/profile.d/maven.sh 或 ~/.mavenrc):

MAVEN_OPTS="-Xms256m -Xmx512m" mvn -B clean package

构建进程存活期间 ps -ef | grep [j]ava 实测回显(截取关键段):

✅ 实测回显(Ubuntu 22.04 / Maven 3.6.3):

java -Xms256m -Xmx512m -classpath /usr/share/maven/boot/plexus-classworlds-2.x.jar -Dclassworlds.conf=/usr/share/maven/bin/m2.conf -Dmaven.home=/usr/share/maven -Dlibrary.jansi.path=/usr/share/maven/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/opt/xalan-demo org.codehaus.plexus.classworlds.launcher.Launcher -B clean package

💡 实测提醒:命令行 java 进程不读取 MAVEN_OPTS/JAVA_OPTS,必须直接写 -Xms256m -Xmx512m;两个变量只被 mvn、Tomcat 等包装脚本消费。

八、常见问题

Q1: -PARAM 传中文参数后输出乱码、匹配数变 0?

答:系统 locale 非 UTF-8 所致。实测在 POSIX locale 下 -PARAM keyword 数据库 乱码,export LANG=C.UTF-8 后同一命令输出 <h1>图书清单(关键字:数据库)</h1> 正常。

Q2: 运行报 NoClassDefFoundError: org/apache/xml/serializer/SerializerTrace?

答:缺 serializer jar。确认 pom 显式声明 xalan:serializer:2.7.3 且 -cp 包含两个 jar(2.7.3 的 xalan 不传递该依赖)。

Q3: newTransformer 报样式表解析错误,如何快速定位?

答:异常消息带 lineNumber/columnNumber(实测定位到第 6 行第 5 列),或接 ErrorListener 在编译期统一打印;命令行场景可加 -EDUMP 文件名 生成转储(注意实测转储记录的是收尾路径二次异常,真正的解析错误看 stderr 第一条)。

Q4: 输出 HTML 想要 XML 声明/纯文本怎么办?

答:样式表 <xsl:output method="..."/> 或代码 setOutputProperty(OutputKeys.METHOD, "xml|html|text");命令行强制用 -XML/-HTML/-TEXT 开关(实测三格式输出见 6.1 节)。

Q5: 大文件转换中途被 OOM 杀掉?

答:给 java 进程加 -Xms256m -Xmx512m 并用 jps -v 验证生效(实测回显见第七节);数据量更大时按 4 倍峰值数据量估算堆上限。

Q6: 同一工程里解释型与 XSLTC 可以按请求混用吗?

答:可以。两个工厂类独立,分别 TransformerFactory.newInstance()(受系统属性影响)或直接 new org.apache.xalan.xsltc.trax.TransformerFactoryImpl() 实例化,互不干扰;实测两种实现同一样式表输出一致。

九、参考链接