为啥 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
来处理它:
java.sql.Date
默认不支持(尽管在EclipseLink JAXB (MOXy) 中支持)。这也可以使用在字段、属性或包级别通过@XmlJavaTypeAdapter
指定的XmlAdapter
来处理:
另外,另一个傻瓜,为什么 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 需要一个无参数构造函数来编组?的主要内容,如果未能解决你的问题,请参考以下文章