Java EE / Jakarta EE

XML em Java: SAX, DOM, StAX e Jakarta XML Binding (JAXB 4)

Intermediário8 min de leituraAtualizado

Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês

XML ainda é o formato de muitas integrações corporativas, arquivos de configuração e padrões de documentos, e o Java oferece três modelos de parsing e um framework de binding para lidar com ele. Este guia compara DOM, SAX e StAX com exemplos que funcionam, explica validação por schema e XPath, mostra como o Jakarta XML Binding (JAXB) mapeia classes para XML nos dois sentidos desde que saiu do JDK e detalha o hardening de parsers de que toda aplicação que consome XML precisa.

O que você vai aprender

  • Escolher entre DOM, SAX e StAX de acordo com o tamanho do documento e o padrão de acesso
  • Fazer parsing, navegar e escrever XML com as APIs JAXP do módulo java.xml
  • Validar documentos contra um XML Schema e consultá-los com XPath
  • Fazer marshal e unmarshal de classes Java com as anotações do Jakarta XML Binding 4 e gerar classes a partir de um schema
  • Configurar cada parser para evitar ataques de XML External Entity (XXE) e de expansão de entidades

O JSON conquistou a web, mas o XML nunca saiu das empresas. Ele chega como envelopes SOAP, arquivos de extrato bancário, mensagens de padrões setoriais, asserções SAML, documentos de escritório e a própria configuração do servidor de aplicação, e costuma vir de fora da sua fronteira de confiança. A biblioteca padrão do Java faz parsing de XML desde 2002 por meio do JAXP (Java API for XML Processing, hoje o módulo java.xml), que oferece três modelos: uma árvore em memória, um fluxo de eventos empurrados para você ou um fluxo de eventos que você puxa. Por cima deles, o Jakarta XML Binding (JAXB) mapeia XML diretamente para classes Java, de modo que a maior parte do código da aplicação nunca vê um elemento sequer. Este guia cobre as quatro abordagens e, como parsers XML são uma superfície de ataque bem conhecida, as configurações que os tornam seguros.

Três modelos de parsing

DOMSAXStAX
ModeloDocumento inteiro como árvore em memóriaPush: o parser chama o seu handler a cada eventoPull: você pede o próximo evento ao reader
MemóriaProporcional ao tamanho do documento (várias vezes o número de bytes)ConstanteConstante
AcessoAleatório; navegar, modificar, gravar de voltaSó para a frente, somente leituraSó para a frente; API de escrita separada
FacilidadeO mais fácil para documentos pequenosO mais difícil: máquina de estados em callbacksCódigo linear, fácil de interromper ou delegar
Use paraConfiguração, mensagens pequenas, documentos que você edita ou consulta com XPathArquivos muito grandes, extração de poucos valores, código legadoArquivos grandes, transformações em streaming, escrita de XML à mão

Os três vêm de factories em javax.xml.parsers e javax.xml.stream. Os pacotes javax.xml são Java SE, não Jakarta EE, e não migraram para jakarta.*; só a API de binding migrou.

DOM: a árvore

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

A API DOM do W3C é verbosa (NodeList não é iterável, o texto é um nó filho e não uma propriedade, e os espaços entre elementos aparecem como nós de texto), mas entrega o documento inteiro para você percorrer, modificar (createElementNS, appendChild, removeChild) e serializar de volta com um Transformer. Sempre chame setNamespaceAware(true): o padrão é false por razões históricas e, sem isso, buscas qualificadas por namespace simplesmente não retornam nada, sem nenhum aviso.

SAX: eventos empurrados

Um parser SAX lê o documento uma única vez e, à medida que avança, chama métodos de um ContentHandler (startElement, characters, endElement). O seu handler guarda o estado de que precisar:

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

O detalhe que quebra handlers ingênuos: characters pode entregar o texto de um mesmo elemento em vários pedaços, então você precisa acumulá-lo e ler o resultado em endElement. O SAX é o modelo mais rápido e enxuto, e o que está por baixo da maioria dos outros parsers, mas a inversão de controle torna trabalhoso tudo o que vai além de uma extração simples.

StAX: eventos puxados

O StAX (javax.xml.stream) inverte o fluxo: você chama next() e inspeciona o evento. A mesma extração se lê de cima para baixo:

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

O XMLStreamWriter é o par de escrita: writeStartElement, writeAttribute, writeCharacters, writeEndElement, com o escaping resolvido para você. Montar XML concatenando strings é o caminho para documentos inválidos e bugs de injeção; use o writer, o DOM ou o JAXB.

Validação, XPath e transformação

A validação com XML Schema (XSD) confere a estrutura e os tipos de um documento antes que o seu código confie nele. Carregue o schema uma vez com uma SchemaFactory e, então, valide um documento já parseado ou associe o schema à factory do parser para que a validação aconteça durante o 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

O XPath (javax.xml.xpath) consulta um DOM: xpath.evaluate("sum(//o:line/o:qty)", doc) com um NamespaceContext que mapeia o prefixo o. É o jeito mais rápido de extrair alguns valores de um documento que já está em memória, e o mais lento de processar um documento grande, porque cada avaliação percorre a árvore. O XSLT, via javax.xml.transform, converte XML em XML, HTML ou texto; o mesmo Transformer sem stylesheet serializa um DOM para um stream, e é assim que se grava de volta um documento modificado.

Jakarta XML Binding

Para documentos estruturados com formato conhecido, lidar com elementos é esforço desperdiçado. O JAXB mapeia uma hierarquia de classes para XML, de modo que ler um documento é uma chamada de método e escrevê-lo é outra.

Onde a biblioteca está hoje

O JAXB vinha embutido no JDK do Java 6 ao Java 10 e foi removido no Java 11 pela JEP 320, junto com o JAX-WS e os demais módulos Java EE. Em qualquer JDK atual ele é uma dependência comum e, desde o Jakarta EE 9, o pacote é 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>

O XML Binding 4.0 é uma parte opcional da plataforma Jakarta EE 10 e não faz parte do Web Profile, então verifique se o seu servidor o inclui (os servidores de plataforma completa geralmente incluem) e, caso contrário, adicione a dependência à aplicação. Código mais antigo que importa javax.xml.bind precisa ter os imports reescritos e os artefatos trocados; fora isso, a API é idêntica.

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

Algumas poucas regras determinam a maior parte do mapeamento. Toda classe vinculada precisa de um construtor sem argumentos. @XmlRootElement marca as classes que podem ser raiz de um documento; os tipos aninhados só precisam ser alcançáveis. Sem @XmlAccessorType(FIELD), o JAXB vincula propriedades JavaBean, o que significa que cada campo precisa de um getter e de um setter, e que uma classe com um campo e uma propriedade de mesmo nome acaba mapeando-o duas vezes. @XmlElementWrapper dá às coleções um elemento envolvente (<lines><line/><line/></lines>); sem ele, os <line> repetidos ficam diretamente sob o elemento pai. Subclasses de XmlAdapter cuidam de qualquer tipo que o JAXB não conheça, inclusive java.time, que ele, ao contrário de JPA e JSON-B, ainda não suporta nativamente.

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

O JAXBContext monta o modelo de mapeamento por reflexão e é o objeto caro; Marshaller e Unmarshaller são baratos e não são thread-safe, então crie um por operação. Defina um Schema no unmarshaller sempre que existir um schema: sem ele, o JAXB ignora silenciosamente elementos desconhecidos e aceita a ausência de elementos obrigatórios, o que é conveniente para leitores tolerantes e perigoso para qualquer coisa que importe. unmarshal com tratamento de JAXBElement (unmarshal(source, OrderDocument.class)) cobre documentos cujo elemento raiz não tem uma classe com @XmlRootElement.

Gerando classes a partir de um schema

Quando o schema vem primeiro (um padrão setorial, um contrato com um parceiro), gere as classes com o xjc, o compilador de schemas que acompanha a distribuição do runtime JAXB e está disponível via plugins Maven. Ele produz uma classe por tipo complexo, uma ObjectFactory com métodos de fábrica e um package-info.java com o namespace. O caminho inverso, o schemagen, deriva um XSD de classes anotadas. Código gerado deve ser tratado como saída do build, regenerado a partir do schema e nunca editado à mão; as customizações vão em um arquivo de binding (.xjb) que renomeia classes, mapeia tipos ou associa adapters.

Protegendo todos os parsers

Por padrão, um parser XML faz o que o documento manda. Um documento pode declarar uma entidade externa que aponta para um arquivo local ou uma URL interna e, quando o parser a expande, o conteúdo do arquivo vai parar nos seus dados ou a requisição sai do seu servidor: é a vulnerabilidade XXE, ainda listada em security misconfiguration no OWASP Top 10. Um documento também pode definir entidades que se expandem exponencialmente (o ataque "billion laughs") e esgotar a memória. A defesa é a mesma em todo lugar: recusar DTDs e, se isso não for possível, recusar entidades externas e o acesso a DTDs e schemas externos.

ParserConfiguração
DocumentBuilderFactory, SAXParserFactorysetFeature("http://apache.org/xml/features/disallow-doctype-decl", true); caso contrário, as features external-general-entities e external-parameter-entities como false, setXIncludeAware(false), setExpandEntityReferences(false)
XMLInputFactory (StAX)SUPPORT_DTD como false, IS_SUPPORTING_EXTERNAL_ENTITIES como false
SchemaFactory, TransformerFactory, ValidatorACCESS_EXTERNAL_DTD, ACCESS_EXTERNAL_SCHEMA, ACCESS_EXTERNAL_STYLESHEET como ""
TodosXMLConstants.FEATURE_SECURE_PROCESSING como true (limita a expansão de entidades; ativo por padrão desde o Java 7, mas defina explicitamente)
JAXBFazer unmarshal a partir de uma fonte StAX ou SAX protegida, e não diretamente de um File ou InputStream

Desde o Java 9, esses limites também podem ser definidos para toda a JVM em $JAVA_HOME/conf/jaxp.properties, e o parser embutido no JDK aplica FEATURE_SECURE_PROCESSING por padrão; mas parsers de terceiros no classpath (Xerces, Woodstox) substituem o do JDK via service loader, e os padrões deles são outros. Defina as features no código, em cada factory, e escreva um teste que envie um documento com um <!DOCTYPE> declarando uma entidade externa e verifique que ele é rejeitado. Implementações de Jakarta REST e stacks SOAP fazem o parsing de XML por você; confira se a configuração delas tem os mesmos ajustes em vez de presumir.

Erros comuns

  • Parsing sem suporte a namespaces. getElementsByTagNameNS não retorna nada e XPath com prefixos falha. Chame setNamespaceAware(true) em todas as factories.
  • Ler characters uma única vez. O texto chega em pedaços; acumule até endElement.
  • DOM para arquivos de gigabytes. OutOfMemoryError. Use streaming com StAX ou SAX.
  • Montar XML concatenando strings. < e & sem escape geram documentos inválidos e permitem que a entrada do usuário injete elementos. Use um writer, DOM ou JAXB.
  • Um único Marshaller compartilhado entre threads. Saída corrompida. Compartilhe o JAXBContext e crie marshallers a cada chamada.
  • Unmarshal sem schema. Documentos quebrados aceitos silenciosamente. Defina o schema e trate os erros de validação.
  • javax.xml.bind em um JDK moderno. ClassNotFoundException em tempo de execução. Adicione os artefatos Jakarta e reescreva os imports.
  • Configurações padrão do parser com entrada não confiável. XXE e expansão de entidades. Proteja todas as factories e teste isso.

Principais pontos

  • O JAXP, no módulo java.xml, oferece DOM (árvore, acesso aleatório, alto consumo de memória), SAX (eventos empurrados, memória mínima, fluxo de controle desajeitado) e StAX (eventos puxados, código linear e ainda um writer).
  • Use DOM para documentos pequenos que você consulta ou edita, StAX para tudo o que for grande ou vier em streaming, e SAX principalmente ao manter código existente.
  • Valide documentos não confiáveis contra um schema e use XPath para extrações pontuais de documentos que já estão em memória.
  • O Jakarta XML Binding 4 mapeia classes para XML com @XmlRootElement, @XmlElement, @XmlAttribute e adapters; ele vive em jakarta.xml.bind como dependência separada desde que a JEP 320 o removeu do JDK.
  • Compartilhe um único JAXBContext, crie um Marshaller ou Unmarshaller por operação, defina um schema ao fazer unmarshal e gere as classes com xjc quando o schema vier primeiro.
  • Proteja cada factory de parser contra XXE (proíba DTDs, bloqueie entidades externas e o acesso a DTDs e schemas externos) e faça unmarshal a partir de uma fonte protegida.

Fontes e leituras complementares

Perguntas frequentes

Por que meu código JAXB falha com ClassNotFoundException no Java 11 ou posterior?
O JAXB fez parte do JDK do Java 6 ao Java 10 como javax.xml.bind e foi removido no Java 11 pela JEP 320. Adicione-o como dependência: para a linha Jakarta atual, jakarta.xml.bind:jakarta.xml.bind-api (4.0.x) mais o runtime org.glassfish.jaxb:jaxb-runtime, e troque os imports para jakarta.xml.bind. Os antigos artefatos javax.xml.bind 2.3 ainda funcionam em JDKs modernos, mas não recebem mais manutenção.
Quando usar SAX em vez de DOM?
Quando o documento é grande em relação à memória disponível, quando você só precisa extrair alguns valores ou quando processa um fluxo que nunca termina. O DOM monta a árvore inteira em memória (normalmente várias vezes o tamanho do arquivo) e oferece acesso aleatório; SAX e StAX leem para a frente uma única vez, com memória constante. Para a maioria dos documentos de até alguns megabytes, a praticidade do DOM compensa; para feeds, exportações e uploads de tamanho desconhecido, use streaming.
XML ainda é relevante quando tudo é JSON?
Em integração corporativa, sim. Serviços SOAP, padrões bancários e de seguros, schemas setoriais, documentos de escritório, a configuração do Maven e dos servidores de aplicação, asserções SAML e muitas interfaces com parceiros são XML. Saber fazer parsing dele com segurança importa mais, e não menos, agora que ele costuma ficar nas mãos de desenvolvedores que raramente o veem.
Qual é a diferença entre StAX e SAX?
Os dois trabalham em streaming. O SAX empurra eventos para callbacks que você implementa, então é o parser que conduz o fluxo e o seu código precisa guardar estado entre os callbacks. O StAX deixa o seu código puxar o próximo evento quando quiser, o que mantém a lógica de parsing linear e facilita parar mais cedo ou passar o reader para outro método. O StAX também tem uma API de escrita; o SAX só lê.