为啥 JAXB 需要一个无参数构造函数来编组?

Posted

技术标签:

【中文标题】为啥 JAXB 需要一个无参数构造函数来编组?【英文标题】:Why does JAXB need a no arg constructor for marshalling?为什么 JAXB 需要一个无参数构造函数来编组? 【发布时间】:2012-03-05 07:13:05 【问题描述】:

如果您尝试编组一个引用了没有无参数构造函数的复杂类型的类,例如:

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 在解组时需要一个无参数构造函数——因为它需要实例化对象。但是为什么 JAXB 在编组时需要一个无参数的构造函数呢?

另外,另一个傻瓜,如果字段为空,为什么 Java 的 JAXB 实现会抛出异常,并且无论如何都不会被编组?

我是否遗漏了什么,或者这些只是 Java 的 JAXB 实现中的错误实现选择?

【问题讨论】:

它实际上是恕我直言错误的实施。 JAXB 应该完成 Jackson 所做的事情并提供构造函数参数注释:cowtowncoder.com/blog/archives/2011/07/entry_457.html。话虽如此,JAXB 仍然比其他 JSR 好得多。 【参考方案1】:

回答您的问题:我认为这只是 JAXB(或者可能是 JAXB 实现)中的糟糕设计。无参数构造函数的存在在创建 JAXBContext 期间得到验证,因此无论您想使用 JAXB 进行编组还是解组都适用。如果 JAXB 将这种类型的检查推迟到 JAXBContext.createUnmarshaller(),那就太好了。我认为如果这种设计实际上是由规范强制规定的,或者它是否是 JAXB-RI 中的实现设计,我认为会很有趣。

但确实有一种解决方法。

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

假设您有一个类Customer,它是一个不可变对象。实例化是通过 Builder Pattern 或静态方法实现的。

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 不知道,这确实永远不会被使用。诀窍是@XmlType annotation with factoryMethod 参数。方法如下:

@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

在这个解决方案中,我们添加了一个私有的无参数构造函数,它只是将 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 的 hack。它绝对适用于 JAXB-RI。

【讨论】:

【参考方案2】:

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

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

http://blog.bdoughan.com/2010/12/jaxb-and-immutable-objects.html

java.sql.Date 默认不支持(尽管在EclipseLink JAXB (MOXy) 中支持)。这也可以使用在字段、属性或包级别通过@XmlJavaTypeAdapter 指定的XmlAdapter 来处理:

http://blog.bdoughan.com/2011/05/jaxb-and-joda-time-dates-and-times.html http://blog.bdoughan.com/2011/01/jaxb-and-datetime-properties.html

另外,另一个傻瓜,为什么 Java 的 JAXB 实现会抛出一个 如果该字段为空并且不会被编组,则异常 还是?

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


更新

您可以执行以下操作:

SqlDateAdapter

下面是一个 XmlAdapter,它将从您的 JAXB 实现不知道如何处理的 java.sql.Date 转换为它所做的 java.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());
    


Foo

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

【讨论】:

如果您尝试按原样编组我在问题中粘贴的类,它会抛出异常,即使 Date 对象为 null 并且甚至不会成为 XML 的一部分。 我已经更新了我的答案以包含一个 XmlAdapter 您可以用来处理 java.sql.Date 字段。 布莱斯 - 谢谢。我已经使用了类似的适配器。请注意,您不需要在适配器中进行 null 检查。我只是指出有关 Java 的 JAXB 实现的一些有趣的事情,因为它需要一个适配器(并引发异常)——即使该字段为 null 并且它不会使用适配器。我觉得这很奇怪。 另外 - 我的主要问题是 - 在编组过程中,编组器需要一个无参数构造函数吗?从响应来看,它似乎不需要一个 - 但编组器只是确保对象可以被解组。 问题是@XmlElement 和朋友不能像杰克逊一样放在构造函数及其参数上。您可以使用XmlAdapter,但恕我直言,我认为杰克逊做得更优雅。也许 Moxy 会添加一些东西(因为这对于具有最终字段的不可变对象来说非常好)。【参考方案3】:

您似乎认为 JAXB 自省代码将具有用于初始化的特定于操作的路径。如果是这样,那将导致大量重复代码,并且实现起来很糟糕。我想 JAXB 代码有一个通用例程,它在第一次需要模型类时检查它并验证它是否遵循所有必要的约定。在这种情况下,它失败了,因为其中一个成员没有所需的无参数构造函数。初始化逻辑很可能不是特定于编组/解组的,也极不可能将当前对象实例考虑在内。

【讨论】:

【参考方案4】:

一些企业和Dependency Injection 框架使用反射Class.newInstance() 来创建类的新实例。此方法需要一个公共的无参数构造函数才能实例化该对象。

【讨论】:

谢谢 - 我明白了 - 我只是想知道,在编组过程中的哪个位置需要创建正在编组的类的新实例?

以上是关于为啥 JAXB 需要一个无参数构造函数来编组?的主要内容,如果未能解决你的问题,请参考以下文章

收到“未定义无参数构造函数”错误,不知道为啥

为啥 Hibernate 不需要参数构造函数?

为啥我必须为 Code First / Entity Framework 提供无参数构造函数

C++:调用无参数的构造函数为啥不加括号

golang函数中的参数为啥不支持默认值

为啥使用set注入,一定要给类提供一个无参的构造函数,否则Spring不能实例化类的.