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

当此对象具有@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 any; public String getId() { return id; } public void setId(String id) { this.id = id; } public List 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 = "\n" + " id-1\n" + " my-value\n" + " my-value2\n" + ""; 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()); } 

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

  id-1 my-value my-value2  

成为这个:

  my-value my-value2 id-1  

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

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

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

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

  

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

  my-value 

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

  my-value my-value2 

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

  my-value my-value2 id-1  

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

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

  my-value my-value2 id-1  

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

  my-value my-value2 id-1  

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

然而,这并没有真正解决问题。 所以这就是你能做的。

  • 忽略它。 输出,虽然可能是冗长和丑陋的,但是正确的。
  • 在编组后应用XSLT转换来清理命名空间。
  • 使用自定义NamespacePrefixMapper。
  • Marshal到XMLEventWriter并让它将自定义事件委托给标准编写器。

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

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

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

               

请注意,如果我在/*上匹配第二个模板并在那里使用方法,它会以某种方式不起作用。

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

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

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