You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

STAX与DOM API处理XML属性换行符的差异原因及STAX强制转义换行的方法

STAX与DOM API处理XML属性换行符的差异原因及STAX强制转义换行的方法

嗨,这个问题其实核心是XML规范的属性值规范化(AVNormalize)规则,以及STAX和DOM两个API的设计差异导致的,我给你慢慢拆解:

一、为什么两个API处理换行的行为不一样?

首先得明确XML的属性值处理规则:根据XML规范,属性值里的换行符(\n)、制表符(\t)、回车(\r)会被解析器自动规范化为空格——除非你把它们转成实体引用(比如
代表换行)。

  • DOM API的行为:DOM的设计是严格遵循XML规范的,当你调用setAttribute设置包含换行的属性值时,DOM会自动帮你把换行符转成实体
写入XML文件。这样后续不管用STAX还是DOM解析,都会把
还原成原始的换行符,所以你读出来的是foo\nbar(带换行)。
  • STAX API的行为:STAX是流处理API,它的设计更偏向“所见即所得”的直接输出,默认不会自动对属性值应用AVNormalize规则的转义。当你调用writeAttribute写入foo\nbar时,它会直接把\n作为换行符写入文件,而不是转成实体。但XML解析器读取这个属性时,会按照规范把换行符规范化为空格,所以你读出来的是foo bar(带空格)。

看你代码里的测试结果就很清楚:

  • STAX生成的文件内容是 <element attribute="foo bar"/>,读取时属性值变成了空格分隔的字符串;
  • DOM生成的文件内容是 <element attribute="foo&#10;bar"/>,读取时能完整保留换行。

以下是你的测试代码(已修正转义问题):

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class WriteXml {
    public static void main(String[] args) throws Exception {
        // 返回 "foo SPACE bar" 因为writeWithStax直接写入换行,解析时被规范化为空格
        Path staxFile = writeWithStax("foo\nbar");
        System.out.println("staxFile = " + Files.readString(staxFile));
        System.out.println("attribute read with stax = " + readWithStax(staxFile));
        System.out.println("attribute read with dom = " + readWithDom(staxFile));
        System.out.println();

        // 返回 "foo NEWLINE bar" 因为writeWithDom把换行转义为"&#10;"
        Path domFile = writeWithDom("foo\nbar");
        System.out.println("domFile = " + Files.readString(domFile));
        System.out.println("attribute read with stax = " + readWithStax(domFile));
        System.out.println("attribute read with dom = " + readWithDom(domFile));
        System.out.println();
    }

    private static Path writeWithStax(String text) throws Exception {
        Path file = Files.createTempFile("stax", ".xml");
        try (OutputStream out = Files.newOutputStream(file)) {
            XMLOutputFactory factory = XMLOutputFactory.newInstance();
            XMLStreamWriter writer = factory.createXMLStreamWriter(out);
            writer.writeStartDocument();
            writer.writeStartElement("element");
            writer.writeAttribute("attribute", text);
            writer.writeEndElement();
            writer.writeEndDocument();
            writer.flush();
            writer.close();
        }
        return file;
    }

    private static Path writeWithDom(String text) throws Exception {
        Path file = Files.createTempFile("dom", ".xml");
        try (OutputStream out = Files.newOutputStream(file)) {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.newDocument();
            Element warningElement = document.createElement("element");
            warningElement.setAttribute("attribute", text);
            document.appendChild(warningElement);
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.transform(new DOMSource(document), new StreamResult(out));
        }
        return file;
    }

    private static String readWithStax(Path file) throws Exception {
        try (InputStream in = Files.newInputStream(file)) {
            XMLInputFactory factory = XMLInputFactory.newInstance();
            XMLStreamReader reader = factory.createXMLStreamReader(in);
            while (reader.hasNext()) {
                int event = reader.next();
                if (event == XMLStreamReader.START_ELEMENT && "element".equals(reader.getLocalName())) {
                    return reader.getAttributeValue(null, "attribute");
                }
            }
        }
        return null;
    }

    private static String readWithDom(Path file) throws Exception {
        try (InputStream in = Files.newInputStream(file)) {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(in);
            return document.getDocumentElement().getAttribute("attribute");
        }
    }
}

二、如何让STAX强制转义换行符?

你自己写的那个workaround其实是可行的,本质上是绕过STAX默认的writeAttribute方法,手动按照XML规范处理属性值的换行。我再给你理清楚这个思路的合理性:

你的解决方案(手动拼接属性)

你通过直接操作Writer来手动输出属性的键值对,这样就能自己控制换行符的转义,确保生成合法的XML:

private static Path writeWithStax(String text) throws Exception {
    Path file = Files.createTempFile("stax", ".xml");
    try (Writer out = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
        XMLOutputFactory factory = XMLOutputFactory.newInstance();
        XMLStreamWriter writer = factory.createXMLStreamWriter(out);
        writer.writeStartDocument();
        writer.writeStartElement("element");
        // 手动处理属性,确保空白字符被正确转义
        writeAttributePreservingWhitespace(out, writer, "attribute", text);
        writer.writeEndElement();
        writer.writeEndDocument();
        writer.flush();
        writer.close();
    }
    return file;
}

private static void writeAttributePreservingWhitespace(Writer out, XMLStreamWriter writer, String name, String value) throws XMLStreamException, IOException {
    // 遵循XML规范处理属性值中的空白字符
    if (value.contains("\t") || value.contains("\n") || value.contains("\r")) {
        writer.flush();
        out.write(" ");
        // 写入属性名(确保名称合法)
        out.write(name);
        out.write("=\"");
        // 手动转义特殊字符
        for (char c : value.toCharArray()) {
            switch (c) {
                case '\t':
                    out.write("&#9;");
                    break;
                case '\n':
                    out.write("&#10;");
                    break;
                case '\r':
                    out.write("&#13;");
                    break;
                case '"':
                    out.write("&quot;");
                    break;
                case '&':
                    out.write("&amp;");
                    break;
                default:
                    out.write(c);
            }
        }
        out.write("\"");
    } else {
        // 无特殊空白字符时,使用STAX原生方法
        writer.writeAttribute(name, value);
    }
}

为什么这个方法有效?

因为你直接绕过了STAX的writeAttribute默认逻辑,手动把换行符转成了XML实体&#10;,这样写入的XML文件就和DOM生成的格式一致了。后续不管用STAX还是DOM解析,都能正确还原出原始的换行符。

简化思路:提前转义属性值

如果你不想直接操作Writer,也可以在调用writeAttribute之前,手动把属性值里的特殊字符转义为XML实体:

// 手动转义属性值中的特殊字符
String escapedValue = value.replace("&", "&amp;")
                           .replace("\"", "&quot;")
                           .replace("\n", "&#10;")
                           .replace("\r", "&#13;")
                           .replace("\t", "&#9;");
writer.writeAttribute(name, escapedValue);

这种方式更简洁,但要注意转义顺序(比如先转义&,避免后续转义实体时产生新的&),否则可能生成不合法的XML。

测试验证

修改后的STAX写入方法生成的文件内容会和DOM完全一致,都是<element attribute="foo&#10;bar"/>,此时不管用STAX还是DOM读取,属性值都会还原成foo\nbar(带换行),和DOM的行为完全对齐。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 11:49:32