Java EE / Jakarta EE

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

Intermedio9 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

XML sigue siendo el formato de muchas integraciones empresariales, archivos de configuración y estándares documentales, y Java ofrece tres modelos de parsing y un framework de binding para tratarlo. Esta guía compara DOM, SAX y StAX con ejemplos que funcionan, explica la validación con esquemas y XPath, muestra cómo Jakarta XML Binding (JAXB) mapea clases a XML en ambos sentidos desde que salió del JDK y detalla el blindaje de parsers que necesita cualquier aplicación que consuma XML.

Lo que aprenderás

  • Elegir entre DOM, SAX y StAX según el tamaño del documento y el patrón de acceso
  • Parsear, recorrer y escribir XML con las API de JAXP del módulo java.xml
  • Validar documentos contra un XML Schema y consultarlos con XPath
  • Hacer marshal y unmarshal de clases Java con las anotaciones de Jakarta XML Binding 4 y generar clases a partir de un esquema
  • Configurar cada parser para evitar ataques XML External Entity (XXE) y de expansión de entidades

JSON ganó la web, pero XML nunca se fue de la empresa. Llega en forma de sobres SOAP, archivos de extractos bancarios, mensajes de estándares sectoriales, aserciones SAML, documentos ofimáticos y la propia configuración del servidor de aplicaciones, y casi siempre viene de fuera de tu frontera de confianza. La biblioteca estándar de Java parsea XML desde 2002 mediante JAXP (Java API for XML Processing, hoy el módulo java.xml), que ofrece tres modelos: un árbol en memoria, un flujo de eventos que te empujan o un flujo de eventos del que tiras tú. Por encima, Jakarta XML Binding (JAXB) mapea el XML directamente a clases Java, de modo que la mayor parte del código de la aplicación nunca ve un elemento. Esta guía cubre los cuatro enfoques y, como los parsers XML son una superficie de ataque bien conocida, la configuración que los hace seguros.

Tres modelos de parsing

DOMSAXStAX
ModeloEl documento completo como árbol en memoriaPush: el parser llama a tu handler en cada eventoPull: le pides al reader el siguiente evento
MemoriaProporcional al tamaño del documento (varias veces los bytes)ConstanteConstante
AccesoAleatorio; recorrer, modificar, volver a escribirSolo hacia delante, solo lecturaSolo hacia delante; API de escritura aparte
FacilidadLa más sencilla para documentos pequeñosLa más difícil: máquina de estados en callbacksCódigo lineal, fácil de detener o delegar
Úsalo paraConfiguración, mensajes pequeños, documentos que editas o consultas con XPathArchivos muy grandes, extraer unos pocos valores, código legacyArchivos grandes, transformaciones en streaming, escribir XML a mano

Los tres se obtienen de factories de javax.xml.parsers y javax.xml.stream. Los paquetes javax.xml son Java SE, no Jakarta EE, y no se movieron a jakarta.*; solo lo hizo la API de binding.

DOM: el árbol

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());
}

La API DOM del W3C es verbosa (NodeList no es iterable, el texto es un nodo hijo y no una propiedad, y los espacios entre elementos aparecen como nodos de texto), pero te da el documento entero para recorrerlo, modificarlo (createElementNS, appendChild, removeChild) y volver a serializarlo con un Transformer. Llama siempre a setNamespaceAware(true): el valor por defecto es false por motivos históricos y, sin él, las búsquedas cualificadas por namespace no devuelven nada sin avisar.

SAX: eventos empujados

Un parser SAX lee el documento una vez y, sobre la marcha, llama a métodos de un ContentHandler (startElement, characters, endElement). Tu handler guarda el estado que necesite:

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);

El detalle que rompe los handlers ingenuos: characters puede entregar el texto de un mismo elemento en varios trozos, así que hay que acumularlo y leer el resultado en endElement. SAX es el modelo más rápido y ligero, y el que está debajo de la mayoría de los demás parsers, pero la inversión de control vuelve incómodo todo lo que vaya más allá de una extracción sencilla.

StAX: eventos que pides tú

StAX (javax.xml.stream) le da la vuelta al flujo: llamas a next() e inspeccionas el evento. La misma extracción se lee de arriba abajo:

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 es su equivalente de escritura: writeStartElement, writeAttribute, writeCharacters, writeEndElement, y del escapado se encarga él. Construir XML concatenando strings es la forma en que nacen los documentos inválidos y los bugs de inyección; usa el writer, el DOM o JAXB.

Validación, XPath y transformación

La validación con XML Schema (XSD) comprueba la estructura y los tipos de un documento antes de que tu código confíe en él. Carga el esquema una sola vez con una SchemaFactory y, o bien valida un documento ya parseado, o bien asocia el esquema a la factory del parser para que la validación ocurra durante el 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) consulta un DOM: xpath.evaluate("sum(//o:line/o:qty)", doc) con un NamespaceContext que mapea el prefijo o. Es la forma más rápida de sacar unos cuantos valores de un documento que ya tienes en memoria, y la más lenta de procesar uno grande, porque cada evaluación recorre el árbol. XSLT, a través de javax.xml.transform, convierte XML en XML, HTML o texto; el mismo Transformer sin hoja de estilos serializa un DOM a un stream, que es como se vuelve a escribir un documento modificado.

Jakarta XML Binding

Con documentos estructurados de forma conocida, trabajar con elementos es un esfuerzo desperdiciado. JAXB mapea una jerarquía de clases a XML, de modo que leer un documento es una llamada a un método y escribirlo es otra.

Dónde vive hoy la biblioteca

JAXB venía incluido en el JDK desde Java 6 hasta Java 10 y JEP 320 lo eliminó en Java 11, junto con JAX-WS y el resto de módulos Java EE. En cualquier JDK actual es una dependencia normal y, desde Jakarta EE 9, el paquete es 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 es una parte opcional de la plataforma Jakarta EE 10 y no está en el Web Profile, así que comprueba si tu servidor lo incluye (los servidores de plataforma completa suelen hacerlo) y, si no, añade la dependencia a la aplicación. El código antiguo que importa javax.xml.bind necesita reescribir sus imports y cambiar de artefactos; por lo demás, la API es idéntica.

Anotar las clases

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;
    }
}

Unas pocas reglas explican la mayor parte del mapeo. Toda clase enlazada necesita un constructor sin argumentos. @XmlRootElement marca las clases que pueden ser raíz de un documento; a los tipos anidados les basta con ser alcanzables. Sin @XmlAccessorType(FIELD), JAXB enlaza propiedades JavaBean, lo que significa que cada campo necesita un getter y un setter, y que una clase con un campo y una propiedad del mismo nombre lo mapea dos veces. @XmlElementWrapper da a las colecciones un elemento contenedor (<lines><line/><line/></lines>); sin él, los <line> repetidos cuelgan directamente del padre. Las subclases de XmlAdapter se encargan de cualquier tipo que JAXB no conozca, incluido java.time, que, a diferencia de JPA y JSON-B, sigue sin soportar de forma nativa.

Marshalling y 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 construye el modelo de mapeo por reflexión y es el objeto caro; Marshaller y Unmarshaller son baratos y no son thread-safe, así que crea uno por operación. Asigna un Schema al unmarshaller siempre que exista un esquema: sin él, JAXB ignora en silencio los elementos desconocidos y acepta que falten los obligatorios, algo cómodo para lectores tolerantes y peligroso para todo lo que importa. unmarshal con manejo de JAXBElement (unmarshal(source, OrderDocument.class)) cubre los documentos cuyo elemento raíz no tiene una clase con @XmlRootElement.

Generar clases a partir de un esquema

Cuando el esquema va primero (un estándar sectorial, un contrato con un partner), genera las clases con xjc, el compilador de esquemas que viene con la distribución del runtime de JAXB y que está disponible mediante plugins de Maven. Produce una clase por cada tipo complejo, una ObjectFactory con métodos de fábrica y un package-info.java con el namespace. La herramienta inversa, schemagen, deriva un XSD a partir de clases anotadas. El código generado debe tratarse como una salida del build: se regenera desde el esquema y nunca se edita a mano; las personalizaciones van en un archivo de binding (.xjb) que renombra clases, mapea tipos o asocia adapters.

Blindar todos los parsers

Por defecto, un parser XML hace lo que el documento le dice. Un documento puede declarar una entidad externa que apunte a un archivo local o a una URL interna y, cuando el parser la expande, el contenido del archivo acaba en tus datos o la petición sale desde tu servidor: es la vulnerabilidad XXE, que el OWASP Top 10 sigue incluyendo en security misconfiguration. Un documento también puede definir entidades que se expanden exponencialmente (el ataque «billion laughs») y agotar la memoria. La defensa es la misma en todas partes: rechazar los DTD y, si no puedes, rechazar las entidades externas y el acceso a DTD y esquemas externos.

ParserConfiguración
DocumentBuilderFactory, SAXParserFactorysetFeature("http://apache.org/xml/features/disallow-doctype-decl", true); si no, las features external-general-entities y external-parameter-entities a false, setXIncludeAware(false), setExpandEntityReferences(false)
XMLInputFactory (StAX)SUPPORT_DTD a false, IS_SUPPORTING_EXTERNAL_ENTITIES a false
SchemaFactory, TransformerFactory, ValidatorACCESS_EXTERNAL_DTD, ACCESS_EXTERNAL_SCHEMA, ACCESS_EXTERNAL_STYLESHEET a ""
TodosXMLConstants.FEATURE_SECURE_PROCESSING a true (limita la expansión de entidades; activo por defecto desde Java 7, pero configúralo explícitamente)
JAXBHacer unmarshal a través de una fuente StAX o SAX blindada en lugar de directamente desde un File o un InputStream

Desde Java 9 estos límites también pueden fijarse para toda la JVM en $JAVA_HOME/conf/jaxp.properties, y el parser integrado en el JDK aplica FEATURE_SECURE_PROCESSING por defecto, pero los parsers de terceros presentes en el classpath (Xerces, Woodstox) sustituyen al del JDK a través del service loader y sus valores por defecto son otros. Configura las features en el código, en cada factory, y escribe un test que le pase un documento con un <!DOCTYPE> que declare una entidad externa y compruebe que se rechaza. Las implementaciones de Jakarta REST y los stacks SOAP parsean XML por ti; revisa que su configuración tenga los mismos ajustes en lugar de darlo por hecho.

Errores habituales

  • Parsear sin soporte de namespaces. getElementsByTagNameNS no devuelve nada y XPath con prefijos falla. Llama a setNamespaceAware(true) en todas las factories.
  • Leer characters una sola vez. El texto llega en trozos; acumúlalo hasta endElement.
  • DOM para archivos de gigabytes. OutOfMemoryError. Usa streaming con StAX o SAX.
  • Construir XML concatenando strings. Los < y & sin escapar generan documentos inválidos y permiten que la entrada del usuario inyecte elementos. Usa un writer, DOM o JAXB.
  • Un único Marshaller compartido entre threads. Salida corrupta. Comparte el JAXBContext y crea un marshaller por llamada.
  • Hacer unmarshal sin esquema. Documentos rotos aceptados en silencio. Asigna el esquema y gestiona los errores de validación.
  • javax.xml.bind en un JDK moderno. ClassNotFoundException en tiempo de ejecución. Añade los artefactos Jakarta y reescribe los imports.
  • Configuración por defecto del parser con entradas no confiables. XXE y expansión de entidades. Blinda todas las factories y pruébalo.

Puntos clave

  • JAXP, en el módulo java.xml, ofrece DOM (árbol, acceso aleatorio, mucho consumo de memoria), SAX (eventos empujados, memoria mínima, flujo de control incómodo) y StAX (eventos que pides tú, código lineal y además un writer).
  • Usa DOM para documentos pequeños que consultas o editas, StAX para todo lo que sea grande o llegue en streaming, y SAX sobre todo al mantener código existente.
  • Valida los documentos no confiables contra un esquema y usa XPath para extracciones puntuales de documentos que ya están en memoria.
  • Jakarta XML Binding 4 mapea clases a XML con @XmlRootElement, @XmlElement, @XmlAttribute y adapters; vive en jakarta.xml.bind como dependencia independiente desde que JEP 320 lo sacó del JDK.
  • Comparte un único JAXBContext, crea un Marshaller o Unmarshaller por operación, asigna un esquema al hacer unmarshal y genera las clases con xjc cuando el esquema va primero.
  • Blinda cada factory de parser contra XXE (prohíbe los DTD y bloquea las entidades externas y el acceso a DTD y esquemas externos) y haz unmarshal a través de una fuente blindada.

Fuentes y lecturas recomendadas

Preguntas frecuentes

¿Por qué mi código JAXB falla con ClassNotFoundException en Java 11 o posterior?
JAXB formó parte del JDK desde Java 6 hasta Java 10 como javax.xml.bind, y JEP 320 lo eliminó en Java 11. Añádelo como dependencia: para la línea Jakarta actual, jakarta.xml.bind:jakarta.xml.bind-api (4.0.x) más el runtime org.glassfish.jaxb:jaxb-runtime, y cambia los imports a jakarta.xml.bind. Los antiguos artefactos javax.xml.bind 2.3 siguen funcionando en JDK modernos, pero ya no se mantienen.
¿Cuándo conviene usar SAX en lugar de DOM?
Cuando el documento es grande en relación con la memoria disponible, cuando solo necesitas extraer algunos valores o cuando procesas un flujo que nunca termina. DOM construye el árbol completo en memoria (normalmente varias veces el tamaño del archivo) y ofrece acceso aleatorio; SAX y StAX leen hacia delante una sola vez con memoria constante. Para la mayoría de los documentos de menos de unos pocos megabytes gana la comodidad de DOM; para feeds, exportaciones y subidas de tamaño desconocido, usa streaming.
¿Sigue siendo relevante XML cuando todo es JSON?
En integración empresarial, sí. Los servicios SOAP, los estándares de banca y seguros, los esquemas sectoriales, los documentos ofimáticos, la configuración de Maven y de los servidores de aplicaciones, las aserciones SAML y muchas interfaces con partners son XML. Saber parsearlo de forma segura importa más, no menos, ahora que suelen manejarlo desarrolladores que casi nunca lo ven.
¿Qué diferencia hay entre StAX y SAX?
Los dos trabajan en streaming. SAX empuja eventos hacia callbacks que tú implementas, así que el parser dirige el flujo y tu código tiene que guardar el estado entre callbacks. StAX deja que tu código tire del siguiente evento cuando quiera, lo que mantiene lineal la lógica de parsing y facilita parar antes de tiempo o pasar el reader a otro método. StAX tiene además una API de escritura; SAX solo lee.