JAXB: как размонтировать список объектов разных типов, но с общим родителем?
В наших приложениях существует довольно распространенная модель. Мы настраиваем настройку набора (или списка) объектов в Xml, которые реализуют общий интерфейс. При запуске приложение считывает Xml и использует JAXB для создания/настройки списка объектов. Я никогда не вычислял (после многократного чтения различных сообщений) "правильный путь", чтобы сделать это, используя только JAXB.
Например, у нас есть интерфейс Fee
и несколько конкретных классов реализации, которые имеют некоторые общие свойства, а также некоторые отклоняющиеся свойства и очень разные поведения. Xml, который мы используем для настройки списка сборов, используемых приложением:
<fees>
<fee type="Commission" name="commission" rate="0.000125" />
<fee type="FINRAPerShare" name="FINRA" rate="0.000119" />
<fee type="SEC" name="SEC" rate="0.0000224" />
<fee type="Route" name="ROUTES">
<routes>
<route>
<name>NYSE</name>
<rates>
<billing code="2" rate="-.0014" normalized="A" />
<billing code="1" rate=".0029" normalized="R" />
</rates>
</route>
</routes>
...
</fee>
</fees>
В приведенном выше Xml каждый элемент <fee>
соответствует конкретному подклассу интерфейса Fee. Атрибут type
предоставляет информацию о том, какой тип должен быть создан, а затем, когда он создается, JMXB unmarshalling применяет свойства из оставшегося Xml.
Мне всегда приходится прибегать к подобным действиям:
private void addFees(TradeFeeCalculator calculator) throws Exception {
NodeList feeElements = configDocument.getElementsByTagName("fee");
for (int i = 0; i < feeElements.getLength(); i++) {
Element feeElement = (Element) feeElements.item(i);
TradeFee fee = createFee(feeElement);
calculator.add(fee);
}
}
private TradeFee createFee(Element feeElement) {
try {
String type = feeElement.getAttribute("type");
LOG.info("createFee(): creating TradeFee for type=" + type);
Class<?> clazz = getClassFromType(type);
TradeFee fee = (TradeFee) JAXBConfigurator.createAndConfigure(clazz, feeElement);
return fee;
} catch (Exception e) {
throw new RuntimeException("Trade Fees are misconfigured, xml which caused this=" + XmlUtils.toString(feeElement), e);
}
}
В приведенном выше коде JAXBConfigurator
представляет собой просто простую оболочку объектов JAXB для unmarshalling:
public static Object createAndConfigure(Class<?> clazz, Node startNode) {
try {
JAXBContext context = JAXBContext.newInstance(clazz);
Unmarshaller unmarshaller = context.createUnmarshaller();
@SuppressWarnings("rawtypes")
JAXBElement configElement = unmarshaller.unmarshal(startNode, clazz);
return configElement.getValue();
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}
В конце вышеприведенного кода мы получаем список, который содержит типы, которые были настроены в Xml.
Есть ли способ заставить JAXB сделать это автоматически без необходимости писать код для повторения элементов, как указано выше?
Ответы
Ответ 1
Примечание. Я являюсь лидером EclipseLink JAXB (MOXy) и членом экспертной группы JAXB (JSR-222).
Если вы используете MOXy в качестве поставщика JAXB, вы можете использовать аннотацию MOXy @XmlPaths
чтобы расширить стандартную аннотацию JAXB @XmlElements
чтобы сделать следующее:
сборы
import java.util.List;
import javax.xml.bind.annotation.*;
import org.eclipse.persistence.oxm.annotations.*;
@XmlRootElement
public class Fees {
@XmlElements({
@XmlElement(type=Commission.class),
@XmlElement(type=FINRAPerShare.class),
@XmlElement(type=SEC.class),
@XmlElement(type=Route.class)
})
@XmlPaths({
@XmlPath("fee[@type='Commission']"),
@XmlPath("fee[@type='FINRAPerShare']"),
@XmlPath("fee[@type='SEC']"),
@XmlPath("fee[@type='Route']")
})
private List<Fee> fees;
}
комиссия
Реализации интерфейса Fee
будут аннотированы в обычном порядке.
import javax.xml.bind.annotation.*;
@XmlAccessorType(XmlAccessType.FIELD)
public class Commission implements Fee {
@XmlAttribute
private String name;
@XmlAttribute
private String rate;
}
Чтобы получить больше информации
Ответ 2
Вы можете использовать XmlAdapter
для этого XmlAdapter
использования. Импульс bleow обрабатывает только тип Commission
но может быть легко расширен для поддержки всех типов. Вы должны убедиться, что AdaptedFee
содержит комбинированные свойства из всех реализаций интерфейса Fee
.
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class FeeAdapter extends XmlAdapter<FeeAdapter.AdaptedFee, Fee>{
public static class AdaptedFee {
@XmlAttribute
public String type;
@XmlAttribute
public String name;
@XmlAttribute
public String rate;
}
@Override
public AdaptedFee marshal(Fee fee) throws Exception {
AdaptedFee adaptedFee = new AdaptedFee();
if(fee instanceof Commission) {
Commission commission = (Commission) fee;
adaptedFee.type = "Commission";
adaptedFee.name = commission.name;
adaptedFee.rate = commission.rate;
}
return adaptedFee;
}
@Override
public Fee unmarshal(AdaptedFee adaptedFee) throws Exception {
if("Commission".equals(adaptedFee.type)) {
Commission commission = new Commission();
commission.name = adaptedFee.name;
commission.rate = adaptedFee.rate;
return commission;
}
return null;
}
}
XmlAdapter
настраивается с @XmlJavaTypeAdapter
аннотации @XmlJavaTypeAdapter
:
import java.util.List;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Fees {
@XmlElement(name="fee")
@XmlJavaTypeAdapter(FeeAdapter.class)
private List<Fee> fees;
}
Чтобы получить больше информации
Ответ 3
Я не думаю, что это возможно, если все элементы называются <fee>
. Даже если бы это было (или есть), это было бы очень запутанно с точки зрения обслуживания.
У вас есть возможность переименовать различные элементы платы на основе типа (например, <tradeFee>
вместо <fee>
)?
В противном случае вы можете создать класс BaseFee
который имеет все поля для каждого возможного типа <fee>
. Вы можете распаковать данные в список объектов BaseFee
и преобразовать их в более определенный тип во время выполнения, например
List<BaseFee> fees = ...;
for (BaseFee fee : fees) {
if (isTradeFee(fee)) {
TradeFee tradeFee = toTradeFee(fee);
// do something with trade fee...
}
}
Немного взлома, но с учетом требований он должен выполнять эту работу.