首页 > 解决方案 > 处理多个版本的 XSD 生成的类

问题描述

我正在编写一个将集成到一个 API 的应用程序,该 API 具有返回由 XSD 支持的 XML 的端点。我的应用程序最初必须针对这个的两个不同版本(可能在未来的版本中更多),这不一定是动态的。当您启动应用程序时,用户必须告诉应用程序支持哪个 API 主要版本。XSD 不在我的控制之下,我不想编辑它们。

XSD 生成同名的类,我已经遇到了问题。我无法将ObjectFactoryXJC 生成的两个类都加载到 JAXBContext 中。我的解决方案现在是一张 JAXBContexts 地图:

private static Map<Integer, Pair<Class<?>, JAXBContext>> contexts = new HashMap<>();

static {
    try {
        contexts.put(4, Pair.of(com.api.v4_2_0.ObjectFactory.class, JAXBContext.newInstance(com.api.v4_2_0.ObjectFactory.class)));
        contexts.put(3, Pair.of(com.api.v3_9_4.ObjectFactory.class, JAXBContext.newInstance(com.api.v3_9_4.ObjectFactory.class)));
    } catch (JAXBException e) {
        LOGGER.error("Failed to initialize JAXBContext", e);
    }
}

该对用于了解 JAXBContext 基于哪个类,因为我无法在运行时恢复它。然后,为了序列化一个对象,我使用了很多有效但感觉不对的魔法反射:

public static String objectToString(final Object object, final Integer majorVersion) {
    try {
        final ByteArrayOutputStream os = new ByteArrayOutputStream();
        getMarshallerForMajorVersion(majorVersion).marshal(createJaxbElementFromObject(object, getObjectFactoryForMajorVersion(majorVersion)), os);
        return os.toString(UTF_8);
    } catch (JAXBException e) {
        throw SesameException.from("Failed to serialize object", e);
    }
}

private static Marshaller getMarshallerForMajorVersion(final Integer majorVersion) throws JAXBException {
    return getContextForMajorVersion(majorVersion).getRight().createMarshaller();
}

private static Class<?> getObjectFactoryForMajorVersion(final Integer majorVersion) {
    return getContextForMajorVersion(majorVersion).getLeft();
}

private static Pair<Class<?>, JAXBContext> getContextForMajorVersion(final Integer majorVersion) {
    if (contexts.containsKey(majorVersion)) {
        return contexts.get(majorVersion);
    }
    throw illegalArgument("No JAXBContext for API with major version %d", majorVersion);
}

private static JAXBElement<?> createJaxbElementFromObject(final Object object, final Class<?> objectFactory) {
    try {
        LOGGER.info("Attempting to find a JAXBElement producer for class {}", object.getClass());
        final Method method = findElementMethodInObjectFactory(object, objectFactory);
        return (JAXBElement<?>) method.invoke(objectFactory.getConstructor().newInstance(), object);
    } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | InstantiationException e) {
        throw illegalArgument("Failed to construct JAXBElement for class %s", object.getClass().getName());
    }
}

private static Method findElementMethodInObjectFactory(final Object object, final Class<?> left) {
    return Arrays.stream(left.getMethods())
            .filter(m -> m.getReturnType().equals(JAXBElement.class))
            .filter(m -> m.getName().endsWith(object.getClass().getSimpleName()))
            .findFirst()
            .orElseThrow(() -> illegalArgument("Failed to find JAXBElement constructor for class %s", object.getClass().getName()));
}

这工作正常,但感觉很脆弱。

问题

更糟糕的是必须使用泛型将 XML 反序列化为对象:

public static <T> T stringToObject(final String xml, final Class<T> toClass, final Integer majorVersion) {
    try {
        final Unmarshaller unmarshaller = getUnmarshallerForVersion(majorVersion);
        final JAXBElement<T> unmarshalledElement = (JAXBElement<T>) unmarshaller.unmarshal(new StringReader(xml));
        return toClass.cast(unmarshalledElement.getValue());
    } catch (JAXBException e) {
        throw SesameException.from(format("Failed to deserialize XML into %s", toClass.getCanonicalName()), e);
    }
}

// And calling this from another class
private com.api.v4_2_0.SomeClass.class toSomeClass(final HttpResponse<String> response) {
    return XmlUtil.stringToObject(response.body(), com.api.v4_2_0.SomeClass.class, apiMajorVersion); // <--- I can't beforehand use this package since major version might be 3.
}

现在(据我所知)我无法使用泛型并根据使用的 API 的主要版本将其映射到正确包中的正确类。

我也尝试过使用抽象基类并ObjectFactory为每个版本只给它一个,但这仍然给了我问题部分中描述的问题。我不知道如何返回该类的正确版本:

private com.api.v4_2_0.SomeClass.class toSomeClass(final HttpResponse<String> response) {
    return version4XmlUtil.stringToObject(response.body(), com.api.v4_2_0.SomeClass.class); // <--- I can't beforehand use this package since major version might be 3.
}

如何构建我的代码来解决这个问题?什么模式有用?我是否完全走错了路?

标签: javaxsdjaxb

解决方案


EclipseLink JAXB (MOXy) 的 @XmlPath 和外部绑定文件扩展可以提供帮助。

我在下面引用的博客将单个对象模型映射到两个不同的 XML 模式。它通过使用标准 JAXB 和 MOXy 扩展注释的组合映射第一个 API 来做到这一点。

package blog.weather;

import java.util.List;

import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

import org.eclipse.persistence.oxm.annotations.XmlPath;

@XmlRootElement(name="xml_api_reply")
@XmlType(propOrder={"location", "currentCondition", "currentTemperature", "forecast"})
@XmlAccessorType(XmlAccessType.FIELD)
public class WeatherReport {

    @XmlPath("weather/forecast_information/city/@data")
    private String location;

    @XmlPath("weather/current_conditions/temp_f/@data")
    private int currentTemperature;

    @XmlPath("weather/current_conditions/condition/@data")
    private String currentCondition;

    @XmlPath("weather/forecast_conditions")
    private List<Forecast> forecast;

}

进而...

package blog.weather;

import org.eclipse.persistence.oxm.annotations.XmlPath;

public class Forecast {

    @XmlPath("day_of_week/@data")
    private String dayOfTheWeek;

    @XmlPath("low/@data")
    private int low;

    @XmlPath("high/@data")
    private int high;

    @XmlPath("condition/@data")
    private String condition;

}

您不能通过注释创建到对象模型的第二组映射,因此其他映射可以利用 MOXy 的 XML 元数据,从而覆盖第二个 API。

<?xml version="1.0"?>
<xml-bindings
    xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    package-name="blog.weather"
    xml-mapping-metadata-complete="true">
    <xml-schema element-form-default="QUALIFIED">
        <xml-ns prefix="yweather" namespace-uri="http://xml.weather.yahoo.com/ns/rss/1.0"/>
    </xml-schema>
    <java-types>
        <java-type name="WeatherReport" xml-accessor-type="FIELD">
            <xml-root-element name="rss"/>
            <xml-type prop-order="location currentTemperature currentCondition forecast"/>
            <java-attributes>
                <xml-attribute java-attribute="location" xml-path="channel/yweather:location/@city"/>
                <xml-attribute java-attribute="currentTemperature" name="channel/item/yweather:condition/@temp"/>
                <xml-attribute java-attribute="currentCondition" name="channel/item/yweather:condition/@text"/>
                <xml-element java-attribute="forecast" name="channel/item/yweather:forecast"/>
            </java-attributes>
        </java-type>
        <java-type name="Forecast" xml-accessor-type="FIELD">
            <java-attributes>
                <xml-attribute java-attribute="dayOfTheWeek" name="day"/>
                <xml-attribute java-attribute="low"/>
                <xml-attribute java-attribute="high"/>
                <xml-attribute java-attribute="condition" name="text"/>
            </java-attributes>
        </java-type>
    </java-types>
</xml-bindings>

默认行为是 MOXy 的映射文档用于扩充模型上指定的任何注释。但是,根据您设置xml-mapping-metadata-complete标志的方式,XML 元数据可以完全替换简单地扩充(默认)注释提供的元数据。

试穿它的大小,让我知道你的想法:

使用 EclipseLink MOXy 将对象映射到多个 XML 模式 http://blog.bdoughan.com/2011/09/mapping-objects-to-multiple-xml-schemas.html


推荐阅读