46 votes

Héritage JAXB, démarchage vers une sous-classe de la classe marshalée

J'utilise JAXB pour lire et écrire du XML. Ce que je veux, c'est utiliser une classe JAXB de base pour le marshalling et une classe JAXB héritée pour le unmarshalling. Le but est de permettre à une application Java émettrice d'envoyer du XML à une autre application Java réceptrice. L'émetteur et le récepteur partageront une bibliothèque JAXB commune. Je veux que le récepteur démarchande le XML dans une classe JAXB spécifique au récepteur qui étend la classe JAXB générique.

Ejemplo:

C'est la classe commune JAXB qui est utilisée par l'expéditeur.

@XmlRootElement(name="person")
public class Person {
    public String name;
    public int age;
}

Il s'agit de la classe JAXB spécifique au récepteur utilisée lors du désarchivage du XML. La classe du récepteur a une logique spécifique à l'application du récepteur.

@XmlRootElement(name="person")
public class ReceiverPerson extends Person {
    public doReceiverSpecificStuff() ...
}

La maréchaussée fonctionne comme prévu. Le problème est que lors de la désarchivage, il y a toujours désarchivage vers Person malgré le JAXBContext en utilisant le nom du paquetage de la sous-classe ReceiverPerson .

JAXBContext jaxbContext = JAXBContext.newInstance(package name of ReceiverPerson);

Ce que je veux, c'est dé-maréchaliser pour ReceiverPerson . Le seul moyen que j'ai trouvé pour y parvenir est de supprimer @XmlRootElement de Person . Malheureusement, cela empêche Person de l'être. C'est comme si JAXB commençait par la classe de base et descendait jusqu'à trouver la première @XmlRootElement avec le nom approprié. J'ai essayé d'ajouter un createPerson() qui renvoie la méthode ReceiverPerson a ObjectFactory mais ça n'aide pas.

21voto

Pascal Thivent Points 295221

Le snippet suivant est une méthode d'un test Junit 4 avec une lumière verte :

@Test
public void testUnmarshallFromParentToChild() throws JAXBException {
  Person person = new Person();
  int age = 30;
  String name = "Foo";
  person.name = name;
  person.age= age;

  // Marshalling
  JAXBContext context = JAXBContext.newInstance(person.getClass());
  Marshaller marshaller = context.createMarshaller();

  StringWriter writer = new StringWriter();
  marshaller.marshal(person, writer);

  String outString = writer.toString();

  assertTrue(outString.contains("</person"));

  // Unmarshalling
  context = JAXBContext.newInstance(Person.class, RecieverPerson.class);
  Unmarshaller unmarshaller = context.createUnmarshaller();
  StringReader reader = new StringReader(outString);
  RecieverPerson reciever = (RecieverPerson)unmarshaller.unmarshal(reader);

  assertEquals(name, reciever.name);
  assertEquals(age, reciever.age);
}

La partie importante est l'utilisation de la JAXBContext.newInstance(Class... classesToBeBound) pour le contexte de désarchivage :

 context = JAXBContext.newInstance(Person.class, RecieverPerson.class);

Avec cet appel, JAXB calculera une fermeture de référence sur les classes spécifiées et reconnaîtra les éléments suivants RecieverPerson . Le test passe. Et si vous changez l'ordre des paramètres, vous obtiendrez une java.lang.ClassCastException (donc ils doit être adopté dans cet ordre).

0 votes

C'est celle qui fonctionne le mieux, pas besoin d'adaptateurs si vous pouvez utiliser les annotations JAXB pour les classes.

20voto

Vous utilisez JAXB 2.0, n'est-ce pas ? (depuis JDK6)

Il y a une classe :

javax.xml.bind.annotation.adapters.XmlAdapter<ValueType,BoundType>

que l'on peut sous-classer, et surcharger les méthodes suivantes :

public abstract BoundType unmarshal(ValueType v) throws Exception;
public abstract ValueType marshal(BoundType v) throws Exception;

Ejemplo:

public class YourNiceAdapter
        extends XmlAdapter<ReceiverPerson,Person>{

    @Override public Person unmarshal(ReceiverPerson v){
        return v;
    }
    @Override public ReceiverPerson marshal(Person v){
        return new ReceiverPerson(v); // you must provide such c-tor
    }
}

L'utilisation se fait de la manière suivante :

@Your_favorite_JAXB_Annotations_Go_Here
class SomeClass{
    @XmlJavaTypeAdapter(YourNiceAdapter.class)
    Person hello; // field to unmarshal
}

Je suis presque sûr qu'en utilisant ce concept, vous pouvez contrôler vous-même le processus de marshalling/dunmarshalling (y compris le choix du [sub|super]type correct à construire).

0 votes

J'ai essayé ça et ça ne marche pas. Peu importe ce que j'essaie, ObjectFactory ou mon XmlAdapter ne sont jamais appelés. D'après ce que j'ai lu, le JAXB de Sun résout les problèmes par des références de classes statiques. Il semble que l'implémentation de Glassfish soit plus prometteuse. jaxb.dev.java.net/guide/Addition de comportements.html

0 votes

Les XmlJavaAdapters sont adaptés pour rendre les classes non annotées par JAXB utilisables avec JAXB. Comme Person et ReceiverPerson (et/ou leur super-classe, le cas échéant) ont des annotations JAXB, cela ne fonctionne pas. Vous avez besoin de Person et ReceiverPerson sans JAXB, ainsi que d'adaptateurs pour les deux.

0 votes

Avez-vous essayé d'échanger les types pour marshall|unmarshall ? [...] extends XmlAdapter<Person,ReceiverPerson> [...] @Override public ReceiverPerson unmarshal(Person v){ return new ReceiverPerson(v) ; } @Override public Person marshal(ReceiverPerson v){ return v ; }

13voto

13ren Points 3672

Sous-classez deux fois Person, une fois pour le récepteur et une fois pour l'expéditeur, et ne mettez le XmlRootElement que sur ces sous-classes (en laissant la superclasse, Person sans XmlRootElement). Notez que l'émetteur et le récepteur partagent les mêmes classes de base JAXB.

@XmlRootElement(name="person")
public class ReceiverPerson extends Person {
  // receiver specific code
}

@XmlRootElement(name="person")
public class SenderPerson extends Person {
  // sender specific code (if any)
}

// note: no @XmlRootElement here
public class Person {
  // data model + jaxb annotations here
}

[testé et confirmé pour fonctionner avec JAXB]. Il contourne le problème que vous notez, lorsque plusieurs classes dans la hiérarchie d'héritage ont l'annotation XmlRootElement.

On peut dire que c'est aussi une approche plus soignée et plus OO, car elle sépare le modèle de données commun, ce qui n'est pas du tout une "solution de rechange".

0 votes

Cela semble être la meilleure solution mais ne fonctionne pas pour moi, peut-être parce que mon cas est légèrement compliqué. J'ai besoin de convertir des instances de la classe 'Group' qui contient à la fois la liste des SenderPerson et des ReceiverPerson. Quelque chose comme ça @XmlRootElement public class Group { public List<SenderPerson> senders ; public List<ReceiverPerson> receivers ; }

7voto

vocaro Points 1995

Créez un ObjectFactory personnalisé pour instancier la classe désirée pendant le désarchivage. Exemple :

JAXBContext context = JAXBContext.newInstance("com.whatever.mypackage");
Unmarshaller unmarshaller = context.createUnmarshaller();
unmarshaller.setProperty("com.sun.xml.internal.bind.ObjectFactory", new ReceiverPersonObjectFactory());
return unmarshaller;

public class ReceiverPersonObjectFactory extends ObjectFactory {
    public Person createPerson() {
        return new ReceiverPerson();
    }
}

0 votes

Cela me semble être une méthode très intelligente car elle nécessite de créer chaque objet une seule fois - comparé à la solution de XmlAdpater où vous créez d'abord l'instance de la classe de type liée et ensuite copiez tous les attributs vers l'instance de la classe de valeur. Mais la question est la suivante : Est-ce que cela fonctionne aussi avec des JREs différents de Sun/Oracle comme OpenJDK ?

0 votes

+1 pour être la seule solution pour moi. J'essaie d'utiliser la substitution de type pour la sous-classification (implClass) dans JAXB 2.* dans un fichier xsd et les classes qui ne sont pas mises en évidence sont des classes générées autres que celles spécifiées dans implClass. La réponse de vocaro résout le problème !

1 votes

Mise à jour mineure pour le cas où JAXB est une implémentation externe (pas celle du SDK) : la propriété est appelée "com.sun.xml.bind.ObjectFactory" .

3voto

TofuBeer Points 32441

Je ne sais pas pourquoi vous voulez faire ça... ça ne me semble pas très sûr.

Considérez ce qui se passerait si ReceiverPerson avait des variables d'instance supplémentaires... alors vous vous retrouveriez avec (je suppose) ces variables qui seraient null, 0 ou false... et que se passerait-il si null n'est pas autorisé ou si le nombre doit être supérieur à 0 ?

Je pense que ce que vous voulez faire, c'est lire la personne et construire un nouveau ReceiverPerson à partir de celle-ci (en fournissant probablement un constructeur qui prend une personne).

0 votes

ReceiverPerson n'a pas de variables supplémentaires. Son but est de faire des choses spécifiques au récepteur.

0 votes

Vous allez toujours devoir, à un certain niveau, le créer en tant que classe différente... et la façon normale de le faire est via un constructeur qui prend le type que vous voulez convertir. De plus, ce n'est pas parce que c'est le cas maintenant que vous ne pourrez pas ajouter de variables à l'avenir.

3 votes

Oups, c'est beaucoup trop de spéculation sur ce qu'une classe pourrait faire dans le futur. Si c'est assez bon pour aujourd'hui alors c'est assez bon, et vous pouvez toujours remanier plus tard...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X