XSLT 在服务器端通常用于将 XML 数据转换为 HTML、PDF 或其他结构化文本。与浏览器端转换不同,服务器端转换完全依赖程序运行环境的默认字符集、输入流的解码方式和转换引擎的输出配置。一旦这些环节中的编码声明不一致,最终生成的页面就可能出现乱码、问号甚至抛出异常。要彻底解决这类问题,不能只修改某个 XML 文件头,必须沿着 XML 输入、样式表解析、转换输出以及 HTTP 响应四条链路逐项检查。

编码问题的根源:三处声明不一致
XSLT 转换涉及三个可能影响字符集的声明位置。第一个是源 XML 文档开头的 encoding 属性,例如 <?xml version="1.0" encoding="ISO-8859-1"?>。第二个是 XSL 样式表自身的 encoding 属性,它决定样式表文件如何被解析。第三个是转换引擎输出阶段的编码设置,例如 Java 中的 OutputKeys.ENCODING 或 .NET 中的 XmlWriterSettings.Encoding。这三个位置只要有一个使用平台默认值,而其他位置使用 UTF-8,乱码就可能出现。
更隐蔽的是,XML 解析器对编码声明的处理并不总是完全一致。根据 XML 规范,当文档没有 BOM 且没有 encoding 声明时,解析器默认按 UTF-8 处理;但如果文档以 UTF-16 编码保存却没有 BOM,解析器可能直接报错。服务器端程序经常从数据库、消息队列或网络流中读取 XML,这些来源通常没有物理文件头,字符集完全取决于应用层如何解码。因此必须显式指定字符集,不要依赖解析器的自动猜测。
样式表的编码同样容易被忽略。很多开发者只关注源 XML,却把 XSL 文件保存为 GBK 或系统默认编码。当样式表中包含中文模板或正则表达式时,解析器如果用 UTF-8 去读取 GBK 文件,会导致模板内容损坏。解决思路是让样式表文件统一使用 UTF-8 无 BOM 保存,并在 XSL 头部声明 <xsl:output encoding="UTF-8" />。
Java 平台下的具体表现与修复
Java 中常用的 XSLT 转换 API 基于 TransformerFactory 和 Transformer。一个常见的错误写法是使用 FileInputStream 直接读取 XML,然后用 StreamSource 传给转换器。这样输入流的解码会采用 JVM 默认字符集,在 Linux 服务器上可能不是 UTF-8。正确做法是使用 InputStreamReader 显式指定 UTF-8。
输出阶段的乱码通常来自 PrintStream 或 FileWriter 使用了平台默认编码。即使 Transformer 的编码属性设置为 UTF-8,如果最终写入到 FileWriter,仍然可能被转换成 GBK 或其他编码。推荐使用 ByteArrayOutputStream 作为中间缓冲,再一次性转成 UTF-8 字符串或字节数组返回。
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class XsltEncodingFix {
public static String transform(String xmlPath, String xsltPath) throws Exception {
TransformerFactory factory = TransformerFactory.newInstance();
// 1. 使用 InputStreamReader 显式指定 UTF-8,避免平台默认字符集
InputStreamReader xmlReader = new InputStreamReader(
new FileInputStream(xmlPath), StandardCharsets.UTF_8);
InputStreamReader xsltReader = new InputStreamReader(
new FileInputStream(xsltPath), StandardCharsets.UTF_8);
Source xmlSource = new StreamSource(xmlReader);
Source xsltSource = new StreamSource(xsltReader);
Transformer transformer = factory.newTransformer(xsltSource);
// 2. 显式声明输出编码为 UTF-8
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.METHOD, "html");
// 3. 使用 ByteArrayOutputStream 避免 FileWriter 的默认编码
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Result result = new StreamResult(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
transformer.transform(xmlSource, result);
return outputStream.toString(StandardCharsets.UTF_8.name());
}
}另一个容易忽略的环节是 TransformerFactory 的实现。不同 JDK 或第三方库(如 Xalan、Saxon)对 OutputKeys.ENCODING 的默认值处理不同。如果代码中没有设置该属性,输出编码可能会从 StreamResult 的 Writer 推断。建议在每次创建 Transformer 后都显式设置编码,不要依赖默认行为。
如果转换结果还要通过 HTTP 响应返回,必须同时设置响应头。Java Web 环境中可以使用 response.setCharacterEncoding("UTF-8") 或 response.setContentType("text/html; charset=UTF-8")。如果只设置输出属性而没有设置响应头,浏览器可能会用错误编码解析,导致 UTF-8 字节被误解为其他字符。
.NET 与 PHP 环境的编码处理差异
.NET 使用 XslCompiledTransform 类执行 XSLT 转换。与 Java 类似,输入 XML 与 XSL 的读取方式决定了字符集。推荐使用 XmlReader 并配合 XmlReaderSettings 来读取,因为 XmlReader 可以自动检测 BOM,但对无 BOM 的文档会按声明或 UTF-8 处理。输出阶段应使用 XmlWriter 和 XmlWriterSettings 显式指定 UTF-8。
using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Xsl;
public class XsltEncodingService
{
public static string Transform(string xmlPath, string xsltPath)
{
var transform = new XslCompiledTransform();
// 读取样式表时使用 XmlReader,自动处理 BOM 与编码声明
var xsltSettings = new XmlReaderSettings();
xsltSettings.DtdProcessing = DtdProcessing.Parse;
using (var xsltReader = XmlReader.Create(xsltPath, xsltSettings))
{
transform.Load(xsltReader);
}
var resultSettings = new XmlWriterSettings();
resultSettings.Encoding = Encoding.UTF8;
resultSettings.Indent = true;
using (var stringWriter = new StringWriter())
using (var xmlWriter = XmlWriter.Create(stringWriter, resultSettings))
{
transform.Transform(xmlPath, xmlWriter);
return stringWriter.ToString();
}
}
}PHP 中的 XSLTProcessor 同样需要从 DOMDocument 加载 XML 与 XSL。加载时可以通过 DOMDocument::load 的选项指定编码,但更可靠的方式是确保文件本身为 UTF-8,并在加载后检查 encoding 属性。输出时使用 transformToXML 返回字符串,再根据框架设置响应头。
<?php
$xml = new DOMDocument();
// 加载时让 XML 解析器根据声明自动处理,但文件必须为 UTF-8 无 BOM
$xml->load('data.xml', LIBXML_NOBLANKS);
$xsl = new DOMDocument();
$xsl->load('template.xsl', LIBXML_NOBLANKS);
$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);
// 在转换前设置输出编码
$processor->setParameter('', 'output-encoding', 'UTF-8');
$result = $processor->transformToXML($xml);
header('Content-Type: text/html; charset=UTF-8');
echo $result;
?>PHP 的一个常见坑是 DOMDocument::load 读取 GBK 文件时不会自动转换,而 transformToXML 可能返回字符串后未指定响应头。遇到非 UTF-8 旧系统数据时,应先使用 iconv 或 mb_convert_encoding 将字符串转成 UTF-8,再加载为 DOM,否则中文可能被拆成乱码。
从 HTTP 响应到浏览器显示的最终一致性
即使服务器端转换结果完全正确,如果 HTTP 响应头缺少 charset=UTF-8,浏览器仍可能按默认的 ISO-8859-1 或 Windows-1252 解析。在 Java Servlet 中要同时设置内容类型和字符编码;在 .NET 中应设置 Response.ContentEncoding;在 PHP 中则通过 header 函数输出。响应头应与转换输出属性一致,不能一个声明为 UTF-8,另一个声明为 GBK。
BOM 是另一个容易被忽视的因素。UTF-8 BOM 是文件开头的三个字节 EF BB BF。如果 XSL 样式表带 BOM,某些解析器会把它当作样式表内容的一部分,可能导致输出结果开头出现不可见字符。建议 XSL 与 XML 源文件统一使用 UTF-8 无 BOM 保存。如果必须使用带 BOM 的文件,应确认转换引擎支持并正确处理,同时在输出阶段不要重复添加 BOM。
排查编码问题时,可以按照固定顺序检查:先确认源 XML 文件本身的字节内容是否与其 encoding 声明匹配;再确认样式表文件的编码声明是否与实际保存格式一致;然后检查转换引擎的输出编码属性;最后核实 HTTP 响应头与浏览器显示。每一步都可以通过打印中间字节或使用十六进制编辑器验证。自动化测试中可以强制使用 UTF-8 读取同一份测试数据,并断言输出字符串包含正确的中文字符,这样能尽早发现环境默认字符集导致的回归问题。