【问题标题】:javax.xml.transform.Transformer line endings no longer respect system property "line.separator"javax.xml.transform.Transformer 行尾不再尊重系统属性“line.separator”
【发布时间】:2018-12-03 14:51:14
【问题描述】:

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", ..),但两者都不起作用。

如何在 Java 11 中控制转换器的换行符?

这里有一些演示代码,在带有 Java 11 的 Windows 下打印“... #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 xml pretty-print


【解决方案1】:

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

-Dline.separator="\n"

并使用命令行执行程序

java @javaArgs TestXmlTransformerLineSeparator

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

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

-Dline.separator="\r\n"

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

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

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 来获得一个DOMResult 或通过直接解析成一个 DOM 并用 @987654336 写出 DOM 来获得 XML 的 DOM 表示@,它为设置行分隔符提供了明确的支持。由于这不再使用Transformer 并且在 Stack Overflow 上还有其他示例,因此我不会在这里进一步讨论。

不过,可能有用的是回顾 Java 11 中的变化以及为什么我认为没有其他方法可以控制 Java 的默认实现 Transformer 使用的行分隔符。 Java 对Transformer 接口的默认实现使用继承自com.sun.org.apache.xml.internal.serializer.ToStreamToXMLStream 类,并在同一个包中实现。查看OpenJDK的提交历史,我发现src/java.xml/share/classes/com/sun/org/apache/xml/internal/serializer/ToStream.java从读取系统属性中当前定义的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

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

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

使用setOutputProperty 在Transformer 本身上显式设置的输出属性通过com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl 的第1029-1128 行定义的setOutputProperties 方法传输到序列化程序(可访问here)。如果您改为定义显式 XSLT 转换并使用其 &lt;xsl:output&gt; 标记来设置输出属性,则传输到序列化程序的属性首先由 com.sun.org.apache.xalan.internal.xsltc.compiler.Output 的第 139-312 行定义的 parseContents 方法过滤(可访问here)并在com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet(可访问here)的第671-715行定义的transferOutputSettings方法中再次过滤。

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

【讨论】:

  • 谢谢,出色的分析!我认为最佳解决方案还将保留 CDATA 部分等中可能不同的 EOL,而 line.separator 和 FilterWriter 方法似乎没有。所以我就保持原样,依赖 git 的 autocrlf 转换。
猜你喜欢
  • 2011-01-11
  • 1970-01-01
  • 1970-01-01
  • 2016-09-23
  • 2021-11-12
  • 1970-01-01
  • 1970-01-01
  • 2020-07-04
  • 2017-07-23
相关资源
最近更新 更多