Java XML DOM Parser
XML in Java parsen, navigieren, ändern und serialisieren mit dem eingebauten DOM-Parser – inkl. vollständigem Lese-Änderungs-Schreib-Beispiel.
Der DOM (Document Object Model) Parser liest ein gesamtes XML-Dokument in den Arbeitsspeicher und liefert einen Knotenbaum, den man navigieren, abfragen und ändern kann. Er ist im JDK unter javax.xml.parsers und org.w3c.dom enthalten, sodass nichts zum Classpath hinzugefügt werden muss. DOM ist das richtige Werkzeug, wenn Dokumente klein genug sind, um im Arbeitsspeicher gehalten zu werden, und wenn wahlfreier Zugriff auf beliebige Teile des Baums erforderlich ist – etwa beim Lesen einer Konfigurationsdatei, beim Transformieren einer Nutzlast oder beim programmgesteuerten Aufbau von XML.
Dieses Kapitel führt durch den vollständigen Lebenszyklus: wie DOM ein Dokument modelliert, wie eine Quelle in einen Baum geparst wird, wie Knoten gelesen und geändert werden und wie der Baum wieder als XML ausgegeben wird. Wer neu bei XML in Java ist, beginnt am besten mit der XML-Einführung; für sehr große Dokumente, bei denen der Speicherbedarf eine Rolle spielt, empfiehlt sich ein Vergleich von DOM mit dem streamenden SAX-Parser.
So modelliert DOM ein Dokument
DOM wandelt Markup in einen Baum aus Node-Objekten um. Jedes Element, jedes Attribut, jeder Textteil und jeder Kommentar ist ein Knoten, und das gesamte Dokument hängt an einem einzigen Document-Wurzelknoten. Man liest den Baum, indem man Knoten nach ihren Kindern fragt, und ändert ihn, indem man Knoten erstellt, verschiebt oder entfernt.
| Konzept | Interface | Was es darstellt |
|---|---|---|
| Document | Document | Die gesamte geparste Datei; Einstiegspunkt in den Baum |
| Element | Element | Ein Tag wie <book>, mit Attributen und Kindern |
| Attribut | Attr | Ein Name/Wert-Paar an einem Element |
| Text | Text | Zeichendaten innerhalb eines Elements |
| Knotenliste | NodeList | Eine geordnete, per Index adressierbare Sammlung von Knoten |
Der entscheidende Kompromiss: DOM ist praktisch, weil der gesamte Baum adressierbar ist, lädt aber alles auf einmal in den Arbeitsspeicher. Für mehrere Gigabyte große Feeds würde man stattdessen SAX oder StAX verwenden, die das Dokument streamen ohne einen Baum aufzubauen. Und wenn XML auf Java-Objekte abgebildet werden soll anstatt rohe Knoten zu durchlaufen, ist JAXB in der Regel mit weniger Code verbunden als handgeschriebenes DOM.
Ein Dokument parsen
Ein Parser wird nie direkt erzeugt. Man fordert von einer DocumentBuilderFactory einen DocumentBuilder an und ruft dann parse auf einem Stream, einer Datei oder einer URI auf. Die Factory sollte vor dem Erstellen konfiguriert werden – Namespace-Bewusstsein und Validierung sind Schalter auf Factory-Ebene.
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("library.xml"));
// Collapse adjacent text nodes and drop empty ones so getTextContent is clean.
doc.getDocumentElement().normalize();parse wirft SAXException bei fehlerhaftem XML und IOException, wenn die Quelle nicht gelesen werden kann – beides sind geprüfte Ausnahmen, die behandelt werden müssen. Das einmalige Aufrufen von normalize() nach dem Parsen führt aufgeteilte Textknoten zusammen – eine häufige Überraschungsquelle beim Lesen von Elementtexten.
Den Baum navigieren
Zwei Methoden decken die meisten Lesevorgänge ab: getElementsByTagName findet alle Nachkommen mit einem bestimmten Tag, und getChildNodes gibt die direkten Kinder eines Knotens zurück. Man beachte, dass getChildNodes auch Leerzeichen-Textknoten enthält, sodass man nach Knotentyp filtern sollte, wenn nur Elemente gewünscht sind.
Element root = doc.getDocumentElement(); // <library>
NodeList books = doc.getElementsByTagName("book"); // every <book> in the tree
for (int i = 0; i < books.getLength(); i++) {
Element book = (Element) books.item(i);
String id = book.getAttribute("id"); // attribute by name
String title = book.getElementsByTagName("title")
.item(0).getTextContent(); // first child <title> text
System.out.println(id + " -> " + title);
}NodeList ist indexbasiert und nicht iterierbar, daher wird mit getLength() und item(i) in einer Schleife gearbeitet. getAttribute gibt einen leeren String zurück (nie null), wenn das Attribut fehlt – das ist wichtig zu wissen, bevor man eine Null-Prüfung schreibt, die niemals auslöst.
Knoten ändern und erstellen
Der DOM-Baum ist veränderlich. Text wird mit setTextContent geändert, Attribute mit setAttribute, und der Baum wird durch das Erstellen von Knoten über die Document-Factory-Methoden und das anschließende Anhängen erweitert. Knoten müssen von demselben Dokument erstellt werden, in das sie eingefügt werden.
// Update existing content.
Element price = (Element) book.getElementsByTagName("price").item(0);
price.setTextContent("49.50");
price.setAttribute("currency", "USD");
// Build a new subtree and attach it.
Element added = doc.createElement("book");
added.setAttribute("id", "b3");
Element title = doc.createElement("title");
title.setTextContent("The Pragmatic Programmer");
added.appendChild(title);
doc.getDocumentElement().appendChild(added);createElement erzeugt einen losgelösten Knoten; im Dokument erscheint nichts, bis er irgendwo per appendChild angehängt wird. Um einen Knoten zu entfernen, ruft man parent.removeChild(child) auf.
Den Baum wieder ausgeben
DOM hat kein toString(), das XML erzeugt. Zum Serialisieren übergibt man das Dokument einem Transformer mit einer DOMSource und einem StreamResult. Das gleiche Paket javax.xml.transform ermöglicht das Schreiben in eine Datei, einen String oder beliebigen Stream sowie das Setzen von Pretty-Printing-Optionen.
Transformer tr = TransformerFactory.newInstance().newTransformer();
tr.setOutputProperty(OutputKeys.INDENT, "yes");
tr.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
tr.transform(new DOMSource(doc), new StreamResult(new File("out.xml")));Bei nicht vertrauenswürdigen Eingaben sollte die Factory vor dem Parsen gehärtet werden – DOCTYPE-Deklarationen mit factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) deaktivieren, um XXE-Angriffe (XML External Entity) zu blockieren.
Ein vollständiges Arbeitsbeispiel
Dieses Programm parst ein im Arbeitsspeicher befindliches Bibliotheksdokument, liest jedes Buch, erhöht jeden Preis um 10%, fügt ein neues Buch ein und serialisiert die erste aktualisierte Preiszeile zurück in XML – und übt dabei den vollständigen Lese-Änderungs-Schreib-Zyklus an einem einzigen Baum.
Was aus der Ausführung zu entnehmen ist:
- Das Wurzelelement wird als
libraryausgegeben, weilgetDocumentElement()den einzelnen obersten Knoten zurückgibt, an dem alles andere hängt. getElementsByTagName("book")meldet vor dem Einfügen eine Anzahl von 2 und bestätigt damit, dass beide<book>-Nachkommen des Wurzelelements erfasst wurden.- Preise werden mit
getTextContent()gelesen und mitDouble.parseDoublegeparst, sodass45.00und38.50zu dem ausgegebenen Gesamtwert von83.50summieren. - Nach
appendChildgibt dieselbegetElementsByTagName("book")-Abfrage 3 zurück und zeigt damit, dass der Live-Baum den mitdoc.createElementerstellten Knoten aufgenommen hat. - Die serialisierte erste Preiszeile lautet
49.50und beweist, dasssetTextContentden speicherinternen Knoten verändert hat und derTransformerden aktualisierten Wert (45.00 um 10% erhöht) zurück in XML geschrieben hat.