Il y a 12 ans -
Temps de lecture 10 minutes
JAXB, le parsing XML — objet
Format privilégié pour les échanges inter-applications, XML est l’objet de nombreuses bibliothèques Java. Cependant, ces bibliothèques masquent toutes le data binding qu’elles effectuent ; la transformation d’un document XML en grappe d’objets. Nous voilà bien démunis dès lors qu’une application produit du XML comme une simple chaîne de caractères. L’utilisation d’API bas niveau (DOM, XPath) — attachées à la structure du document — se révélant fastidieuse, la majorité des implémentations JAX-RS (Jersey, CXF) ont retenu la même API de haut niveau — concentrée sur les données — : JAXB. Faisons de même.
- Décrire un format d’échange
- Raffiner le format d’échange
- Épilogue : un contrat d’échange côté serveur
Décrire un format d’échange
Lorsqu’une application produisant du XML n’a pas de mécanisme pour partager l’agencement de ses noeuds, il incombe à ses consommateurs de retenir une méthode pour l’exploiter au mieux. Cela peut être réalisé via une grappe d’objets équivalente à la sortie XML ; voyons comment à l’aide d’un dessert savoureux.
<recipe name="Compote de poires" type="dessert"> <cooking duration="15"> <step optional="true">Réserver une gousse de vanille</step> <step>Éplucher et évider les poires</step> <step>Découper les poires en quartiers</step> <step>Verser les quartiers dans une casserole avec l'eau</step> </cooking> <menu>17-02-2011</menu> <menu>17-03-2011</menu> </recipe>
JAXB identifie chaque noeud comme un élément doté d’attributs. Un élément est un type complexe doté d’une séquence d’éléments (toujours en premier) puis d’une liste d’attributs. Un élément peut porter une simple valeur textuelle lorsqu’il n’a pas de sous-noeuds. Un attribut ne dispose que d’une valeur textuelle. Chaque noeud XML est représenté par un élément du schéma puis, à l’aide de XJC, par un objet du même nom généré à partir de ce schéma (la génération XJC est abordée en annexe).
Voici la description du XML précédent à l’aide d’un XSD :
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="recipe"> <xsd:complexType> <xsd:sequence> <xsd:element name="menu" type="xsd:string" maxOccurs="unbounded" /> <xsd:element name="cooking" type="cooking" /> </xsd:sequence> <xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string" /> </xsd:complexType> </xsd:element> <xsd:complexType name="cooking"> <xsd:sequence> <xsd:element name="step" type="step" maxOccurs="unbounded" /> </xsd:sequence> <xsd:attribute name="duration" type="xsd:int" /> </xsd:complexType> <xsd:complexType name="step" mixed="true"> <xsd:attribute name="optional" type="xsd:boolean" /> </xsd:complexType> </xsd:schema>
Les noeuds de ce schéma sont tous précédés de « xsd » car leur définition, sur la première ligne, nomme le XML Namespace (xmlns) ainsi. Les namespaces sont une manière de différentier les déclarations des imports d’en-tête les uns des autres, comme le ferrait un package en java.
Les variables sont typées selon la recommandation w3c. Lorsqu’une liste de types primitifs est nécessaire (noeud menu) sa déclaration n’occasionnera pas la création d’une classe, seulement d’un attribut typé. En revanche si cette liste dispose d’attributs (noeud step), il est nécessaire d’indiquer que son contenu est mixte : par défaut les éléments ont soit une valeur textuelle, soit une liste d’attributs et d’éléments, pas les deux.
Voici le code généré à partir du schéma précédent :
@XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "recipe") public class Recipe { protected List<String> menu; protected Cooking cooking; @XmlAttribute protected String name; @XmlAttribute protected String type; } @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "cooking") public class Cooking { protected List<Step> step; @XmlAttribute protected Integer duration; } @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "step") public class Step { @XmlValue protected String content; @XmlAttribute protected Boolean optional; }
Seule une classe est annotée @XmlRootElement. Elle est la seule (dans ce schéma) à pouvoir jouer le rôle de premier noeud. Une fois la grappe résultat générée, 3 lignes de code suffisent à réaliser le parsing XML :
public class JaxbTest { @Test public void should_parse_recipe() throws JAXBException { URL xmlUrl = Resources.getResource("recipe.xml"); Recipe recipe = parse(xmlUrl, Recipe.class); assertEquals(Integer.valueOf(15), recipe.getCooking().getDuration()); } private <T> T parse(URL url, Class<T> clazz) throws JAXBException { Unmarshaller unmarshaller = JAXBContext.newInstance(clazz).createUnmarshaller(); return clazz.cast(unmarshaller.unmarshal(url)); } }
Contrairement aux bibliothèques de bas niveau, aucune conversion de type n’est nécessaire. La déclaration du type dans le schéma suffit à nous affranchir de cette responsabilité. En cas d’erreur de conversion (une chaine non convertible dans le type attendu) l’IllegalArgumentException correspondante est levée. En l’absence d’un noeud, les variables correspondantes sont null.
Raffiner le format d’échange
Une fois un data binding réussi, plusieurs opérations sont couramment nécessaires :
- limiter un champ à un ensemble fini de valeurs ;
- manipuler des dates plus simples que le XMLGregorianCalendar manipulé par défaut par JAXB ;
- nommer une classe différemment de l’élément qu’elle représente ;
- utiliser l’héritage entre éléments ;
- annoter manuellement des classes existantes.
Limiter un champ à un ensemble fini de valeurs
Limiter l’attribut « type » de recette à « entrée, plat, dessert » peut être fait de la façon suivante :
<xsd:element name="recipe"> <xsd:attribute name="type" type="formule" /> </xsd:element> <xsd:simpleType name="formule"> <xsd:restriction base="xsd:string"> <xsd:enumeration value="entree"></xsd:enumeration> <xsd:enumeration value="plat"></xsd:enumeration> <xsd:enumeration value="dessert"></xsd:enumeration> </xsd:restriction> </xsd:simpleType>
Ce qui donne, une fois généré :
@XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "recipe") public class Recipe { @XmlAttribute protected Formule type; } @XmlEnum public enum Formule { @XmlEnumValue("entree") ENTREE("entree"), @XmlEnumValue("plat") PLAT("plat"), @XmlEnumValue("dessert") DESSERT("dessert"); private final String value; }
Lors du binding, s’il s’avérait que la valeur du champ ne corresponde pas à l’une de celles spécifiées ici, la valeur null serait retournée. JAXB fait le choix de positionner ses variables à null en cas de problème de valeur. Les problèmes de typage, eux, lèvent tous une exception.
Manipuler des dates simples
Pour manipuler des dates Java en lieu et place du XMLGregorianCalendar utilisé par défaut par JAXB, un convertisseur va être nécessaire. Attention, un nouveau namespace est nécessaire à sa déclaration. Il permet de redéfinir le typage de JAXB.
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:jxb="http://java.sun.com/xml/ns/jaxb" jxb:version="2.0"> <xsd:annotation><xsd:appinfo> <jxb:globalBindings> <jxb:javaType name="java.util.Date" xmlType="xsd:dateTime" parseMethod="com.xebia.jaxb.JaxbDateConverter.parseDateTime" /> </jxb:globalBindings> </xsd:appinfo></xsd:annotation> <xsd:element name="menu" type="xsd:dateTime" /> </xsd:schema>
Le convertisseur doit respecter la logique JAXB, si la valeur en entrée ne convient pas, aucune exception n’est levée et la valeur null est renvoyée.
public class JaxbDateConverter { public static Date parseDateTime(String s) { DateFormat formatter = new SimpleDateFormat("dd-MM-yyyy"); try { return formatter.parse(s); } catch (ParseException e) { return null; } } }
Lors de la génération, JAXB crée une classe adapter qu’il lie aux méthodes statiques du converter.
@XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "recipe") public class Recipe { @XmlElement(type = String.class) @XmlJavaTypeAdapter(Adapter1.class) @XmlSchemaType(name = "dateTime") protected List<Date> menu;
Nommer une classe différemment de l’élément qu’elle représente
Pour nommer une classe différemment de l’élément qu’elle représente, il suffit d’ajouter les modifications suivantes à un schéma (attention à bien indiquer les deux namespaces) :
<xsd:element name="menu" type="xsd:dateTime" /> <xsd:annotation><xsd:appinfo> <jxb:class name="meal" /> </xsd:appinfo></xsd:annotation>
Utiliser l’héritage entre éléments
Utiliser l’héritage entre objet est aisé (les objets générés hériteront l’un de l’autre, bien entendu) :
<xsd:complexType name="menuxl"> <xsd:complexContent> <xsd:extension base="menu" /> </xsd:complexContent> <xsd:attribute name="cook" type="xsd:string" /> </xsd:complexType>
Annoter manuellement des classes existantes
Jusqu’ici nous avons fait reposer les objets d’échange sur une génération à l’aide d’un schéma. Annoter manuellement une classe en vue de lui binder du XML est également possible. Le schema correspondant peut même être généré à posteriori à partir des sources.
L’utilisation d’annotations manuelles permet d’avoir un controle plus fin sur la grappe d’objets, leur type et, pourquoi pas, d’utiliser des objets déjà utiles par ailleurs (ajouter un champ non concerné par le binding se fait à l’aide @XmlTransient).
Afin de respecter la logique JAXB, il est toutefois nécessaire de modifier les accesseurs des listes. Par convention, en l’absence de valeur, le code généré retourne des listes vides plutôt que null. JAXB, lors de la génération d’objets effectue cela via la modification des getters. Il est recommandé de faire de même.
public List<String> getMenu() { if (menu == null) { menu = new ArrayList<String>(); } return menu; }
Épilogue : un contrat d’échange côté serveur
Jusqu’ici nous avons considéré qu’aucun mécanisme décrivant l’agencement des noeuds XML n’était fourni du producteur aux consommateurs ; qu’il était résolu à postériori par ces derniers. Il est préférable, lorsque c’est possible, de les affranchir de cette contrainte en leur communiquant le schéma avec les données.
Pour ce faire, il est judicieux d’opter pour un mode de développement dirigé par le contrat. Générer les objets — côté serveur — de la couche d’échange au lieu de les annoter limite le couplage entre le schéma et son implémentation. Se restreindre aux possibilités de typage et d’agencement d’un schéma XSD garantit la compatibilité du format à tout type de consommateur (notamment autre que java). La documentation de référence Spring détaille ce propos en l’illustrant.
Annexe : outillage
Afin d’effectuer les manipulations présentées ici deux outils sont nécessaires, la dépendance maven de JAXB et le plugin de génération associé dont voici une version simple (exécutable via mvn jaxb2:xjc) :
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxb2-maven-plugin</artifactId> <configuration> <outputDirectory>${basedir}/src/main/java</outputDirectory> <schemaDirectory>${basedir}/src/main/resources/xsd</schemaDirectory> <packageName>com.xebia.jaxb.generated</packageName> <schemaFiles>schema.xsd</schemaFiles> </configuration> </plugin>
Pour prolonger l’aventure du data binding avec JAXB, une riche documentation est disponible en ligne.
Commentaire
7 réponses pour " JAXB, le parsing XML — objet "
Published by Thomas Huguerre , Il y a 12 ans
Pour l’avoir utilisé sur de gros projets basés sur des échanges de flux Xml, je recommande chaudement cet outil. Et l’association avec un outil du genre XmlMerge devient magique…
Published by Sébastien Lorber , Il y a 12 ans
A noter que certaines implémentations JAXB peuvent avoir quelques trucs sympa en plus.
Comme @XmlPath pour MOXy, parfois bien utile si on veut (un)marshaller un objet qui n’est pas le reflet direct du xml produit/consommé.
Published by Sébastien , Il y a 12 ans
A préciser aussi que Jaxb est devenu de plus en plus performant au fil des implémentations, si bien qu’il a dépassé Jibx qui tenait le haut du classement depuis longtemps.
Published by Mihinot , Il y a 11 ans
Bonjour,
Je m’imprègne actuellement de JAXB et là ce tuto m’est d’ue grande aide, merci encore. Par contre je dois vraiment mal m’y prendre mais dans la classe de test suivante :
public class JaxbTest {
@Test
public void should_parse_recipe() throws JAXBException {
URL xmlUrl = Resources.getResource(« recipe.xml »);
…
}
}
Je ne parviens pas à trouver à quelle package la classe « Resources » appartient. J’utilise maven mais je ne sais pas non plus quelle dépendance je devrais renseigner. j’ai trouvé celle de google (guava) qui implémente une classe semblable mais une fois que je lance le test, il m signale des erreur au niveau de celle-ci. please, de l’aide :)
Merci.
Published by Brother_from_Chatx , Il y a 11 ans
SAlut Bro,
Comment ça vas ?
Petite remarque concernant le plugin maven :
je te conseille vivement pour des raisons de paramétrage et de maintenance du plugin maven d’utiliser le plugin suivant :
http://java.net/projects/maven-jaxb2-plugin/pages/Home
au lieu de celui org.codehaus.mojo
Published by Ric , Il y a 9 ans
Bonjour,
Est-ce que les objets associés à un objet sont inclus par JAXB dans la représentation XML de l’objet ? Est-ce paramétrable ? J’ai une entité avec une association 1-N et une association N-1. L’objet de l’association N-1 est inclus dans le XML mais pas la collection des objets de l’association 1-N. Je suis débutant en JAXB et je n’arrive pas à trouver de documentation sur ce problème.
Published by developper , Il y a 8 ans
you can find a complete example
http://j2eeandroid.blogspot.com/2014/12/tutorial-jaxb-2.html