java - 避免在anyElement上重复命名空间定义

标签 java xml jaxb

当此对象具有@XmlAnyElement属性时,首先解组然后再编组一个对象时,我当前面临一个奇怪的JAXB名称空间行为。

这里的设置:

包信息.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());
}


此设置的问题在于,所有“未知”名称空间都会重复添加到任何元素中。

<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的名称空间是预先未知的,因此无法通过包定义进行注册...

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

最佳答案

当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。
编组到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转换的源。

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

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

关于java - 避免在anyElement上重复命名空间定义,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42300327/

相关文章:

c# - C#中如何向字典中添加数据

java - JAXB 是否存在内存使用问题?

java - Java 中散列 xml 字符串的最佳散列函数

java - 使用要创建的类参数化 JAXB 工厂方法

java - 如何编写抑制具有特定属性值的元素的架构?

java - JTextPane 中是如何实现自动换行的,如何让它在没有空格的情况下换行?

java - 与 glassfish 为多个 web 项目共享 ejb 3

java - 无法使用 SHA-256 和 salt 创建用于密码散列的 Java 方法的 Javascript 模拟

java - Java 1.5 中的 JTable 对行进行排序

java - 当 Xml 不包含标记时,JAXB 返回 null