防止写入默认属性值 JAXB

Prevent writing default attribute values JAXB

我正在尝试编写 class 以便在 JAXB 中编写具有属性的元素。在这个 XML 中有一些默认值,无论它们是字符串、整数还是自定义 class 类型。

以下缩减示例:

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "FIELD")
public class TestLayoutNode
{
    // I want to not write this to the xml when it is 0
    @XmlAttribute(name = "num")
    private int number;

    // I want to not write this when it is "default"
    @XmlAttribute(name = "str")
    private String str;
}

根据JAXB Avoid saving default values 我知道如果我不想写字符串,我可以修改 getters/setters 以写入 null 并在读取 null 时读取默认值。

但是,对于 int,我不确定该怎么做,因为除非特别更改它,否则它的值将始终为 0。

有更好的方法吗?我可以将内部数据类型更改为 String,然后在需要时转换它,但这有点混乱。

可以改成整数

private Integer number;

那么这个对象的值在没有实例化的时候就是null

使用 Integer 而不是原始 int。将所有基本类型替换为对应的对象,然后您可以使用 NULL.

根据字符串默认值,使用并修改getter

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "FIELD")
public class NullAttrs {

    private Integer number;
    private String str;

    public void setNumber(Integer number) {
        this.number = number;
    }

    @XmlAttribute(name = "num")
    public Integer getNumber() {
        return number;
    }

    public void setStr(String str) {
        this.str = str;
    }

    @XmlAttribute(name = "str")
    public String getStr() {
        if (str != null && str.equalsIgnoreCase("default"))
         return null;
        else if (str == null)
         return "default";
        else
         return str;
    }

    public static void main(String[] args) throws JAXBException {
        JAXBContext jc = JAXBContext.newInstance(NullAttrs.class);

        NullAttrs root = new NullAttrs();
        root.setNumber(null);
        root.setStr("default");

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(root, System.out);
    }
}

本例中的结果为空 FIELD:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<FIELD/>

您可以通过将字段更改为默认空值不出现在 XML 表示中的对象类型来执行以下操作)并在 getter 中放置一些逻辑:

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

    @XmlAttribute(name = "num")
    private Integer number;

    @XmlAttribute
    private String str;

    public int getNumber() {
        if(null == number) {
           return 0;
        } else {
           return number;
        }
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public String getStr() {
        if(null == str) {
            return "default";
        } else {
            return str;
        }
    }

    public void setStr(String str) {
        this.str = str;
    }
}

允许取消设置 属性

如果你想让set操作return a 属性到它的默认状态那么你需要在set方法中添加逻辑。

public void setNumber(int number) {
    if(0 == number) {
        this.number = null;
    } else {
        this.number = number;
    }
 }

或者您可以提供一个未设置的方法:

public void unsetNumber() {
    this.number = null;
}

允许设置为 null

如果你想允许 str 属性 被设置为 null 这样 get 方法将 return null 而不是 "default" 然后你可以维护一个标志来跟踪它是否已设置:

    private strSet = false;

    public String getStr() {
        if(null == str && !strSet) {
            return "default";
        } else {
            return str;
        }
    }

    public void setStr(String str) {
        this.str = str;
        this.strSet = true;
    }

更新

Blaise, don't you think that the solution is pretty verbose?

I mean that such use case should be probably supported by framework. For example using annotation like @DefaultValue.

JAXB 目前如何支持默认值

如果 XML 中没有节点,则不会对 Java 对象中相应的 field/property 执行集合。这意味着您将 属性 初始化为的任何值仍然存在。在编组上,因为值已填充,它将被编组出来。

真正要求的是什么

真正要求的是在 field/property 具有默认值时不封送它。通过这种方式,您希望 marshal 行为对于空值和默认值是相同的。这就引入了一些需要解决的问题:

  1. 您现在如何将 null 编组到 XML?默认情况下,它是否仍被编组为缺失节点?
  2. 是否需要提供一种机制来区分 属性 是默认值(不存在于 XML 中)和已设置为与默认值相同的值(存在于 XML 中) XML)?

人们今天在做什么?

通常对于此用例,人们只需将 int 属性 更改为 Integer 并将 null 设为默认值。我以前没有遇到过有人要求这种行为 String

虽然它不像人们希望的那样简洁,但可以创建 XmlAdapter 以避免编组默认值。

用例是这样的:

@XmlRootElement(name = "FIELD")
public class TestLayoutNode
{
  @XmlAttribute(name = "num")
  @XmlJavaTypeAdapter(value = IntegerZero.class, type = int.class)
  public int number;

  @XmlAttribute(name = "str")
  @XmlJavaTypeAdapter(StringDefault.class)
  public String str = "default";
}

这是适配器。

整数零:

public class IntegerZero extends DefaultValue<Integer>
{
  public Integer defaultValue() { return 0; }
}

字符串默认值:

public class StringDefault extends DefaultValue<String>
{
  public String defaultValue() { return "default"; }
}

默认值适配器:

public class DefaultValue<T> extends XmlAdapter<T, T>
{
  public T defaultValue() { return null; }

  public T marshal(T value) throws Exception
  {
    return (value == null) || value.equals(defaultValue()) ? null : value;
  }

  public T unmarshal(T value) throws Exception
  {
    return value;
  }
}

对于少量不同的默认值,此方法效果很好。

我发现使用自定义 getters/setters 或适配器的解决方案非常冗长,所以我选择了一个不同的解决方案:一个检查值并在默认情况下将其清空的编组器。

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Set;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;
import javax.xml.bind.helpers.AbstractMarshallerImpl;
import javax.xml.transform.Result;
import com.google.common.collect.ImmutableSet;

class MyJaxbMarshaller extends AbstractMarshallerImpl {
    /** See https://docs.oracle.com/cd/E13222_01/wls/docs103/webserv/data_types.html#wp221620 */
    private static final Set<String> SUPPORTED_BASIC_TYPES = ImmutableSet.of(
            "boolean", "java.lang.Boolean", "byte", "java.lang.Byte", "double", "java.lang.Double",
            "float", "java.lang.Float", "long", "java.lang.Long", "int", "java.lang.Integer",
            "javax.activation.DataHandler", "java.awt.Image", "java.lang.String",
            "java.math.BigInteger", "java.math.BigDecimal", "java.net.URI", "java.util.Calendar",
            "java.util.Date", "java.util.UUID", "javax.xml.datatype.XMLGregorianCalendar",
            "javax.xml.datatype.Duration", "javax.xml.namespace.QName",
            "javax.xml.transform.Source", "short", "java.lang.Short");
    private final Marshaller delegate;

    MyJaxbMarshaller(Marshaller delegate) {
        this.delegate = delegate;
    }

    @Override
    public void setProperty(String name, Object value) throws PropertyException {
        super.setProperty(name, value);
        delegate.setProperty(name, value);
    }

    @Override
    public void marshal(Object jaxbElement, Result result) throws JAXBException {
        try {
            delegate.marshal(clearDefaults(jaxbElement), result);
        } catch (ReflectiveOperationException ex) {
            throw new JAXBException(ex);
        }
    }

    private Object clearDefaults(Object element) throws ReflectiveOperationException {
        if (element instanceof Collection) {
            return clearDefaultsFromCollection((Collection<?>) element);
        }
        Class<?> clazz = element.getClass();
        if (isSupportedBasicType(clazz)) {
            return element;
        }
        Object adjusted = clazz.getConstructor().newInstance();
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            copyOrRemove(field, element, adjusted);
        }
        return adjusted;
    }

    private Object clearDefaultsFromCollection(Collection<?> collection)
            throws ReflectiveOperationException {
        @SuppressWarnings("unchecked")
        Collection<Object> result = collection.getClass().getConstructor().newInstance();
        for (Object element : collection) {
            result.add(clearDefaults(element));
        }
        return result;
    }

    private static boolean isSupportedBasicType(Class<?> clazz) {
        return SUPPORTED_BASIC_TYPES.contains(clazz.getName());
    }

    private void copyOrRemove(Field field, Object element, Object adjusted)
            throws ReflectiveOperationException {
        Object value = field.get(element);
        if (value != null) {
            if (value.equals(field.get(adjusted))) {
                value = null;
            } else {
                value = clearDefaults(value);
            }
        }
        field.set(adjusted, value);
    }
}

这适用于 class 类

@XmlRootElement
public class Foo {
    @XmlAttribute public Integer intAttr = 0;
    @XmlAttribute public String strAttr = "default";
}

如果需要,您可以使其更加灵活,例如您可以使用注释来标记您希望在默认情况下忽略的属性,或者扩展 class 以了解 @XmlTransient 或方法访问器之类的东西(这两个都不是我的问题现在的项目)。

您为绑定的简单性付出的代价 classes 是编组器将创建您将要编组的对象的深层副本,并与默认值进行大量比较确定要消除的内容。因此,如果运行时性能对您来说是一个问题,那么这可能是不行的。