【问题标题】:Avoid repeated namespace definitions on anyElement避免在 anyElement 上重复命名空间定义
【发布时间】:2017-02-18 00:27:22
【问题描述】:

当对象具有@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 的命名空间是未知的,因此无法通过包定义注册它...

此外,未使用的命名空间是否也可能被剥离(按需)?

【问题讨论】:

    标签: java xml jaxb


    【解决方案1】:

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

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

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

    现在它开始挖掘必须编组但不是属性的字段。它会找到您的 List&lt;Object&gt; any 字段并开始工作。第一个条目是一些对象,它将被编组到命名空间http://www.test.org 中的value 元素。该命名空间在当前上下文中尚未绑定到任何前缀,因此它被添加,并且通过 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,再次在同一个命名空间中,但它在当前上下文中不再具有该实例。所以同样的事情也会发生。

    <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>
    

    请注意,如果我在 /* 的匹配项中打开第二个模板并在那里使用 &lt;xsl:copy&gt; 方法,它不知何故不起作用。

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

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

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

    【讨论】:

    • 哇。很好的答案。谢谢! XSLT 方法看起来很诱人,但我认为加上未知的命名空间,这也可能会变得非常丑陋......自定义映射器会很好,但不幸的是我没有使用参考实现(只是痛苦 JDK no deps)。因此,我认为我必须忍受丑陋。 一个最后一个问题:为什么在第 2 步中添加了“未使用”的 NS?不需要...编组器是否只是添加所有 remaining 命名空间,因为它不知道会发生什么?
    • @Ingo 如果您使用的是标准 Java 发行版,那么您可能已经在使用参考实现。当然,为了真正能够导入内部命名空间中的类,你需要一些依赖,但它可以被认为是“提供的”。至于“未使用”的命名空间,我知道我忘记了一些东西!即将进行编辑。
    • 再次...感谢您的快速响应和更新。如您所见,我没有任何@XmlSeeAlso 参考,因此,我不知道为什么会发生这种情况。 any 元素实际上是一个扩展,因此其他人可以自己添加内容。没有模式,没有一代,什么都没有。只是一个扩展点。好的...那么 他们 应该只发送他们在 xml 中真正使用的命名空间声明。我们这边没有清理。 ;-)
    • 关于参考实现...您也在这里。但不知何故我无法执行marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper());。我得到一个javax.xml.bind.PropertyException。但我认为这个问题最好放在单独的线程中。
    猜你喜欢
    • 1970-01-01
    • 2011-02-19
    • 1970-01-01
    • 2019-09-03
    • 2016-02-18
    • 1970-01-01
    • 2011-05-14
    • 1970-01-01
    • 2012-03-05
    相关资源
    最近更新 更多