Introduction
Le but de ce papier est de présenter un aperçu de ce que la technologie Java JAXB permet de faire ainsi que de répondre aux questions que vous vous poserez peut-être (je me les suis bien posées). Ce document ne sera pas exhaustif sur la technologie mais présentera les facettes que j’ai pu explorer ; les problèmes que j’ai rencontré, et leurs solutions quand celles-ci existent. Les utilisateurs 'avancés' de JAXB ne trouveront rien de neuf sur cette page.
Autre point, je ne vais aborder JAXB que par sa facette basée sur les annotations Java ; manière de faire qui a l’avantage de ‘cacher’ les objets internes à JAXB et de rendre le code beaucoup plus élégant (amha).
Présentation rapide
JAXB - késako ?
JAXB signifie «Java Architecture for XML Binding». C’est à dire, c’est une technologie qui va permettre de faire correspondre à un modèle XML un ensemble de classes Java ; et cela de manière automatique ou non ( voir le chapitre xxx sur la génération du code à partir d’un schéma xml).
Exemples simples
Dans les exemples qui vont suivre, nous allons générer de l’XML à partir de nos classes Java. Nous verrons plus loin que ce n’est pas la seule façon de faire : nous pouvons aussi commencer à travailler sur un schéma xml à partir duquel nous générerons le code java.
Exemple simple numéro 1
Nous allons dans cet exemple générer une liste de valeurs, chacune contenue dans un même tag. Essayons de générer ce morceau de XML :
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <list> <name>test1</name> <name>test2</name> </list>
La classe suivante suffira pour faire l'affaire :
class Simple1
package org.beynet.testjaxb;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="list")
public class Simple1 {
@XmlElement(name="name")
public List<String> names = new ArrayList<String>();
}
Moyennant le code suivant :
Simple1 si = new Simple1();
si.names.add("étoile");
si.names.add("test2");
StringWriter sw = new StringWriter();
context.createMarshaller().marshal(si, sw);
System.out.println(sw.toString());
Nous verrons un peu plus tard ce que sont les classes JAXBContext et les Marshaller/Unmarshaller.
Exemple simple 2 :Création d’un type complexe
Création d’un type complexe Simple2ComplexType (voir élément complexType de la spécification xml schéma).
Classe Simple2Root :
package org.beynet.testjaxb;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="root")
public class Simple2Root {
@XmlElement(name="cpType")
public List<Simple2Complex> list = new ArrayList<Simple2Complex>();
}
Classe Simple2Complex :
package org.beynet.testjaxb;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
@XmlType(name="Simple2ComplexType")
public class Simple2Complex {
@XmlAttribute(name="type")
public int getType() {
return(this.type);
}
public void setType(int type) {
this.type = type ;
}
@XmlElement(name="name")
public String getName() {
return(this.name);
}
public void setName(String name) {
this.name = name ;
}
private int type ;
private String name;
}
Cet exemple va générer le fichier xml suivant :
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <root> <cpType type="3"> <name>name</name> </cpType> <cpType type="4"> <name>name 2</name> </cpType> </root>
Et cela seulement avec les lignes de codes suivantes :
JAXBContext context =JAXBContext.newInstance(Simple2Root.class);
Simple2Root root = new Simple2Root();
Simple2Complex cp1 = new Simple2Complex();
cp1.setType(3);
cp1.setName("name");
root.list.add(cp1);
Simple2Complex cp2 = new Simple2Complex();
cp2.setType(4);
cp2.setName("name 2");
root.list.add(cp2);
StringWriter sw = new StringWriter();
context.createMarshaller().marshal(root, sw);
System.out.println(sw.toString());
JAXBContext, Marshaller et Unmarshaller
Construction du JAXBContext
Mécanisme général
D'une manière ou d'une autre, un JAXContext est construit à partir de classes qu'on lui donne comme point d'entrée ; classes qui seront analysées et à partir desquelles il va ensuite charger toutes les autres classes définissant l'arbre des balises XML définies par nos annotations. Par exemple, en analysant la classe Simple2Root, le parser JAXB va remonter à la classe Simple2Complex.
Pour créer un JAXBContext, il faut utiliser l’une de ses méthodes newInstance. Pour cela, plusieurs philosophies existent.
A partir de la liste des classes ‘racines’ de l’arbre JAXB
Ce que j'appelle une classe racine est une classe définissant un élément XLML susceptible d'être la racine d'un document XML. C'est ce type de construction, à partir d'une classe racine que nous avons mis en oeuvre dans les exemples précédents :
JAXBContext context =JAXBContext.newInstance(Simple1.class);
et
JAXBContext context =JAXBContext.newInstance(Simple2Root.class);
Pour construire un JAXContext capable de produire à la fois le résultat de l’exemple 1 et celui de l’exemple 2 nous aurions pu écrire :
context=JAXBContext.newInstance(Simple1.class,Simple2Root.class);
Vous remarquerez que nous n’avons pas rajouté la classe Simple2Complex à la liste des classes passées au constructeur du JAXBContext car comme nous l'avons évoqué ici seuls les points d’entrées de l’arbre JAXB suffisent : toutes les classes utilisées comme Attribut ou Element sont automatiquement analysées.
Cette méthode est très simple mais suppose de rajouter dans les arguments de cette méthode newInstance chaque classe susceptible d’être utilisée
comme un noeud racine (ie. chaque classe susceptible d’être à la racine d’un fichier xml). Lorsque le schéma xml utilisé est très complexe, la liste de ces
arguments risque d’être assez longue. Dans ce cas, la méthode suivante pourrait vous satisfaire un peu plus.
A partir d’un nom de package
Il est possible de construire un JAXBContext à partir d’un nom de package. Pour gérer toutes les classes de nos exemples précédents, il aurait suffit de constuire le JAXBContext ainsi :
context=JAXBContext.newInstance("org.beynet.testjaxb");
Pour que cette méthode fonctionne, un fichier jaxb.index doit se trouver dans le package org.beynet.testjaxb ; toutes les classes qui y
sont référencées seront analysées lors de la construction du JAXBContext. Pour faire fonctionner nos exemples précédents avec cette méthode,
le fichier jaxb.index doit contenir les deux lignes suivantes :
Simple1 Simple2Root
Remarque : le nom des classes dans ce fichier doit être relatif au nom du package donné à l’appel de la méthode
newsIntance. Si par exemple on veut ‘faire
connaître’ à notre JAXBContext une classe nommée org.beynet.testjaxb.souspackage.Simple3 le fichier jaxb.index devra désormais contenir les lignes suivantes :
Simple1 Simple2Root souspackage.Simple3
Marshaller en Unmarshaller
Ces interfaces représentent respectivement les objets permettant de serialiser l'XML (sur disque, dans une chaîne de caractère, ...) et de dé-serialiser un contenu XML (à parir du disque, d'un buffer d'octets, ...).
Configurabilité
Marshaller et unmarshaller peuvent être configurés par des propriétés. Certaines sont spécifiques à l’implémentation, d’autre sont (devraient) être standard.
Exemple :
Générer l'attribut schemaLocation : pour pouvoir valider un document.
ma.setProperty( Marshaller.JAXB_SCHEMA_LOCATION, "http://mynamespace1.fresnes.info name1.xsd http://mynamespace2.fresnes.info name2.xsd");
Changer l'encoding des documents XML produits
Par défaut, le Marshaller va générer des documents XML en 'UTF-8'. Ce comportement peut-être changé ainsi :
ma.setProperty(Marshaller.JAXB_ENCODING,"UTF-16");
Problématique de l'encoding
Comme nous l'avons vu ici, l'encoding produit peut être changé. Il existe toutefois des pièges classiques dans lesquels on peut facilement tomber si l'on est un peu étourdis. Voici une petite liste des erreurs les plus classiques.
Sérialisation dans un objet String et écriture dans un autre encoding que celui promis dans l'entête XML.
Nous allons montrer ici comment mettre en incohérence l'xml produit et l'encoding effectivement produit. Pour cela, nous allons sérialiser un objet JAXB dans une chaîne puis écrire cette chaine dans un fichier. Pour cela, reprenons le StringWriter généré dans notre exemple 1 ; récupérons son contenu dans une chaîne et écrivons la dans le fichier /tmp/res.xml. Extrait du code :
String result = sw.toString();
FileOutputStream fo = new FileOutputStream(new File("/tmp/res.xml"));
try {
fo.write(result.getBytes());
} finally {
if (fo!=null) fo.close();
}
Partons du postulat, juste pour illustrer notre exemple, que votre environnement est configuré en ISO, et que vous n'avez pas surchargé cette configuration au niveau de votre JVM.
La ligne suivante :
fo.write(result.getBytes());
est équivalente à la suivante :
fo.write(result.getBytes("ISO-8859-1"));
Nous venons donc d'écrire notre document xml sur le disque dans un autre encoding que celui promis par le header xml. Lorsque l'on serialize un graphe d'objets JAXB, il faut donc toujours savoir dans quel encoding notre marshaller est configuré. Si, par exemple, on sait dans notre programme que nous avons un marshaller configuré pour produire de l'UTF-8, un buffer xml se sauvegardera ainsi :
fo.write(result.getBytes("UTF-8"));
Fournir l'encoding lors du unmarshall
Il faut garder en tête que l'encoding d'un document xml n'est pas forcément contenu à l'intérieur : l'entête xml peut ne pas être présente, il peut ne pas y avoir de marqueur BOM. De la même façon, l'encoding d'un fichier xml reçu via le réseau n'est pas forcément précisé dans son sein : en http il peut par exemple être précisé dans l'entête de la requête. Dans tous ces cas, il vous faut aider un peu le parseur, par exemple en fournissant au unmarshaller un objet InputSource avec l'encoding de précisé.
ByteArrayInputStream is = new ByteArrayInputStream(content);
InputSource source = new InputSource();
source.setByteStream(is);
source.setEncoding(encoding);
Unmarshaller un = null;
Object result = null;
un = getUnmarshaller();
try {
result = un.unmarshal(source);
} catch (JAXBException e) {
throw new Exception("Could not parse document - Jaxb ERROR", e);
}
Performances
La construction du JAXBContext peut s’avérer longue, de manière proportionnelle au nombre de classes à analyser et à la complexité du schéma décrit. Il peut s’avérer utile de se débrouiller pour ne le construire qu’une seule fois par exécution de vos programmes.
Personnellement, je l’initialise en statique. La classe JAXBContext obtenue pourra être utilisée dans un environnement multi-thread car elle est thread-safe. Attention, ce n’est pas le cas des classes implémentant les interfaces Marshaller et Unmarshaller. Celles-ci ne devront donc pas être utilisées de manière concurrentes par plusieurs Thread. Elles sont par contre réutilisables ; c’est à dire un marshaller pourra être utilisé pour marshaller plusieurs documents et de la même manière le unmarshaller pourra être ré-utilisé.
Travailler avec plusieurs namespaces
Il peut s'avérer utile de travailler avec plusieurs namespaces ; par exemple lorsque l'on veut étendre la syntaxe XML d'un format défini par un tier.
Par exemple, vous connaissez sans doute le format atom. C'est ce format qui est utilisé par google pour représenter la liste des documents d'un compte google docs lorqu'on y accède via leur api web. Les développeurs de chez google on décidé d'étendre atom pour rajouter des attributs qui leur sont spécifiques plutôt que d'inventer un schéma XML en entier.
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:docs="http://schemas.google.com/docs/2007" xmlns:gAcl="http://schemas.google.com/acl/2007" xmlns:gd="http://schemas.google.com/g/2005" gd:etag="W/"CkcMQH0zcSt7ImA9WhdQF0w.""> <id>https://docs.google.com/feeds/metadata/user%40example.com</id> <updated>2011-08-18T23:28:01.389Z</updated> <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/docs/2007#metadata" label="metadata"/> <title>Document List User Metadata</title> <link rel="self" type="application/atom+xml" href="https://docs.google.com/feeds/metadata/user%40example.com"/> <author> <name>Some User</name> <email>user@example.com</email> </author> <gd:quotaBytesTotal>1073741824</gd:quotaBytesTotal> <gd:quotaBytesUsed>17936436</gd:quotaBytesUsed> ...
Comme vous pouvez le constater, la balise quotaBytesTotal est une extension faite au schéma Atom.
Le fichier package-info.java
Pour pouvoir déclarer un namespace pour les documents que vous allez générer, il faut créer un fichier de nom package-info.java dans
chaque package qui contiendra des classes faisant partie du graphe d'objet définissant vos objets XML.
Namespace par défaut
Pour que l'XML produit par l'exemple 1 soit désormais :
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <list xmlns="http://fresnes.info/simple1/2011-11-11/"> <name>test1</name> <name>test2</name> </list>
Il suffit que le fichier package-info.java existe dans le package org.beynet.testjaxb et contienne les lignes suivantes:
@javax.xml.bind.annotation.XmlSchema(
namespace ="http://fresnes.info/simple1/2011-11-11/",
elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
package org.beynet.testjaxb;
Plusieurs namespaces utilisés au sein des classes d'un même package :
Etendons un peu notre exemple 1 pour générer l'XML suivant : une nouvelle balise definition dans un autre namespace
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <list xmlns="http://fresnes.info/simple1/2011-11-11/"> <name>test1</name> <name>test2</name> <extend:definition>test3</extend:definition> </list>
class Simple1Extended.java
package org.beynet.testjaxb;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="list")
public class Simple1Extended {
@XmlElement(name="name")
public List<String> names = new ArrayList<String>();
@XmlElement(name="definition",namespace="http://fresnes.info/simple2/2011-11-11/")
public String definition ;
}
Pour générer cet XML, il faut aussi déclarer le nouveau namespace utilisé dans le fichier package-info.java, selon :
@javax.xml.bind.annotation.XmlSchema(
namespace ="http://fresnes.info/simple1/2011-11-11/",
xmlns = {
@XmlNs( namespaceURI = "http://fresnes.info/simple1/2011-11-11/", prefix = ""),
@XmlNs( namespaceURI = "http://fresnes.info/simple2/2011-11-11/", prefix = "extend"),
},
elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
package org.beynet.testjaxb;
import javax.xml.bind.annotation.XmlNs;
On voit que la liste des namespaces déclarés apparaît dans l'élément xmlns, chacun dans un élément XmlNs. Attention, avant la version
2.2 de l'implémentation jaxb, j'ai constaté que la directive xmlns ne semblait pas être prise en compe. Pour être plus précis, j'ai du explicitement utiliser
une version plus récente de l'implementation JAXB que celle fournie de base dans ma JVM. J'ai rajouté les dépendances suivantes dans mon fichier
pom.xml
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.2.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.2.2</version> <scope>test</scope> </dependency>
Meilleures pratiques de codage
Meilleures pratiques, le terme est un peu pompeux, j'aurais plutôt du dire mes pratiques préférées ou conseillées ; mais bon, j'asume.
Séparer les classes de namespace différent
Nous l'avons vu, il est possible dans une même classe de facilement mixer plusieurs namespaces : toutes les annotations JAXB permettent de préciser le namespace pour l'élément ou l'attribut décrit. Mais pour des raisons de clarté du code, lorsque c'est possible, il est souhaitable de séparer les classes qui décrivent des objets java dont les pendants en XML ne sont pas dans un même namespace.
Les points d'extension d'extension Xml Schéma ( tag Any ) en JAXB
Représentation en java
Lorsque un développeur veut autoriser des points d'extension au schéma xml qu'il est en train d'écrire,
il utilise le tag <any>. Cette balise se traduit ainsi en code java :
@XmlAnyElement(lax=true) @XmlMixed public List<Object> extension = new ArrayList<Object>();
L'annotation XmlAnyElement correspond à la balise xml schéma any ; l'autre annotation permet, comme son nom l'indique plutôt bien, d'avoir au niveau de ce point d'extension un contenu
de type 'mixed content'. Vu que nous voulons notre point d'extension le plus générique possible, c'est une précaution (rappelons le, le
contenu de type mixed est un contenu pouvant être composé à la fois de texte et de balises.
Fonctionnement un peu plus détaillé
Lors du processus de unmarshalling, si l'attribut lax de l'annotation XmlAnyElement n'est pas présent ou si il vaut false, tous les éléments trouvés dans ce point
d'extension seront représentés en DOM. A l'inverse, si lax=true, lorsque un élement trouvé au niveau du point d'extension correspond à un élément connu du context JAXB, il sera représenté par sa
classe JAXB. Je vous invite à lire la javadoc ici pour des explications plus détaillées.