Java EE / Jakarta EE

XML en Java : SAX, DOM, StAX et Jakarta XML Binding (JAXB 4)

Intermédiaire9 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

XML reste le format de nombreuses intégrations d'entreprise, de fichiers de configuration et de standards documentaires, et Java propose trois modèles de parsing ainsi qu'un framework de binding pour le traiter. Ce guide compare DOM, SAX et StAX sur des exemples fonctionnels, explique la validation par schéma et XPath, montre comment Jakarta XML Binding (JAXB) fait correspondre classes et XML dans les deux sens depuis son retrait du JDK, et détaille le durcissement des parsers dont toute application consommant du XML a besoin.

Ce que vous allez apprendre

  • Choisir entre DOM, SAX et StAX selon la taille du document et le mode d'accès
  • Parser, parcourir et écrire du XML avec les API JAXP du module java.xml
  • Valider des documents contre un XML Schema et les interroger avec XPath
  • Marshaller et unmarshaller des classes Java avec les annotations de Jakarta XML Binding 4, et générer des classes à partir d'un schéma
  • Configurer chaque parser pour empêcher les attaques XML External Entity (XXE) et d'expansion d'entités

JSON a gagné le web, mais XML n'a jamais quitté l'entreprise. Il arrive sous forme d'enveloppes SOAP, de relevés bancaires, de messages normalisés par des standards sectoriels, d'assertions SAML, de documents bureautiques et de la configuration du serveur d'applications lui-même, et il vient le plus souvent de l'extérieur de votre périmètre de confiance. La bibliothèque standard de Java parse du XML depuis 2002 grâce à JAXP (Java API for XML Processing, aujourd'hui le module java.xml), qui propose trois modèles : un arbre en mémoire, un flux d'événements qu'on vous pousse, ou un flux d'événements que vous tirez. Par-dessus, Jakarta XML Binding (JAXB) fait correspondre directement le XML à des classes Java, si bien que la plupart du code applicatif ne voit jamais un seul élément. Ce guide couvre les quatre approches et, parce que les parsers XML sont une surface d'attaque bien connue, les réglages qui les rendent sûrs.

Trois modèles de parsing

DOMSAXStAX
ModèleDocument entier sous forme d'arbre en mémoirePush : le parser appelle votre handler à chaque événementPull : vous demandez l'événement suivant au reader
MémoireProportionnelle à la taille du document (plusieurs fois le nombre d'octets)ConstanteConstante
AccèsAléatoire ; parcourir, modifier, réécrireEn avant uniquement, lecture seuleEn avant uniquement ; API d'écriture séparée
SimplicitéLe plus simple pour les petits documentsLe plus difficile : machine à états dans des callbacksCode linéaire, facile d'arrêter ou de déléguer
À utiliser pourConfiguration, petits messages, documents que vous modifiez ou interrogez en XPathTrès gros fichiers, extraction de quelques valeurs, code legacyGros fichiers, transformations en streaming, écriture de XML à la main

Les trois s'obtiennent via des factories de javax.xml.parsers et javax.xml.stream. Les packages javax.xml relèvent de Java SE, pas de Jakarta EE, et ne sont pas passés en jakarta.* ; seule l'API de binding a migré.

DOM : l'arbre

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.*;

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);  // no DTD, no XXE
factory.setExpandEntityReferences(false);

Document doc = factory.newDocumentBuilder().parse(new File("orders.xml"));
NodeList lines = doc.getElementsByTagNameNS("urn:shop:orders", "line");
for (int i = 0; i < lines.getLength(); i++) {
    Element line = (Element) lines.item(i);
    String sku = line.getAttribute("sku");
    int qty = Integer.parseInt(line.getElementsByTagNameNS("urn:shop:orders", "qty").item(0).getTextContent());
}

L'API DOM du W3C est verbeuse (NodeList n'est pas itérable, le texte est un nœud enfant et non une propriété, les espaces entre éléments apparaissent comme des nœuds texte), mais elle vous donne le document entier à parcourir, à modifier (createElementNS, appendChild, removeChild) et à sérialiser à nouveau avec un Transformer. Appelez toujours setNamespaceAware(true) : la valeur par défaut est false pour des raisons historiques, et sans cela les recherches qualifiées par namespace ne renvoient silencieusement rien.

SAX : les événements poussés

Un parser SAX lit le document une seule fois et appelle au fil de l'eau des méthodes d'un ContentHandler (startElement, characters, endElement). C'est à votre handler de conserver l'état dont il a besoin :

import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;

class TotalQuantityHandler extends DefaultHandler {
    private final StringBuilder text = new StringBuilder();
    private boolean inQty;
    long total;

    @Override public void startElement(String uri, String local, String qName, Attributes atts) {
        inQty = "qty".equals(local);
        text.setLength(0);
    }
    @Override public void characters(char[] ch, int start, int length) {
        if (inQty) text.append(ch, start, length);     // may be called several times per element
    }
    @Override public void endElement(String uri, String local, String qName) {
        if (inQty) { total += Long.parseLong(text.toString().trim()); inQty = false; }
    }
}

SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
var handler = new TotalQuantityHandler();
spf.newSAXParser().parse(new File("orders.xml"), handler);

Le détail qui fait tomber les handlers naïfs : characters peut livrer le texte d'un même élément en plusieurs morceaux, il faut donc l'accumuler et lire le résultat dans endElement. SAX est le modèle le plus rapide et le plus économe, et celui qui se trouve sous la plupart des autres parsers, mais l'inversion de contrôle rend pénible tout ce qui dépasse l'extraction simple.

StAX : les événements tirés

StAX (javax.xml.stream) inverse le flux : vous appelez next() et examinez l'événement. La même extraction se lit de haut en bas :

import javax.xml.stream.*;

XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);                       // XXE hardening
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);

long total = 0;
try (var in = new FileInputStream("orders.xml")) {
    XMLStreamReader r = xif.createXMLStreamReader(in);
    while (r.hasNext()) {
        if (r.next() == XMLStreamConstants.START_ELEMENT && "qty".equals(r.getLocalName())) {
            total += Long.parseLong(r.getElementText());   // reads the text and consumes the end tag
        }
    }
    r.close();
}

XMLStreamWriter est le pendant en écriture : writeStartElement, writeAttribute, writeCharacters, writeEndElement, avec l'échappement géré pour vous. Construire du XML par concaténation de chaînes, c'est ainsi qu'apparaissent les documents invalides et les failles d'injection ; utilisez le writer, le DOM ou JAXB.

Validation, XPath et transformation

La validation par XML Schema (XSD) vérifie la structure et les types d'un document avant que votre code ne lui fasse confiance. Chargez le schéma une fois avec une SchemaFactory, puis validez un document déjà parsé ou attachez le schéma à la factory du parser pour que la validation ait lieu pendant le parsing :

SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");        // no fetching of remote schemas
Schema schema = sf.newSchema(new File("orders.xsd"));
schema.newValidator().validate(new StreamSource(new File("orders.xml")));  // throws SAXException on violation

XPath (javax.xml.xpath) interroge un DOM : xpath.evaluate("sum(//o:line/o:qty)", doc) avec un NamespaceContext qui associe le préfixe o. C'est le moyen le plus rapide d'extraire quelques valeurs d'un document déjà en mémoire, et le plus lent de traiter un gros document, car chaque évaluation parcourt l'arbre. XSLT, via javax.xml.transform, convertit du XML en XML, en HTML ou en texte ; le même Transformer sans feuille de style sérialise un DOM vers un flux, et c'est ainsi que l'on réécrit un document modifié.

Jakarta XML Binding

Pour des documents structurés dont la forme est connue, manipuler des éléments est un effort inutile. JAXB fait correspondre une hiérarchie de classes au XML, de sorte que lire un document tient en un appel de méthode, et l'écrire en un autre.

Où se trouve la bibliothèque aujourd'hui

JAXB était fourni avec le JDK de Java 6 à Java 10, puis JEP 320 l'a retiré en Java 11, avec JAX-WS et les autres modules Java EE. Sur tout JDK actuel, c'est une dépendance ordinaire, et depuis Jakarta EE 9 le package s'appelle jakarta.xml.bind :

<dependency>
  <groupId>jakarta.xml.bind</groupId>
  <artifactId>jakarta.xml.bind-api</artifactId>
  <version>4.0.2</version>
</dependency>
<dependency>
  <groupId>org.glassfish.jaxb</groupId>
  <artifactId>jaxb-runtime</artifactId>     <!-- the reference runtime; scope runtime -->
  <version>4.0.5</version>
  <scope>runtime</scope>
</dependency>

XML Binding 4.0 est une partie optionnelle de la plateforme Jakarta EE 10 et ne figure pas dans le Web Profile : vérifiez si votre serveur l'embarque (c'est généralement le cas des serveurs full platform), sinon ajoutez la dépendance à l'application. Le code plus ancien qui importe javax.xml.bind doit voir ses imports réécrits et ses artefacts remplacés ; pour le reste, l'API est identique.

Annoter les classes

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import jakarta.xml.bind.annotation.*;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement(name = "order", namespace = "urn:shop:orders")
@XmlAccessorType(XmlAccessType.FIELD)          // map fields, ignore getters: predictable and no setter needed
@XmlType(propOrder = { "reference", "placed", "lines" })
public class OrderDocument {

    @XmlAttribute(required = true)
    String id;

    @XmlElement(required = true)
    String reference;

    @XmlElement
    @XmlJavaTypeAdapter(LocalDateAdapter.class)   // java.time is not built in; adapter converts to/from text
    LocalDate placed;

    @XmlElementWrapper(name = "lines")
    @XmlElement(name = "line")
    List<Line> lines = new ArrayList<>();

    @XmlTransient
    BigDecimal cachedTotal;                        // not part of the document

    public static class Line {
        @XmlAttribute String sku;
        @XmlElement int qty;
        @XmlElement BigDecimal unitPrice;
    }
}

Quelques règles gouvernent l'essentiel du mapping. Toute classe liée a besoin d'un constructeur sans argument. @XmlRootElement désigne les classes qui peuvent servir de racine de document ; il suffit que les types imbriqués soient atteignables. Sans @XmlAccessorType(FIELD), JAXB lie les propriétés JavaBean : chaque champ doit alors avoir un getter et un setter, et une classe qui possède à la fois un champ et une propriété du même nom le mappe deux fois. @XmlElementWrapper donne aux collections un élément englobant (<lines><line/><line/></lines>) ; sans lui, les éléments <line> répétés se placent directement sous le parent. Les sous-classes de XmlAdapter prennent en charge tout type que JAXB ne connaît pas, y compris java.time, qu'il ne gère toujours pas nativement, contrairement à JPA et JSON-B.

Marshalling et unmarshalling

import jakarta.xml.bind.*;
import javax.xml.stream.XMLInputFactory;
import javax.xml.transform.stream.StreamSource;

JAXBContext context = JAXBContext.newInstance(OrderDocument.class);   // expensive: create once, it is thread-safe

// Writing
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
marshaller.marshal(order, new File("order-42.xml"));

// Reading, through a hardened StAX reader so no DTD is ever processed
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
var reader = xif.createXMLStreamReader(new StreamSource(new File("incoming.xml")));

Unmarshaller unmarshaller = context.createUnmarshaller();
unmarshaller.setSchema(schema);                                         // validate while unmarshalling
OrderDocument incoming = (OrderDocument) unmarshaller.unmarshal(reader);

JAXBContext construit le modèle de mapping par réflexion et c'est l'objet coûteux ; Marshaller et Unmarshaller ne coûtent presque rien et ne sont pas thread-safe, créez-en donc un par opération. Positionnez un Schema sur l'unmarshaller dès qu'un schéma existe : sans lui, JAXB ignore silencieusement les éléments inconnus et accepte l'absence d'éléments obligatoires, ce qui est pratique pour un lecteur tolérant et dangereux pour tout ce qui compte vraiment. unmarshal avec gestion de JAXBElement (unmarshal(source, OrderDocument.class)) couvre les documents dont l'élément racine n'a pas de classe annotée @XmlRootElement.

Générer des classes à partir d'un schéma

Quand le schéma vient en premier (un standard sectoriel, un contrat avec un partenaire), générez les classes avec xjc, le compilateur de schémas livré avec la distribution du runtime JAXB et disponible via des plugins Maven. Il produit une classe par type complexe, une ObjectFactory avec des méthodes de fabrique, et un package-info.java portant le namespace. L'outil inverse, schemagen, déduit un XSD de classes annotées. Le code généré doit être traité comme un artefact de build, régénéré depuis le schéma et jamais modifié à la main ; les personnalisations vont dans un fichier de binding (.xjb) qui renomme des classes, mappe des types ou attache des adapters.

Durcir chaque parser

Par défaut, un parser XML fait ce que le document lui demande. Un document peut déclarer une entité externe pointant vers un fichier local ou une URL interne : lorsque le parser la développe, le contenu du fichier atterrit dans vos données ou la requête part depuis votre serveur. C'est la vulnérabilité XXE, toujours rangée sous security misconfiguration dans l'OWASP Top 10. Un document peut aussi définir des entités dont l'expansion est exponentielle (l'attaque dite « billion laughs ») et épuiser la mémoire. La parade est partout la même : refuser les DTD et, si ce n'est pas possible, refuser les entités externes ainsi que l'accès aux DTD et schémas externes.

ParserRéglage
DocumentBuilderFactory, SAXParserFactorysetFeature("http://apache.org/xml/features/disallow-doctype-decl", true) ; à défaut, les features external-general-entities et external-parameter-entities à false, setXIncludeAware(false), setExpandEntityReferences(false)
XMLInputFactory (StAX)SUPPORT_DTD à false, IS_SUPPORTING_EXTERNAL_ENTITIES à false
SchemaFactory, TransformerFactory, ValidatorACCESS_EXTERNAL_DTD, ACCESS_EXTERNAL_SCHEMA, ACCESS_EXTERNAL_STYLESHEET à ""
TousXMLConstants.FEATURE_SECURE_PROCESSING à true (limite l'expansion d'entités ; actif par défaut depuis Java 7, mais positionnez-le explicitement)
JAXBFaire l'unmarshalling depuis une source StAX ou SAX durcie plutôt que directement depuis un File ou un InputStream

Depuis Java 9, ces limites peuvent aussi être fixées pour toute la JVM dans $JAVA_HOME/conf/jaxp.properties, et le parser intégré au JDK applique FEATURE_SECURE_PROCESSING par défaut ; mais des parsers tiers présents dans le classpath (Xerces, Woodstox) remplacent celui du JDK via le service loader, et leurs valeurs par défaut diffèrent. Positionnez les features dans le code, sur chaque factory, et écrivez un test qui soumet un document avec un <!DOCTYPE> déclarant une entité externe et vérifie qu'il est rejeté. Les implémentations de Jakarta REST et les stacks SOAP parsent le XML à votre place : vérifiez qu'elles sont configurées avec les mêmes réglages au lieu de le supposer.

Erreurs courantes

  • Parser sans gestion des namespaces. getElementsByTagNameNS ne renvoie rien et XPath avec préfixes échoue. Appelez setNamespaceAware(true) sur chaque factory.
  • Lire characters une seule fois. Le texte arrive par morceaux ; accumulez-le jusqu'à endElement.
  • DOM pour des fichiers de plusieurs gigaoctets. OutOfMemoryError. Passez en streaming avec StAX ou SAX.
  • Construire du XML par concaténation de chaînes. Des < et & non échappés produisent des documents invalides et permettent à une saisie utilisateur d'injecter des éléments. Utilisez un writer, le DOM ou JAXB.
  • Un seul Marshaller partagé entre threads. Sortie corrompue. Partagez le JAXBContext et créez un marshaller par appel.
  • Unmarshalling sans schéma. Des documents invalides acceptés en silence. Positionnez le schéma et traitez les erreurs de validation.
  • javax.xml.bind sur un JDK récent. ClassNotFoundException à l'exécution. Ajoutez les artefacts Jakarta et réécrivez les imports.
  • Réglages par défaut du parser sur des entrées non fiables. XXE et expansion d'entités. Durcissez chaque factory et testez-la.

À retenir

  • JAXP, dans le module java.xml, propose DOM (arbre, accès aléatoire, gourmand en mémoire), SAX (événements poussés, mémoire minimale, flux de contrôle malcommode) et StAX (événements tirés, code linéaire, plus un writer).
  • Utilisez DOM pour les petits documents que vous interrogez ou modifiez, StAX pour tout ce qui est volumineux ou en flux, et SAX surtout pour maintenir du code existant.
  • Validez les documents non fiables contre un schéma, et servez-vous de XPath pour des extractions ponctuelles dans des documents déjà en mémoire.
  • Jakarta XML Binding 4 fait correspondre classes et XML avec @XmlRootElement, @XmlElement, @XmlAttribute et des adapters ; il vit dans jakarta.xml.bind comme dépendance séparée depuis que JEP 320 l'a retiré du JDK.
  • Partagez un seul JAXBContext, créez un Marshaller ou un Unmarshaller par opération, positionnez un schéma lors de l'unmarshalling, et générez les classes avec xjc quand le schéma vient en premier.
  • Durcissez chaque factory de parser contre XXE (interdire les DTD, bloquer les entités externes et l'accès aux DTD et schémas externes) et faites l'unmarshalling depuis une source durcie.

Sources et lectures complémentaires

Questions fréquentes

Pourquoi mon code JAXB échoue-t-il avec une ClassNotFoundException sur Java 11 ou plus ?
JAXB faisait partie du JDK de Java 6 à Java 10 sous le nom javax.xml.bind, et JEP 320 l'a retiré en Java 11. Ajoutez-le comme dépendance : pour la branche Jakarta actuelle, jakarta.xml.bind:jakarta.xml.bind-api (4.0.x) plus le runtime org.glassfish.jaxb:jaxb-runtime, et remplacez les imports par jakarta.xml.bind. Les anciens artefacts javax.xml.bind 2.3 fonctionnent encore sur les JDK récents, mais ne sont plus maintenus.
Quand utiliser SAX plutôt que DOM ?
Quand le document est volumineux au regard de la mémoire disponible, quand vous n'avez besoin que de quelques valeurs, ou quand vous traitez un flux qui ne se termine jamais. DOM construit tout l'arbre en mémoire (souvent plusieurs fois la taille du fichier) et offre un accès aléatoire ; SAX et StAX lisent une seule fois, en avant, avec une mémoire constante. Pour la plupart des documents de moins de quelques mégaoctets, le confort de DOM l'emporte ; pour les flux, les exports et les uploads de taille inconnue, passez en streaming.
XML a-t-il encore un intérêt quand tout passe en JSON ?
En intégration d'entreprise, oui. Les services SOAP, les standards bancaires et assurantiels, les schémas sectoriels, les documents bureautiques, la configuration de Maven et des serveurs d'applications, les assertions SAML et de nombreuses interfaces partenaires sont en XML. Savoir le parser en toute sécurité compte davantage, pas moins, maintenant qu'il est souvent manipulé par des développeurs qui n'en voient presque jamais.
Quelle différence entre StAX et SAX ?
Les deux fonctionnent en streaming. SAX pousse des événements vers des callbacks que vous implémentez : c'est le parser qui pilote, et votre code doit conserver son état d'un callback à l'autre. StAX laisse votre code tirer l'événement suivant quand il le souhaite, ce qui garde une logique de parsing linéaire et permet facilement de s'arrêter tôt ou de passer le reader à une autre méthode. StAX dispose aussi d'une API d'écriture ; SAX ne fait que lire.