JAXB:如何解组不同类型的对象,但与共同的父对象?

问题描述:

在我们的应用程序中有一个相当常见的模式。我们在Xml中配置一个配置对象的集合(或列表),它们都实现了一个通用接口。在启动时,应用程序读取Xml并使用JAXB创建/配置对象列表。我从来没有想过(在多次阅读各种帖子后)使用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>元素对应于收费接口的具体子类。 type属性给出有关要实例化哪种类型的信息,然后一旦实例化,JAXB解组就应用剩余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对象进行解组:

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

在最后,在上面的代码中,我们得到一个List,它包含在Xml中配置的任何类型。

有没有办法让JAXB自动执行此操作,而无需编写代码来迭代上述元素?

+0

你可以改变的XML格式还是你:

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注解配置必须按原样使用它? – jtahlborn 2013-02-26 00:05:39

+0

@jtahlborn:Xml可以是任何易于手动编辑的东西。 – 2013-02-26 03:32:38

备注:我是EclipseLink JAXB (MOXy)的领导者和JAXB (JSR-222)专家组的成员。

如果您正在使用莫西为您的JAXB提供者,那么你可以使用莫西的@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接口的实现wou ld正常注释。

import javax.xml.bind.annotation.*; 

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

    @XmlAttribute 
    private String name; 

    @XmlAttribute 
    private String rate; 

} 

更多信息

+1

谢谢,我会试试以上。 – 2013-02-26 14:31:17

+1

JAXB是否有可能增强factoryMethod特性来获取传入当前Xml元素的参数,以便可以通过Reflection处理实例化对象? – 2013-02-26 16:32:56

+0

再次感谢这个例子。我从中学到了很多东西。最后,我只使用了XmlElements注释,并为每个具体子类型改变了Xml标签名称。当然,如果你可以在代码中添加一个新的具体子类型,更新Xml,而不必更新绑定注释,那么它会更好一些...... :) – 2013-02-26 19:57:32

我不认为这是可能的,如果所有的元素被命名为<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... 
    } 
} 

一点点的黑客,但考虑到要求它应该做的工作。

+0

xml可以使用户易于手动编辑,但也适用于JAXB。 – 2013-02-26 03:34:40

你可以使用一个XmlAdapter这个用例。 impl只处理Commission类型,但可以轻松扩展以支持所有类型。您需要确保AdaptedFee包含Fee接口的所有实现的组合属性。

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; 

} 

更多信息

+1

也感谢这个建议。我已经多次探索过你的博客,寻找与JAXB做事的不同方式! – 2013-02-26 16:34:05