JAXB - 使用不同的名称空间加载 XML 文件

JAXB - Load XML file with different namespaces

我需要加载一个 XML 文件,但存在两种相同格式的文件,除了命名空间不同 - 在我的简化示例中, apple:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:container xmlns:ns2="apple">
</ns2:container>

pear:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:container xmlns:ns2="pear">
</ns2:container>

XmlRootElement 引用了特定的命名空间,因此我无法以相同的方式处理这两个文件:

public class NamespaceTest {
    @XmlRootElement(namespace = "apple")
    public static class Container {
    }

    public static void main(final String[] args) throws Exception {
        // Correct namespace - works
        unmarshall("""
                <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
                <ns2:container xmlns:ns2="apple">
                </ns2:container>
            """);

        // Incorrect namespace - doesn't work
        unmarshall("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <ns2:container xmlns:ns2="pear">
            </ns2:container>
            """);
        }

        private static void unmarshall(final String xml) throws Exception {
            try (Reader reader = new StringReader(xml)) {
                System.out.println(JAXBContext.newInstance(Container.class).createUnmarshaller().unmarshal(reader));
            }
        }
    }
}

给出输出:

com.my.app.NameSpaceTest$Container@77167fb7
Exception in thread "main" javax.xml.bind.UnmarshalException: unexpected element (uri:"pear", local:"container"). Expected elements are <{apple}container>

目前,我通过使用 修改正在读取的数据,以一种次优的方式工作——但如果可能的话,我想将其移至 JAXB 中。

public class NameSpaceTest {
    @XmlRootElement(namespace = "apple")
    public static class Container {
    }

    public static void main(final String[] args) throws Exception {
        // Correct namespace
        unmarshall("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <ns2:container xmlns:ns2="apple">
            </ns2:container>
            """);

        // Incorrect namespace
        unmarshall("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <ns2:container xmlns:ns2="pear">
            </ns2:container>
            """);
        }

        private static void unmarshall(final String xml) throws Exception {
        try (Reader reader = new TranslatingReader(new BufferedReader(new StringReader(xml))) {
            @Override
            public String translate(final String line) {
            return line.replace("pear", "apple");
            }
        }) {
            System.out.println(JAXBContext.newInstance(Container.class).createUnmarshaller().unmarshal(reader));
        }
    }

    /** @see <a href=" */
    private abstract static class TranslatingReader extends Reader {
        private final BufferedReader input;
        private StringReader output = new StringReader("");

        public TranslatingReader(final BufferedReader input) {
            this.input = input;
        }

        public abstract String translate(final String line);

        @Override
        public int read(final char[] cbuf, int off, int len) throws IOException {
            int read = 0;

            while (len > 0) {
            final int nchars = output.read(cbuf, off, len);

            if (nchars == -1) {
                final String line = input.readLine();

                if (line == null) {
                break;
                } else {
                output = new StringReader(translate(line) + System.lineSeparator());
                }
            } else {
                read += nchars;
                off += nchars;
                len -= nchars;
            }
            }

            if (read == 0) {
            read = -1;
            }

            return read;
        }

        @Override
        public void close() throws IOException {
            input.close();
            output.close();
        }
    }
}

输出:

com.my.app.NameSpaceTest$Container@6ce139a4
com.my.app.NameSpaceTest$Container@18ce0030

在读取数据时过滤数据是正确的方法(JAXB,或者一般的数据绑定,如果您必须处理词汇表的版本和变体,则不是理想的技术选择)。但是使用 SAX 过滤器过滤它,而不是在流级别。

或者,在使用 JAXB 处理数据之前使用 XSLT 转换规范化数据。

一种选择是使用自定义 org.xml.sax.ContentHandler,它在委托给 jaxb 的“正常”Content Handler 之前重写命名空间的 sax 事件。

这里有一个独立的例子:

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

public class JaxbSaxRewriteNamespaceExample {

    @XmlRootElement(name = "container", namespace = "apple")
    @XmlAccessorType(XmlAccessType.NONE)
    static class Container {

        @XmlAttribute(namespace = "apple")
        private String attribute;
        @XmlElement(namespace = "apple")
        private String element;

        public String getAttribute() {
            return attribute;
        }

        public String getElement() {
            return element;
        }
    }

    public static void main(String[] args) throws Exception {
        String orangeXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n"
                + "<ns2:container xmlns:ns2=\"apple\" ns2:attribute=\"oranges\"><ns2:element>Orange Element</ns2:element>\r\n"
                + "</ns2:container>";
        String appleXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n"
                + "<ns2:container xmlns:ns2=\"apple\" ns2:attribute=\"apples\"><ns2:element>Apple Element</ns2:element>\r\n"
                + "</ns2:container>";

        JAXBContext jc = JAXBContext.newInstance(Container.class);

        Container orange = read(jc, orangeXml, Collections.singletonMap("orange", "apple"));
        Container apple = read(jc, appleXml, Collections.emptyMap());
        System.out.println(orange.getAttribute());
        System.out.println(orange.getElement());

        System.out.println(apple.getAttribute());
        System.out.println(apple.getElement());

    }

    private static Container read(JAXBContext jc, String xml, Map<String, String> namespaceMapping) throws Exception {
        UnmarshallerHandler unmarshallerHandler = jc.createUnmarshaller().getUnmarshallerHandler();

        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setNamespaceAware(true); // Make sure sax parser is namespace aware

        SAXParser sp = spf.newSAXParser();
        XMLReader xr = sp.getXMLReader();
        // Wrap the Jaxb ContentHandler with the custome NamespaceRenamer
        xr.setContentHandler(new RenameNamespaceContentHandler(unmarshallerHandler, namespaceMapping));

        // See javadoc of InputSource for more options to pass in data, e.g. InputStream
        InputSource inputSource = new InputSource(new StringReader(xml)); //
        xr.parse(inputSource);
        return (Container) unmarshallerHandler.getResult();
    }

    public static class RenameNamespaceContentHandler implements ContentHandler {

        private final ContentHandler delegate;

        private final Map<String, String> namespaceMapping;

        public RenameNamespaceContentHandler(ContentHandler delegate, Map<String, String> namespaceMapping) {
            this.delegate = delegate;
            this.namespaceMapping = namespaceMapping;
        }

        @Override
        public void setDocumentLocator(Locator locator) {
            delegate.setDocumentLocator(locator);
        }

        @Override
        public void startDocument() throws SAXException {
            delegate.startDocument();
        }

        @Override
        public void endDocument() throws SAXException {
            delegate.endDocument();
        }

        @Override
        public void startPrefixMapping(String prefix, String uri) throws SAXException {
            if (namespaceMapping.containsKey(uri)) {
                delegate.startPrefixMapping(prefix, namespaceMapping.get(uri));
            }
            delegate.startPrefixMapping(prefix, uri);
        }

        @Override
        public void endPrefixMapping(String prefix) throws SAXException {
            delegate.endPrefixMapping(prefix);
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
            delegate.startElement(uri, localName, qName, atts);
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            delegate.endElement(uri, localName, qName);
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            delegate.characters(ch, start, length);
        }

        @Override
        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
            delegate.ignorableWhitespace(ch, start, length);
        }

        @Override
        public void processingInstruction(String target, String data) throws SAXException {
            delegate.processingInstruction(target, data);
        }

        @Override
        public void skippedEntity(String name) throws SAXException {
            delegate.skippedEntity(name);
        }

    }

}

假设

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>jaxb-test</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency> 
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-impl</artifactId>
      <version>3.0.2</version> <!-- latest, depends on jakarta.xml.bind:jakarta.xml.bind-api:3.0.1 -->
    </dependency>
  </dependencies>
</project>

OOP 解决方案

我们抽象 (public) Container,并引入(私有或我们选择的可见性(,空))实现,与正确的 qName:

public class NamespaceTest {

  public static interface Container {
  }

  @XmlRootElement(namespace = "apple", name = "container")
  private static class ContainerApple implements Container {

  }

  @XmlRootElement(namespace = "pear", name = "container")
  private static class ContainerPear implements Container {

  }
  ...

..!

使用相同的 main 方法,unmarshall 将(仍然)看起来像:

  ...
  private static void unmarshall(final String xml) throws Exception {
    Unmarshaller umler = CTXT.createUnmarshaller();
    try ( Reader reader = new StringReader(xml)) {
      System.out.println(umler.unmarshal(reader)
      );
    }
  }

  private static final JAXBContext CTXT = initContext();

  private static JAXBContext initContext() {
    try {
      return JAXBContext.newInstance(ContainerApple.class, ContainerPear.class);
    } catch (JAXBException ex) {
      throw new IllegalStateException("Could not initialize jaxb context.");
    }
  }
}
  • 单例 JAXBContext。
  • (静态)初始化:
    • 捕获异常并重新抛出 (runtime/unchecked)。
    • 所有(已知)jaxb classes/packages/context(配置)。

打印我们:

com.example.jaxb.test.NamespaceTest$ContainerApple@4493d195
com.example.jaxb.test.NamespaceTest$ContainerPear@2781e022