为什么 JAXB 需要一个无 arg 构造函数来进行编组?

2022-08-31 22:12:46

如果尝试封送一个类,该类引用没有无参数构造函数的复杂类型,例如:

import java.sql.Date;

@XmlRootElement(name = "Foo")
@XmlAccessorType(XmlAccessType.FIELD)
public class Foo {
    int i;
    Date d; //java.sql.Date does not have a no-arg constructor
}

使用作为 Java 一部分的 JAXB 实现,如下所示:

    Foo foo = new Foo();
    JAXBContext jc = JAXBContext.newInstance(Foo.class);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    Marshaller marshaller = jc.createMarshaller();
    marshaller.marshal(foo, baos);

JAXB 将抛出一个

com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions java.sql.Date does not have a no-arg default constructor

现在,我明白了为什么 JAXB 在取消编组时需要一个无 arg 构造函数 - 因为它需要实例化对象。但是,为什么 JAXB 在编组时需要一个无 arg 构造函数呢?

另外,另一个问题是,如果字段为空,为什么Java的JAXB实现会引发异常,并且无论如何都不会被编组?

我是否遗漏了一些东西,或者这些只是Java的JAXB实现中糟糕的实现选择?


答案 1

JAXB (JSR-222) 实现初始化其元数据时,它确保它可以同时支持编组和取消编组。

对于没有无参数构造函数的 POJO 类,可以使用类型级别来处理它:XmlAdapter

java.sql.Date默认情况下不受支持(尽管在 EclipseLink JAXB (MOXy) 中它是受支持的)。这也可以在字段、属性或包级别使用指定的 via 进行处理:XmlAdapter@XmlJavaTypeAdapter


另外,另一个问题是,如果字段为空,为什么Java的JAXB实现会引发异常,并且无论如何都不会被编组?

您看到的异常是什么?通常,当字段为 null 时,它不会包含在 XML 结果中,除非用它来注释,在这种情况下,元素将包含 。@XmlElement(nillable=true)xsi:nil="true"


更新

您可以执行以下操作:

SqlDateAdapter

下面是一个将从 JAXB 实现不知道如何处理的转换为它所处理的:XmlAdapterjava.sql.Datejava.util.Date

package forum9268074;

import javax.xml.bind.annotation.adapters.*;

public class SqlDateAdapter extends XmlAdapter<java.util.Date, java.sql.Date> {

    @Override
    public java.util.Date marshal(java.sql.Date sqlDate) throws Exception {
        if(null == sqlDate) {
            return null;
        }
        return new java.util.Date(sqlDate.getTime());
    }

    @Override
    public java.sql.Date unmarshal(java.util.Date utilDate) throws Exception {
        if(null == utilDate) {
            return null;
        }
        return new java.sql.Date(utilDate.getTime());
    }

}

通过注释注册:XmlAdapter@XmlJavaTypeAdapter

package forum9268074;

import java.sql.Date;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement(name = "Foo")
@XmlAccessorType(XmlAccessType.FIELD)
public class Foo {
    int i;

    @XmlJavaTypeAdapter(SqlDateAdapter.class)
    Date d; //java.sql.Date does not have a no-arg constructor
}

答案 2

回答你的问题:我认为这只是JAXB(或者JAXB实现)中的糟糕设计。无 arg 构造函数的存在会在创建 期间得到验证,因此无论您要使用 JAXB 进行编组还是取消编组,它都适用。如果 JAXB 将这种类型的检查推迟到 ,那就太好了。我认为深入研究这个设计是否真的是由规范规定的,或者它是JAXB-RI中的实现设计,这将是很有趣的。JAXBContextJAXBContext.createUnmarshaller()

但确实有一个解决方法。

JAXB 实际上并不需要无参数构造函数进行编组。在下文中,我假设您仅使用 JAXB 进行编组,而不是取消编组。我还假设您可以控制要编组的不可变对象,以便您可以更改它。如果不是这种情况,那么唯一的出路就是XmlAdapter,如其他答案中所述。

假设您有一个类 ,它是一个不可变对象。实例化是通过生成器模式或静态方法进行的。Customer

public class Customer {
    
    private final String firstName;
    private final String lastName;

    private Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // Object created via builder pattern
    public static CustomerBuilder createBuilder() {
        ...
    }
    
    // getters here ...
}

没错,默认情况下,您无法让 JAXB 取消此类对象的元组。你会得到错误“....客户没有无参数默认构造函数”。

至少有两种方法可以解决这个问题。它们都依赖于放入一个方法或构造函数,只是为了让 JAXB 的内省快乐。

解决方案 1

在此方法中,我们告诉 JAXB,有一个静态工厂方法可用于实例化类的实例。我们知道,但JAXB不知道,这确实永远不会被使用。诀窍是使用 factoryMethod 参数@XmlType注释。操作方法如下:

@XmlType(factoryMethod="createInstanceJAXB")
public class Customer {
    ...
    
    private static Customer createInstanceJAXB() {  // makes JAXB happy, will never be invoked
        return null;  // ...therefore it doesn't matter what it returns
    }

    ...
}

该方法是否像示例中那样是私有的并不重要。JAXB仍然会接受它。如果将方法设为私有,您的 IDE 会将该方法标记为未使用,但我仍然更喜欢私有。

解决方案 2

在此解决方案中,我们添加了一个私有的 no-arg 构造函数,该构造函数仅将 null 传递到实际构造函数中。

public class Customer {
    ...
    private Customer() {  // makes JAXB happy, will never be invoked
        this(null, null);   // ...therefore it doesn't matter what it creates
    }

    ...
}

构造函数是否像示例中那样是私有的并不重要。JAXB仍然会接受它。

总结

这两种解决方案都满足了 JAXB 对无参数实例化的需求。当我们自己知道我们只需要编组,而不是解除元帅时,我们需要这样做是一种耻辱。

我不得不承认,我不知道这在多大程度上是一个黑客,它只能与JAXB-RI一起使用,例如不适用于EclipseLink MOXy。它绝对可以与JAXB-RI一起使用。


推荐