避免在anyElement上重复使用名称空间定义

时间:2017-02-17 14:26:11

标签: java xml jaxb

当这个对象具有@XmlAnyElement属性时,我首先在解组然后编组对象时遇到奇怪的JAXB命名空间行为。

这里的设置:

package-info.java

@XmlSchema(
    namespace = "http://www.example.org",
    elementFormDefault = XmlNsForm.QUALIFIED,
    xmlns = { @javax.xml.bind.annotation.XmlNs(prefix = "example", namespaceURI = "http://www.example.org") }
)

类型定义:

@XmlRootElement
@XmlType(namespace="http://www.example.org")
public class Message {

    private String id;

    @XmlAnyElement(lax = true)
    private List<Object> any;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public List<Object> getAny() {
        if (any == null) {
            any = new ArrayList<>();
        }
        return this.any;
    }
}

和测试代码本身:

@Test
public void simpleTest() throws JAXBException {

    JAXBContext jaxbContext = JAXBContext.newInstance(Message.class);
    Marshaller marshaller = jaxbContext.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

    String xml =
            "<example:message xmlns:example=\"http://www.example.org\" xmlns:test=\"http://www.test.org\" xmlns:unused=\"http://www.unused.org\">\n" +
            "   <example:id>id-1</example:id>\n" +
            "   <test:value>my-value</test:value>\n" +
            "   <test:value>my-value2</test:value>\n" +
            "</example:message>";
    System.out.println("Source:\n"+xml);

    // parsed
    Object unmarshalled = unmarshaller.unmarshal(new StringReader(xml));

    // directly convert it back
    StringWriter writer = new StringWriter();
    marshaller.marshal(unmarshalled, writer);
    System.out.println("\n\nMarshalled again:\n"+writer.toString());
}

此设置的问题在于所有未知的&#39;名称空间被重复添加到任何元素。

<example:message xmlns:example="http://www.example.org" xmlns:test="http://www.test.org" xmlns:unused="http://www.unused.org">
   <example:id>id-1</example:id>
   <test:value>my-value</test:value>
   <test:value>my-value2</test:value>
</example:message>

成为这个:

<example:message xmlns:example="http://www.example.org">
    <test:value xmlns:test="http://www.test.org" xmlns:unused="http://www.unused.org">my-value</test:value>
    <test:value xmlns:test="http://www.test.org" xmlns:unused="http://www.unused.org">my-value2</test:value>
    <example:id>id-1</example:id>
</example:message>

因此,我该如何避免这种情况!为什么在根元素中定义的命名空间不像输入xml那样?由于anyElement的命名空间不是预先知道的,因此无法通过包定义来注册它...

此外,是否有可能删除未使用的命名空间(按需)?

1 个答案:

答案 0 :(得分:2)

当JAXB开始将对象编组为XML时,它将具有一些上下文,具体取决于对象层次结构中的位置和输出XML。根据定义,它是一个流媒体操作,所以它只会查看此刻正在发生的事情及其当前背景。

所以说它开始整理你的Message个实例。它将检查本地元素名称应该是什么(message),它必须在(http://www.example.org)中的命名空间以及是否有一个绑定到该命名空间的特定前缀(在您的情况下,是的,example前缀)。只要您在Message实例中,它现在就成了上下文的一部分。如果它遇到层次结构中位于同一名称空间内的其他对象,它将在其上下文中使用它并重用相同的前缀,因为它知道它声明了一些父元素或祖先元素。它还检查是否有任何要编组的属性,因此它可以完成开始标记。到目前为止,XML输出如下所示:

<example:message xmlns:example="http://www.example.org">

现在它开始深入挖掘必须编组但不具备属性的字段。它会找到您的List<Object> any字段并开始工作。第一个条目是一些对象,它将被编组到命名空间value中的http://www.test.org元素。该命名空间尚未绑定到当前上下文中的任何前缀,因此会添加该命名空间,并通过package-info注释(或其他一些受支持的方法)找到首选前缀。没有进一步嵌套在需要编组的值中,因此它可以完成该部分,输出现在看起来像这样:

<example:message xmlns:example="http://www.example.org">
    <test:value xmlns:test="http://www.test.org" xmlns:unused="http://www.unused.org">my-value</test:value>

此处第一个列表条目的编组结束,value元素获取其结束标记,并且其上下文到期。到下一个列表条目。它又是一个对象的实例,它再次在同一名称空间中被编组到value,但它在当前上下文中不再具有该对象的实例。所以同样的事情发生了。

<example:message xmlns:example="http://www.example.org">
    <test:value xmlns:test="http://www.test.org" xmlns:unused="http://www.unused.org">my-value</test:value>
    <test:value xmlns:test="http://www.test.org" xmlns:unused="http://www.unused.org">my-value2</test:value>

现在它转到String id字段,该字段与Message位于同一名称空间内。在当前的背景下,这一点仍然是众所周知的,因为我们仍然在消息中。因此,名称空间不会再次声明。

<example:message xmlns:example="http://www.example.org">
    <test:value xmlns:test="http://www.test.org" xmlns:unused="http://www.unused.org">my-value</test:value>
    <test:value xmlns:test="http://www.test.org" xmlns:unused="http://www.unused.org">my-value2</test:value>
    <example:id>id-1</example:id>
</example:message>

那么为什么JAXB只维护一个命名空间列表及其前缀绑定并将它们放在根元素中呢?因为它的流输出。它不能跳回来。如果它在内存中构建DOM,它可能会非常有效。

相反,为什么它不首先遍历其对象树并创建要使用的命名空间绑定列表?再说一次,因为那样效率不高。而且,在处理过程中上下文将如何变化可能根本不完全已知。也许我们最终会在某个包中使用不同的命名空间,但前缀与其他命名空间相同。如果在XML中我们目前还没有绑定任何前缀,那很好。就像这里(注意第二个测试命名空间):

<example:message xmlns:example="http://www.example.org">
    <test:value xmlns:test="http://www.test.org">my-value</test:value>
    <test:value xmlns:test="http://completelydifferenttest">my-value2</test:value>
    <example:id>id-1</example:id>
</example:message>

但在其他情况下,它必须选择一些不同的前缀。像这个语义上等效的文档:

<example:message xmlns:example="http://www.example.org" xmlns:test="http://www.test.org">
    <test:value>my-value</test:value>
    <ns1:value xmlns:ns1="http://completelydifferenttest">my-value2</ns1:value>
    <example:id>id-1</example:id>
</example:message>

所以JAXB只是在当前包含的上下文和本地中查看事物。它并没有预先挖掘。

然而,

然而,这并没有真正解决问题。所以,你可以做什么。

  • 忽略它。尽管它可能是粗略和丑陋的输出,但这是正确的。
  • 在编组后应用XSLT转换以清理命名空间。
  • 使用自定义NamespacePrefixMapper。
  • 将Marshal连接到XMLEventWriter并让它将自定义事件委派给标准编写者。

自定义映射器是一种依赖于JAXB参考实现的解决方案,并使用内部类。因此,它的前向兼容性无法得到保证。 Blaise Doughan在这个答案中解释了它的用法:https://stackoverflow.com/a/28540700/630136

最后一个选项涉及更多。您可以编写一些事件编写器,它在根元素上输出具有默认前缀绑定的所有命名空间,并在后续元素作为已知命名空间时忽略它们。你从一开始就有效地保持了一些全球背景。

XSLT可能是最简单的,但可能需要进行一些实验才能看到XSLT处理器如何处理它。这个实际上对我有用了:

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

    <xsl:template match="node()|@*">
      <xsl:copy>
          <xsl:apply-templates select="node()|@*" />
      </xsl:copy>
    </xsl:template>

    <xsl:template match="/example:message">
        <example:message>
            <xsl:apply-templates select="node()|@*" />
        </example:message>
    </xsl:template>

</xsl:transform>

请注意,如果我在/*的匹配项中转换第二个模板并在那里使用<xsl:copy>方法,那么它在某种程度上无效。

要从一个对象编组并在一个平滑的步骤中转换生成的XML,请查看the JAXBSource class的使用。它允许您使用JAXB对象作为XML转换的源。

编辑:关于&#34;未使用&#34;命名空间。我记得在某些时候在一些JAXB输出中得到了一堆甚至不需要的命名空间,在这种情况下,它被证明与XML上放置在某些类上的@XmlSeeAlso注释有关。我正在使用的Java编译器(起点是XML模式)。注释确保如果将类加载到JAXBContext中,则包含@XmlSeeAlso中引用的类。这可以使上下文的创建更容易。但副作用是它包含了一些我并不总是需要的东西,并且在上下文中并不一定总是需要。我认为JAXB将为它在那时可以找到的所有内容创建名称空间前缀映射。

说到这一点,这实际上可以为您的问题提供另一种解决方案。如果在根类上放置@XmlSeeAlso注释,并引用可能被使用的其他类(或至少是子层次结构的根),那么JAXB可能已经绑定了所遇到的包的所有命名空间在根。我并不总是喜欢注释,因为我不认为超类应该引用实现,而层次结构中较高的类不应该担心其中较低级别的细节。但如果它与你的架构没有冲突,那就值得一试。