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...
    }
}

Немного взлома, но с учетом требований он должен выполнять эту работу.