2013-02-25 7 views
9

W naszych aplikacjach występuje dość powszechny wzorzec. Konfigurujemy zestaw (lub listę) obiektów w Xml, które wszystkie implementują wspólny interfejs. Po uruchomieniu aplikacja odczytuje Xml i używa JAXB do tworzenia/konfigurowania listy obiektów. Nigdy nie rozgryzłem (po wielokrotnym czytaniu różnych postów) "właściwej drogi", aby to zrobić używając tylko JAXB.JAXB: jak wydzielać listę obiektów różnych typów, ale ze wspólnym rodzicem?

Na przykład mamy interfejs Fee i wiele konkretnych klas implementacyjnych, które mają pewne wspólne właściwości, jak również niektóre właściwości rozbieżne i bardzo różne zachowania. XML używamy, aby skonfigurować listę opłat stosowanych przez aplikację jest:

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

W powyższym XML, każdy element <fee> odpowiada konkretnej podklasy interfejsu Fee. Atrybut type podaje informacje o tym, który typ należy utworzyć, a następnie po jego utworzeniu, JMBB zignoruje zastosowanie właściwości z pozostałego Xml.

zawsze muszę uciekać się do robienia czegoś takiego:

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); 
    } 
} 

W powyższym kodzie JAXBConfigurator jest tylko prosty wrapper wokół JAXB obiektów dla 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); 
    } 
} 

Na koniec powyższego kodu otrzymamy listę zawierającą typy skonfigurowane w Xml.

Czy istnieje sposób, aby JAXB zrobił to automatycznie, bez konieczności pisania kodu do iterowania elementów, jak powyżej?

+0

można zmieniać formatu XML czy też pracować z nim tak jak jest? – jtahlborn

+0

@jtahlborn: Xml może być wszystkim, co można łatwo edytować ręcznie. –

Odpowiedz

4

Uwaga: Jestem EclipseLink JAXB (MOXy) ołowiu i członkiem grupy JAXB (JSR-222) ekspertów.

Jeśli używasz Moxy jako dostawcę JAXB następnie można korzystać z Moxy za @XmlPaths adnotacji przedłużyć standardowy JAXB @XmlElements adnotacji wykonać następujące czynności:

opłat

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; 

} 

Komisja

Implementacje interfejsu Fee wou Powinniśmy normalnie pisać adnotacje.

import javax.xml.bind.annotation.*; 

@XmlAccessorType(XmlAccessType.FIELD) 
public class Commission implements Fee { 

    @XmlAttribute 
    private String name; 

    @XmlAttribute 
    private String rate; 

} 

Aby uzyskać więcej informacji

+1

Dzięki, dam powyżej spróbować. –

+1

Czy jest jakaś szansa, że ​​JAXB ulepszy funkcję factoryMethod, aby wziąć argument, który przechodzi w bieżącym elemencie Xml, aby instancję obiektu można było obsłużyć za pomocą Reflection? –

+0

Jeszcze raz dziękuję za przykład. Dowiedziałem się z tego całkiem sporo. W końcu użyłem adnotacji XmlElements i zmieniłem nazwę tagu Xml dla każdego podtypu konkretnego. Oczywiście byłoby fajniej, gdybyś mógł dodać nowy konkretny podtyp w kodzie, zaktualizować Xml i nie aktualizować adnotacji wiążącej ... :) –

0

Nie sądzę, że jest to możliwe, jeśli wszystkie elementy mają nazwę <fee>. Nawet jeśli był (lub jest) byłby bardzo mylący z punktu widzenia konserwacji.

Czy masz możliwość zmiany nazwy różnych elementów opłat na podstawie typu (np. <tradeFee> zamiast <fee>)?

W przeciwnym razie można utworzyć klasę BaseFee, która ma wszystkie pola dla każdego możliwego typu <fee>. Możesz odrzucić dane na listę obiektów BaseFee i przekształcić je w bardziej szczegółowy typ w środowisku wykonawczym, np.

List<BaseFee> fees = ...; 
for (BaseFee fee : fees) { 
    if (isTradeFee(fee)) { 
     TradeFee tradeFee = toTradeFee(fee); 
     // do something with trade fee... 
    } 
} 

Trochę hakowania, ale biorąc pod uwagę wymagania, powinien wykonać to zadanie.

+0

Plik xml może być dowolny, co ułatwia edycję ręczną, ale działa również z JAXB. –

4

Można użyć XmlAdapter dla tego przypadku użycia. Implant typu impl obsługuje tylko typ Commission, ale można go łatwo rozszerzyć, aby obsługiwał wszystkie typy. Musisz upewnić się, że AdaptedFee zawiera połączone właściwości ze wszystkich implementacji interfejsu 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 jest skonfigurowany przy użyciu @XmlJavaTypeAdapter adnotacji:

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; 

} 

Aby uzyskać więcej informacji

+1

Dziękuję również za tę sugestię. Wielokrotnie umieściłem Twojego bloga, szukając różnych sposobów robienia rzeczy z JAXB! –