Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
184 views
in Technique[技术] by (71.8m points)

java - Why is the ObjectFactory not used during unmarshalling?

I have defined the following ObjectFactory:

@XmlRegistry
public class ObjectFactory {

    public Dogs createDogs() {
        return new Dogs();
    }

    @XmlElementDecl(name = "dog")
    public Dog createDog(DogType value) {
        return new Dog(value);
    }

    @XmlElementDecl(name = "fido", substitutionHeadName = "dog", substitutionHeadNamespace = "")
    public Dog createFido(DogType value) {
        return new Dog("fido", value);
    }

    @XmlElementDecl(name = "barks", substitutionHeadName = "dog", substitutionHeadNamespace = "")
    public Dog createBarks(DogType value) {
        return new Dog("barks", value);
    }
}

(Dogs class is trivial, Dog and DogType see below or here.)

I am unmarshalling the following XML:

<listOfDogs>
    <dogs>
        <dog>
            <name>henry</name>
            <sound>bark</sound>
        </dog>
        <fido>
            <sound>woof</sound>
        </fido>
        <barks>
            <sound>miau</sound>
        </barks>
    </dogs>
</listOfDogs>

I was sincerely expecting that JAXB will call my createFido(...) and createBarks(...) methods during unmarshalling. But this does not happen. The Dog constructor is invoked directly via reflection, the appropriate create... methods are not used.

My question is:

Why is ObjectFactory not called during unmarshalling?

Shouldn't it be? Or is ist just a dummy to hold the @XmlRegistry/@XmlElementDecl declarations?

I have also checked this question:

What is the ObjectFactory role during JAXB-Unmarshalling?

The solution there is to use @XmlType.factoryClass and factoryMethod. This will not work here because I don't want to statically link my DogType to the certain instantiation routine. I want it to be decided in the runtime based on the element name. My goal is to instantiate the same class but differently, depending on the element name.


Now some code to make it complete.

Root element class:

@XmlRootElement(name = "listOfDogs")
public class Dogs {

    private List<JAXBElement<DogType>> dogs = new LinkedList<JAXBElement<DogType>>();

    @XmlElementWrapper(name = "dogs")
    @XmlElementRef(name = "dog")
    public List<JAXBElement<DogType>> getDogs() {
        return this.dogs;
    }

    @Override
    public String toString() {
        return "Dogs [dogs=" + dogs + "]";
    }
}

Dog, the wrapper element class for the DogType:

public class Dog extends JAXBElement<DogType> {

    public static final QName NAME = new QName("dog");

    private static final long serialVersionUID = 1L;

    public Dog(DogType value) {
        super(NAME, DogType.class, value);
    }

    public Dog(String dogName, DogType value) {
        super(NAME, DogType.class, value);
    }

    @Override
    public QName getName() {
        final DogType value = getValue();
        if (value != null && value.getName() != null) {
            return new QName(value.getName());
        } else {
            return super.getName();
        }
    }
}

DogType:

public class DogType {

    private String name;
    private String sound;

    public String getName() {
        return name;
    }

    public void setName(String dogName) {
        this.name = dogName;
    }

    public String getSound() {
        return sound;
    }

    public void setSound(String sound) {
        this.sound = sound;
    }
}

Test:

public class DogTest {

    @Test
    public void unmarshallsDogs() throws JAXBException {
        final JAXBContext context = JAXBContext
                .newInstance(ObjectFactory.class);
        final Dogs dogs = (Dogs) context.createUnmarshaller().unmarshal(
                getClass().getResource("dogs.xml"));
        Assert.assertEquals(3, dogs.getDogs().size());
        // Does not work
//      Assert.assertEquals("henry", dogs.getDogs().get(0).getValue()
//              .getName());
        Assert.assertEquals("bark", dogs.getDogs().get(0).getValue().getSound());
        // Does not work
//      Assert.assertEquals("fido", dogs.getDogs().get(1).getValue()
//              .getName());
        Assert.assertEquals("woof", dogs.getDogs().get(1).getValue().getSound());
        // Does not work
//      Assert.assertEquals("barks", dogs.getDogs().get(2).getValue()
//              .getName());
        Assert.assertEquals("miau", dogs.getDogs().get(2).getValue().getSound());
    }
}

The code is also available on GitHub here and here.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

The short answer is because the factory methods are not generated into the @XmlType annotation to tell JAXB to do so:

@XmlRootElement(name = "listOfDogs")
@XmlType(factoryClass=ObjectFactory.class, factoryMethod="createDogs") // not generated
public class Dogs {

Shouldn't it be? Or is ist just a dummy to hold the @XmlRegistry/@XmlElementDecl declarations?

In my opinion yes it should be used to instantiate the classes.

ObjectFactory is a throw back to JAXB 1.0. In JAXB 1.0 the spec defined what the generated interfaces looked like and implementations could back those generated interfaces with what ever impl they wanted to provide. Back then you needed to use the ObjectFactory class to create your model in a vendor independent way.

JAXB 2.0 switched to a POJO model where you were free to use the default constructor. If JAXB 1.0 had never existed would there be an ObjectFactory class, that's hard to tell. Since it previously existed the ObjectFactory class was kept for a couple of reasons:

  1. It made it easier for people transitioning for people transitioning from JAXB 1.0 to interact with the generated model.
  2. It provided a location to specify the multiple root elements for a class via @XmlElementDecl. The @XmlRegistry annotation is really just a marker annotation used to indicate the class that contains the @XmlElementDecl annotations without restricting it to a class called ObjectFactory.

Your Use Case

Your use case may be able to be achieved with an XmlAdapter, although its not clear to me what logic you are trying to have in the ObjectFactory.

XmlAdapter (DogAdapter)

Your custom logic goes on the XmlAdapter.

import javax.xml.bind.*;
import javax.xml.bind.annotation.adapters.*;

public class DogAdapter extends XmlAdapter<JAXBElement<DogType>, JAXBElement<DogType>> {

    @Override
    public JAXBElement<DogType> unmarshal(JAXBElement<DogType> v) throws Exception {
        return new Dog(v.getName().getLocalPart(), v.getValue());
    }

    @Override
    public JAXBElement<DogType> marshal(JAXBElement<DogType> v) throws Exception {
        return v;
    }

}

Dogs

The XmlAdapter is referenced from the @XmlJavaTypeAdapter annotation.

import java.util.*;
import javax.xml.bind.*;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement(name = "listOfDogs")
public class Dogs {

    private List<JAXBElement<DogType>> dogs = new LinkedList<JAXBElement<DogType>>();

    @XmlElementWrapper(name = "dogs")
    @XmlElementRef(name = "dog")
    @XmlJavaTypeAdapter(DogAdapter.class)
    public List<JAXBElement<DogType>> getDogs() {
        return this.dogs;
    }

    @Override
    public String toString() {
        return "Dogs [dogs=" + dogs + "]";
    }

}

ObjectFactory

ObjectFactory is now a dumb class that just holds the @XmlElementDecl annotations:

import javax.xml.bind.*;
import javax.xml.bind.annotation.*;
import javax.xml.namespace.QName;

@XmlRegistry
public class ObjectFactory {

    public Dogs createDogs() {
        return new Dogs();
    }

    @XmlElementDecl(name = "dog")
    public JAXBElement<DogType> createDog(DogType value) {
        return new Dog(value);
    }

    @XmlElementDecl(name = "fido", substitutionHeadName = "dog", substitutionHeadNamespace = "")
    public JAXBElement<DogType> createFido(DogType value) {
        return new JAXBElement<DogType>(new QName("fido"), DogType.class, value);
    }

    @XmlElementDecl(name = "barks", substitutionHeadName = "dog", substitutionHeadNamespace = "")
    public JAXBElement<DogType> createBarks(DogType value) {
        return new JAXBElement<DogType>(new QName("barks"), DogType.class, value);
    }

}

UPDATE

My question, however is more about the specification. According to the spec, should the create* methods from the ObjectFactory be executed or not?

In JAXB 2 there is no difference in a model created from scratch versus one generated from an XML Schema. As such you need to look to the spec at what it says about classes. According to what is reference below it comes down to no-arg constructor or a specified factory method.

From section 8.7.1.2 Mapping of the JAXB 2.2 (JSR-222) specification:

a class must have a public or protected no-arg constructor or a factory method identified by {factoryClass(), factoryMethod()} unless it is adapted using @XmlJavaTypeAdapter.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...