JAXB 完全从接口进行编组

2022-09-04 04:00:14

我有一个复杂的Java接口层次结构,我想用JAXB来封送(不一定是取消元帅)。这些接口表示将从 JAX-RS REST API 返回的对象,如 XML、JSON、YAML 等(我使用的是 RestEasy,它可以以 XML 以外的格式封送 JAXB 注释的类型。

问题似乎在于JAXB基本上是面向类的。我对 JAXB 和接口的困难做了很多 Web 研究,最接近的解决方案是 MOXy JAXB - 将接口映射到 XMLJAXB 以及 Interface Fronted Models。但是,我有两个主要问题:a)我想根据接口进行注释/工作,而不是具体的类(其中将有多个实现并包含不应封送处理的重要其他状态),以及b)我有多个级别的接口继承。下面是一个接口示例,减去到目前为止的任何 JAXB 注释:

interface Uuided {
  UUID getId();
}
interface Named {
  String getName();
}
interface Component extends Uuided, Named {
  Map<String, ComponentAttribute> getAttributes();
}
interface Attribute extends Named {
  Type getType();
  Object getValue();
}
interface ComponentAttribute extends Attribute {
  Component getDeclaringComponent();
}

理想情况下,这将生成如下内容:

<component id="xxx" name="thing">
  <attributes>
    <componentAttribute name="color">
      <type><stringType/></type>
      <value>green</value>
      <declaringComponent idref="xxx"/>
    </componentAttribute>
  </attributes>
</component>

显然,抽象地说,这导致了诸如确定最派生的带注释接口之类的问题,理论上可能存在多个接口。但是,在我的情况下,我相信具体类只实现应该封送处理的单个接口。取消marshaling不是必需的,因为我有单独的类来定义upsert属性。

所以我的问题是,这甚至有可能用JAXB处理,如果是这样,如何处理?即使我必须非常明确地定义绑定、适配器等,我也希望在 JAXB 框架中工作,以获得 RestEasy 中所有非 XML 提供程序的好处。


答案 1

简短的回答:在您的界面字段上使用。@XmlElement(type = Object.class)

详情如下:

我已经找到了2种方法,你可以让JAXB序列化你的接口:

  1. @XmlAnyElement
  2. @XmlElement(type = Object.class)

1.@XmlAnyElement

只需使用@XmlAnyElement注释接口类型字段,JAXB 就会从接口的具体类型序列化接口。不要忘记使用@XmlRootElement注释具体类型,并将具体类型添加到 JAXBContext 中。完整示例如下:

public class InterfaceSerializer {

    @XmlRootElement
    public static class Pojo {
        Pojo() {
            field1 = new PojoFieldImpl1();
            field2 = new PojoFieldImpl2();
            field3 = new PojoFieldImpl1();
        }

        @XmlAnyElement
        public IPojoField field1;
        @XmlAnyElement
        public IPojoField field2;
        @XmlAnyElement
        public IPojoField field3;

        @Override
        public String toString() {
            return "field1 = " + field1 + "\nfield2 = " + field2 + "\nfield3 = " + field3;
        }
    }

    public static interface IPojoField {

    }

    @XmlRootElement
    public static class PojoFieldImpl1 implements IPojoField {

        PojoFieldImpl1() {
            value = "PojoFieldImpl1 value";
        }

        public String value;

        @Override
        public String toString() {
            return value;
        }
    }

    @XmlRootElement
    public static class PojoFieldImpl2  implements IPojoField {

        PojoFieldImpl2() {
            value = "PojoFieldImpl2 value1";
            value2 = "PojoFieldImpl2 value2";
        }

        public String value;
        public String value2;

        @Override
        public String toString() {
            return value + " " + value2;
        }
    }

    public static void main(String []args) throws JAXBException {
        Pojo pojo = new Pojo();
        JAXBContext jaxbContext = JAXBContext.newInstance(Pojo.class, PojoFieldImpl1.class, PojoFieldImpl2.class);
        Marshaller marshaller = jaxbContext.createMarshaller();

        marshaller.marshal(pojo, new File("interfaceSerializer.xml"));
    }
}

输出 XML:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<pojo>
    <pojoFieldImpl1>
        <value>PojoFieldImpl1 value</value>
    </pojoFieldImpl1>
    <pojoFieldImpl2>
        <value>PojoFieldImpl2 value1</value>
        <value2>PojoFieldImpl2 value2</value2>
    </pojoFieldImpl2>
    <pojoFieldImpl1>
        <value>PojoFieldImpl1 value</value>
    </pojoFieldImpl1>
</pojo>

此方法的缺点:

  • 您无法将每个字段与pojo与XML区分开来(相同的实现将使用相同的标记编写)
  • 您没有任何类型信息来取消您的XML(如果您愿意,可以这样做)

这些缺点在第二种解决方案中是固定的:

2.@XmlElement(类型 = 对象.class)

我在mikesir87的博客文章中偶然发现了这一点。只需将上面的@XmlAnyElement注释替换为@XmlElement(type = Object.class)在上面的Pojo类中应该有这样的东西:

@XmlElement(type = Object.class)
public IPojoField field1;
@XmlElement(type = Object.class)
public IPojoField field2;
@XmlElement(type = Object.class)
public IPojoField field3;

重新运行我们的示例,即生成的 XML:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<pojo>
    <field1 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="pojoFieldImpl1">
        <value>PojoFieldImpl1 value</value>
    </field1>
    <field2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="pojoFieldImpl2">
        <value>PojoFieldImpl2 value1</value>
        <value2>PojoFieldImpl2 value2</value2>
    </field2>
    <field3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="pojoFieldImpl1">
        <value>PojoFieldImpl1 value</value>
    </field3>
</pojo>

这也可以反序列化:

Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
Pojo unmarshalledPojo = (Pojo) unmarshaller.unmarshal(new File("interfaceSerializer.xml"));  
System.out.println(unmarshalledPojo);

结果输出:

field1 = PojoFieldImpl1 value
field2 = PojoFieldImpl2 value1 PojoFieldImpl2 value2
field3 = PojoFieldImpl1 value

可能是一个“黑客”解决方案,但它可以完成工作。


答案 2

我认为答案是JAXB根本不打算支持这一点,试图强迫它是愚蠢的。此外,JAXB驱动的JSON封送处理也不理想。

我最终编写了自己的封送处理框架,其中包含自己的一组注释:

@MarshalMixin // marshal fields but not a top-level object
interface Uuided {
  @MarshalAsString // ignore properties; just call toString()
  @MarshalId // treat as identifier for @MarshalUsingIds or cyclic ref
  UUID getId();
}
@MarshalMixin
interface Named {
  @MarshalId
  String getName();
}
@MarshalObject // top-level marshaled object providing class name
interface Component extends Uuided, Named {
  @MarshalAsKeyedObjectMap(key = "name") // see description below
  Map<String, ComponentAttribute> getAttributes();
}
@MarshalObject
interface Attribute extends Named {
  Type getType();
  @MarshalDynamic // use run-time (not declared) type
  Object getValue();
}
interface ComponentAttribute extends Attribute {
  @MarshalUsingIds
  Component getDeclaringComponent();
}

生成的封送处理程序写入抽象层(当前为 JSON 和 XML 实现)。这提供了更大的灵活性,使输出自然地用于不同的表示形式,而无需大量的注释和适配器。例如,我称之为键控对象映射,其中每个对象都包含其映射键。在 JSON 中,您需要一个映射,但在 XML 中,您需要一个序列:

{..., map: {'a': {'name': 'a', ...}, 'b': {'name: 'b', ...}, ...}, ...}
...<map><thing name='a'>...</thing><thing name='b'>...</thing></map>...

似乎有多达4个人也关心这个,所以希望我最终能开源它。:-)


推荐