Java JAXB marshall/unmarshall 使用 Java 可选

Java JAXB marshall/unmarshall using Java Optionals

我的应用程序需要在 Java 和 XML 之间转换数据。

在转换数据时,我需要区分值是否存在,值是否明确设置为 null 或值是否有值。

XML 示例:

<person><name>Bob</name></person>     <-- element 'name' contains value "Bob"

<person><name nil="true"/></person>   <-- element 'name' was set explicitly to 'nil'/null

<person></person>                     <-- element 'name' is missing

由于 Java 类型像 'String' 只知道两种状态(null 或 not null),我尝试使用 Java 选项来解决这个问题。

XML 和 Java 可选值之间的映射可能如下所示:

<person></person>                   <=> Optional<String> name = null;

<person><name>Bob</name></person>   <=> Optional<String> name = Optional.of("Bob");

<person><name nil="true"/></person> <=> Optional<String> name = Optional.empty();

我尝试使用 JAXB 进行编组和解组。这个想法是,只有当需要将值显式设置为值时,才会调用字段的 setter 。这意味着隐式缺少值。


我查看了其他类似以下的 Whosebug 问题,但所有这些问题都无法完整处理我需要实现的行为:

How to generate JaxB-Classes with java.util.Optional?

Using generic @XmlJavaTypeAdapter to unmarshal wrapped in Guava's Optional

Using Guava's Optional with @XmlAttribute

这两天我一直在为这个问题苦苦挣扎。我尝试使用 XMLAdapter 和 GenericAdapter,尝试了几种方法来注释字段和 getter/setter 与 @XmlElement,尝试使用 @XmlAnyElment 有无松懈,但所有这些都只导致了部分成功。要么是 nil 值没有被正确处理,要么是列表没有被正确打印出来,...

我认为每个 Java 正确实施补丁操作的网络服务都应该有这个问题。 (不是在谈论“json 补丁方法”(RFC 6902))

有解决我问题的通用方法吗?

您可以在 java class 中使用一些验证,例如 @NotNull、@Size 等。或者您可以输入默认值,以确保它不会为空。之后,您可以使用推荐的 Xml 注释创建 DTO(数据传输对象)并使用 ModelMapper 对其进行映射。

以下代码能够区分空名称和空名称。为了使解决方案有效,我创建了一个 PersonList 元素来包含所有的 person 元素。如果元素被 XML:

显式设置为 null,则每个 Person 都包含一个 Name,该 Name 的 isNil() return 为真

Person.java:

import java.util.Optional;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlType(propOrder = {"name"})
@XmlRootElement(name = "person")
public class Person {

    private Optional<Name> optionalName;

    public Person() {
        optionalName = Optional.<Name>empty();
    }

    public Optional<Name> getOptionalName() {
        return optionalName;
    }

    public Name getName() {
        return (optionalName.isPresent()) ? (optionalName.get()) : (null);
    }

    @XmlElement(name = "name", required = false)
    public void setName(Name name) {
        optionalName = Optional.ofNullable(name);
    }

    @Override
    public String toString() {
        return String.format("Person(optionalName.isPresent() = %s, name = %s)",
                             Boolean.toString(optionalName.isPresent()),
                             ((getName() == null) ? ("null") : (getName().toString())));
    }
}

Name.java:

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "name")
public class Name {

    @XmlAttribute(name = "nil")
    private boolean nil;

    @XmlValue
    private String value;

    public Name() {
        nil = false;
        value = null;
    }

    public boolean isNil() {
        return nil;
    }

    public void setNil(boolean torf) {
        this.nil = torf;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return String.format("Name(nil = %s, value = %s)",
                             Boolean.toString(nil),
                             (value == null) ? ("null"):("\""+getValue()+"\""));
    }
}

PersonList.java:

import java.util.Iterator;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "PersonList")
public class PersonList {

    private List<Person> persons;

    public PersonList() {
        persons = null;
    }

    @XmlElement(name = "person")
    public List<Person> getPersons() {
        return persons;
    }

    public void setPersons(List<Person> persons) {
        this.persons = persons;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("PersonList(persons = ");
        if(persons == null) {
            sb.append("null");
        }
        else {
            sb.append("[");
            Iterator<Person> iterator = persons.iterator();
            while(iterator.hasNext()) {
                sb.append(iterator.next().toString());
                if(iterator.hasNext()) {
                    sb.append(", ");
                }
            }
            sb.append("]");
        }
        sb.append(")");
        return sb.toString();
    }
}

主要class演示解决方案:

import java.io.ByteArrayInputStream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

public class XmlOptional {
    public static final int STATUS_OKAY = 0;
    public static final int STATUS_ERROR = -1;

    public static final String XML_DATA = "<PersonList>" +
                                          "<person><name>Bob</name></person>" +
                                          "<person><name nil=\"true\" /></person>" +
                                          "<person></person>" +
                                          "</PersonList>";

    private XmlOptional() {
    }

    private static PersonList loadXml() {
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(XML_DATA.getBytes());
            JAXBContext context = JAXBContext.newInstance(PersonList.class);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            PersonList personList = (PersonList)unmarshaller.unmarshal(bais);
            return personList;
        }
        catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        int status = STATUS_OKAY;

        try {
            PersonList personList = loadXml();
            System.out.format("Xml contained: %s%n", personList);
        }
        catch (Throwable thrown) {
            status = STATUS_ERROR;
            thrown.printStackTrace();
        }
        finally {
            System.exit(status);
        }
    }
}

示例输出:

Xml 包含:PersonList(persons = [Person(optionalName.isPresent() = true, name = Name(nil = false, value = "Bob")), Person(optionalName.isPresent() = true, name = Name(nil = true, value = "")), Person(optionalName.isPresent() = false, name = null)])

由于仅靠正确使用和配置 JAXB 无法完全解决问题,因此我决定按如下方式解决:

(主要目标是基于XML写一个子系统与外部系统通信)

作为起点,我使用目标系统提供的 XSD 模式进行通信,并使用 JAXB 生成相应的 (XML)Java 类和 XSD 文件。生成的 类 中的所有字段都是 JAXBElement<> 类型,以便能够保持所需的 3 种状态(不存在、空、someValue)。

在业务模型方面,我使用 Java 类 和 Optional<> 字段类型来保持 3 种状态。

对于映射,我编写了一个映射器,它使用反射递归地从 JAXB 映射到 Java,反之亦然。当从 Java 映射到 JAXB 时,映射器使用 ObjectFactory 来创建 JAXBElement 对象。 (Mapper 本身只有大约 300 行代码)。 字段是根据匹配的字段名称映射的。

最丑陋和最具挑战性的部分是,需要更改 XSD 模式文件,以便使 JAXB 生成 类 使用 JAXBElement 字段类型。因此,如果尚未设置,我必须手动将 minOccurs="0" nillable="true" 属性添加到 XML 元素。

通过上述解决方案,考虑到所需的 3 个状态,我最终成功地将 XML 映射到 Java,反之亦然。

当然,这个解决方案有其缺点。 一种是手动修改XSD文件。通常不好的做法是更改外部系统提供的 XSD 文件,该文件充当接口合同。

对于我当时的要求,该解决方案非常有效。甚至对外部系统的接口契约的修改也可以很容易地实现。