javax.xml.transform.Transformer 行尾不再遵循系统 属性 "line.separator"

javax.xml.transform.Transformer line endings no longer respect system property "line.separator"

How to control line endings that javax.xml.transform.Transformer creates? 的评论建议设置系统 属性 "line.separator"。这在 Java 8 (Oracle JDK 1.8.0_171) 中对我有用(并且对于我手头的任务是可以接受的),但在 Java 11 (openjdk 11.0. 1).

根据票证 XALANJ-2137 我做了一个(未受过教育,因为我什至不知道我使用的是哪个 javax.xml 实现)猜测尝试 setOutputProperty("{http://xml.apache.org/xslt}line-separator", ..) 或者 setOutputProperty("{http://xml.apache.org/xalan}line-separator", ..),但都不起作用。

如何控制Java11中变压器的断线?

这是一些演示代码,它在 Windows 和 Java 11 下打印“... #13 #10 ...”,它应该打印“... #10 ... " 仅。

package test.xml;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.stream.Collectors;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;


public class TestXmlTransformerLineSeparator {
    public static void main(String[] args) throws Exception {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><foo/></root>";
        final String lineSep = "\n";

        String oldLineSep = System.setProperty("line.separator", lineSep);
        try {
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
            transformer.setOutputProperty("{http://xml.apache.org/xalan}line-separator", lineSep);
            transformer.setOutputProperty("{http://xml.apache.org/xslt}line-separator", lineSep);

            StreamSource source = new StreamSource(new StringReader(xml));
            StringWriter writer = new StringWriter();
            StreamResult target = new StreamResult(writer);

            transformer.transform(source, target);

            System.out.println(writer.toString().chars().mapToObj(c -> c <= ' ' ? "#" + c : "" + Character.valueOf((char) c))
                    .collect(Collectors.joining(" ")));
            System.out.println(writer);
        } finally {
            System.setProperty("line.separator", oldLineSep);
        }
    }
}

据我所知,您可以控制 Java 接口的默认 Java 实现在 Java 11 中使用的行分隔符的唯一方法是设置line.separator 属性 在 Java 命令行上。对于这里的简单示例程序,您可以通过创建一个名为 javaArgs reading

的文本文件来实现
-Dline.separator="\n"

并用命令行执行程序

java @javaArgs TestXmlTransformerLineSeparator

Java9 中引入的@ 语法在这里很有用,因为@-文件的解析方式会将“\n”转换为 LF 行分隔符。没有 @-file 也可以完成同样的事情,但我所知道的唯一方法需要更复杂的 OS-dependent 语法来定义一个变量,该变量包含您想要的行分隔符并具有 java 命令行展开变量。

如果您想要的行分隔符是 CRLF,那么 javaArgs 文件将改为读取

-Dline.separator="\r\n"

在较大的程序中,更改整个应用程序的 line.separator 变量可能是不可接受的。为了避免为整个应用程序设置 line.separator,可以使用刚才讨论的命令行启动一个单独的 Java 进程,但是启动进程和与单独进程通信的开销要转移Transformer 应该写入流的数据可能会使它成为一个不受欢迎的解决方案。

所以实际上,更好的解决方案可能是实现一个 FilterWriter 来过滤输出流以将行分隔符转换为您想要的行分隔符。此解决方案不会更改变压器本身使用的线路分隔符,并且可能被视为 post-processing 变压器的结果,因此从某种意义上说,它不是对您的特定问题的答案,但我认为它确实给出了所需的结果没有很多开销。下面是一个使用 FilterWriter 从输出 writer 中删除所有 CR 字符(即回车符 returns)的示例。

import java.io.FilterWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.stream.Collectors;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;


public class TransformWithFilter {

    private static class RemoveCRFilterWriter extends FilterWriter {

        RemoveCRFilterWriter(Writer wrappedWriter) {
            super(wrappedWriter);
        }

        @Override
        public void write(int c) throws IOException {
            if (c != (int)('\r')) {
                super.write(c);
            }
        }

        @Override
        public void write(char[] cbuf, int offset, int length) throws IOException {
            int localOffset = offset;
            for (int i = localOffset; i < offset + length; ++i) {
                if (cbuf[i] == '\r') {
                    if (i > localOffset) {
                        super.write(cbuf, localOffset, i - localOffset);
                    }
                    localOffset = i + 1;
                }
            }
            if (localOffset < offset + length) {
                super.write(cbuf, localOffset, offset + length - localOffset);
            }
        }

        @Override
        public void write(String str, int offset, int length) throws IOException {
            int localOffset = offset;
            for (int i = localOffset; i < offset + length; ++i) {
                if (str.charAt(i) == '\r') {
                    if (i > localOffset) {
                        super.write(str, localOffset, i - localOffset);
                    }
                    localOffset = i + 1;
                }
            }
            if (localOffset < offset + length) {
                super.write(str, localOffset, offset + length - localOffset);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><foo/></root>";
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

        StreamSource source = new StreamSource(new StringReader(xml));
        StringWriter stringWriter = new StringWriter();
        FilterWriter writer = new RemoveCRFilterWriter(stringWriter);
        StreamResult target = new StreamResult(writer);

        transformer.transform(source, target);

        System.out.println(stringWriter.toString().chars().mapToObj(c -> c <= ' ' ? "#" + c : "" + Character.valueOf((char) c))
                .collect(Collectors.joining(" ")));
        System.out.println(stringWriter);
    }
}

序列化 XML 问题的另一个实用解决方案是通过使用 Transformer 获得 [=20] 来获得 XML 的 DOM 表示=] 或直接解析为 DOM 并用 LSSerializer 写出 DOM,这为设置行分隔符提供了明确的支持。由于这不再使用 Transformer 并且 Stack Overflow 上还有其他示例,因此我不会在这里进一步讨论它。

不过,回顾一下 Java 11 中的更改以及为什么我认为没有其他方法可以控制 Java 默认实现所使用的行分隔符的原因Transformer。 Java 对 Transformer 接口的默认实现使用继承自 com.sun.org.apache.xml.internal.serializer.ToStreamToXMLStream class 并在同一个包中实现。查看 OpenJDK 的提交历史,我发现 src/java.xml/share/classes/com/sun/org/apache/xml/internal/serializer/ToStream.java 已更改 here,从读取系统属性中当前定义的 line.separator 属性 改为读取 System.lineSeparator() ,对应于 Java 虚拟机初始化时的行分隔符。此提交首次发布于 Java 11,因此问题中的代码的行为应与 Java 8 中的行为相同,直至并包括 Java 10。

如果你花一些时间阅读 ToStream.java,因为它在提交后存在,它改变了行分隔符的读取方式(可访问 here),特别关注第 135 到 140 行和第 508 到 514 行,你会注意到序列化器实现确实支持使用其他行分隔符,事实上,输出 属性 标识为

{http://xml.apache.org/xalan}line-separator

应该是一种控制使用哪个行分隔符的方法。

那么,为什么问题中的示例不起作用?回答:在当前 Java 默认实现的 Transformer 接口中,只有少数特定的用户设置的属性被传输到序列化程序。这些主要是 XSLT 规范中定义的属性,但特殊的 indent-amount 属性 也被转移。但是,行分隔符输出 属性 不是传输到序列化程序的属性之一。

使用 setOutputProperty 在 Transformer 本身上显式设置的输出属性通过 com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl 第 1029-1128 行定义的 setOutputProperties 方法传输到序列化程序(可访问 here). If you instead define an explicit XSLT transform and use its <xsl:output> tag to set the output properties, the properties that are transferred to the serializer are filtered first of all by the parseContents method defined on lines 139-312 of com.sun.org.apache.xalan.internal.xsltc.compiler.Output (accessible here) and filtered again in the transferOutputSettings method defined on lines 671-715 of com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet (accessible here).

总而言之,似乎没有可以在 Transformer 接口的默认 Java 实现上设置的输出 属性 来控制它使用的行分隔符.很可能还有其他 Transformer 实现的提供者确实提供了对行分隔符的控制,但除了默认实现之外,我对 Java 11 中 Transformer 接口的任何实现没有任何经验随 OpenJDK 版本提供。