StAX

Frissítve: 2017. november 19.

Technológiák: JAXP 1.4, StAX 1.0

Ha már a webszolgáltatásokról volt szó, mely szorosan kapcsolódik az XML-hez, nézzük, hogy mi újítást hozott a JDK 6-ban bevezetett JAXP 1.4. A legnagyobb újdonság a StAX bevezetése, mely egy nagy teljesítményű stream alapú XML feldolgozó szűréssel (filter), mely támogatja a módosítást is.

A JAXP-ben már a W3C által definiált SAX és DOM feldolgozási modell régóta létezett. A SAX esetében esemény alapú feldolgozást lehet megvalósítani, ahol a feldolgozó (parser) hívja az osztályainkat (parser client). Ezt hívják ún. push parsernek, mikor a parser megy végig az XML-elen, és tolja nekünk az adatokat. Ezt viszonylag nehéz programozni, mert tárolni kell az állapotot, manuálisan kell pl. vermet kezelni, hogy tároljuk, hol tartunk a feldolgozásban. A DOM a teljes XML-t betölti egy faként a memóriába, melyen lehet fabejárást is végezni, sőt tetszőlegesen módosítható is. Ez könnyen használható, a fa adatszerkezetet is a legtöbben ismerik, viszont rengeteg memóriára van szüksége. Ennek feloldására a BEA kezdett el kidolgozni egy API-t, mely a JSR 173 specifikációban lett megfogalmazva, Streaming API for XML címmel.

Célja, hogy egyszerűbben programozható legyen, mint a SAX, de kevesebb memóriát fogyasszon, mint a DOM. Streaming API-nak nevezik, mert a parser XML dokumentumnak csak egy részét látja egyszerre. Itt jön képbe a consume (felemészt) fogalom is, ami azt jelenti, hogy a streamen nem haladhatunk visszafele, így ha egyszer átment rajta a parser, akkor a stream beolvasásra került, tehát ha újra fel akarjuk dolgozni, valamilyen módon biztosítani kell a stream újra rendelkezésre állását (pl. klónozás). Ezen kívül a parsert pull parsernek nevezik, mert a kliens irányítja, ő kéri el az adatokat. Ennek több előnye is van, pl. egyszerre több dokumentumot is lehet párhuzamosan feldolgozni, akár összefésülni az eredményeket, valamint a szálat teljes kontroll alatt lehet tartani, ugyanis akkor szüneteltetjük a végrehajtást, mikor akarjuk. A library is kisebb lesz, és a kliens kód is kisebb lesz.

A StAX további előnye, hogy nem csak olvasni (pl. SAX) képes a dokumentumokat, hanem írni is képes azokat. A StAX alapvetően két API-val is rendelkezik, az egyik a cursor API, a másik az iterator API. E mögötti meggondolás az, hogy inkább két egyszerű API-t készítenek, mint egy bonyolultat. Az előbbihez az XMLStreamReader, és XMLStreamWriter osztályok tartoznak, míg a másodikhoz az XMLEventReader és XMLEventWriter osztályok.

A példa projekt letölthető a GitHubról. Nézzük is a cursor API használatával hogyan dolgoznánk fel a következő XML fájlt:

<?xml version="1.0" encoding="UTF-8"?>

<catalog>
    <book isbn10="059610149X">
        <title>Java and XML</title>
        <author>Brett McLaughlin, Justin Edelson</author>
        <publisher>O'Reilly Media</publisher>
        <year>2006</year>
    </book>
    <book isbn10="1590597060">
        <title>Pro XML Development with Java Technology</title>
        <author>Ajay Vohra</author>
        <publisher>Apress</publisher>
        <year>2005</year>
    </book>
    <book isbn10="1449365116">
        <title>Java Web Services: Up and Running</title>
        <author>Martin Kalin</author>
        <publisher>O'Reilly Media</publisher>
        <year>2013</year>
    </book>
</catalog>

A kód, mely ezt feldolgozza cursor API használatával:

List<Book> catalog = new ArrayList<>();
XMLInputFactory f = XMLInputFactory.newInstance();
XMLStreamReader r = f.createXMLStreamReader(source);
Book book = null;
while (r.hasNext()) {                
 if (r.getEventType() == XMLStreamConstants.START_ELEMENT) {
  if ("book".equals(r.getName().getLocalPart())) {
   book = new Book();
   catalog.add(book);
   book.setIsbn10(r.getAttributeValue(null, "isbn10"));
  }
  else if ("title".equals(r.getName().getLocalPart())) {
   book.setTitle(r.getElementText());
  }
 }
 r.next();
}
return catalog;

Látható, hogy először gyártatunk egy XMLInputFactory példányt, majd egy XMLStreamReader példányt. Amíg tart a feldolgozás, addig feldolgozzuk az adatokat, de mindig mi lépünk a következő adatra a next() metódus segítségével (pull API). Az adat típusa, melyen a reader éppen áll, az XML-nél megszokottak lehetnek: XML deklaráció, nyitó tag, záró tag, karakterek, megjegyzések, stb. Attól függően, hogy mi, hívhatjuk meg a reader többi metódusát. Pl. nyitó tag esetén lekérdezhető a getName metódussal a neve, vagy getTextmetódussal a tartalma, sőt a getAttribute* kezdetű metódusokkal az attribútumai is. A XMLStreamException kivételt kezelni kell.

Nézzük, hogyan írjunk ki egy hasonló XML dokumentumot:

StringWriter sw = new StringWriter();
XMLOutputFactory output = XMLOutputFactory.newInstance();
XMLStreamWriter writer = output.createXMLStreamWriter(sw);
writer.writeStartDocument();
writer.writeStartElement("catalog");
for (Book book: catalog) {
 writer.writeStartElement("book");
 writer.writeAttribute("isbn10", book.getIsbn10());
 writer.writeStartElement("title");
 writer.writeCharacters(book.getTitle());
 writer.writeEndElement();
 writer.writeEndElement();
}
writer.writeEndElement();
writer.flush();
return sw.toString();

Nézzük az XML feldolgozást most az iterator API segítségével:

List<Book> catalog = new ArrayList<>();
XMLInputFactory f = XMLInputFactory.newInstance();
XMLEventReader r = f.createXMLEventReader(source);
Book book = null;
while (r.hasNext()) {
 XMLEvent event = r.nextEvent();
 if (event.getEventType() == XMLStreamConstants.START_ELEMENT) {
  if (event instanceof StartElement) {
   StartElement element = (StartElement) event;
   if ("book".equals(element.getName().getLocalPart())) {
    book = new Book();
    catalog.add(book);
    book.setIsbn10(element.getAttributeByName(new QName("isbn10")).getValue());
   }
   else if ("title".equals(element.getName().getLocalPart())) {
    book.setTitle(r.getElementText());
   }
  }
 }
}
return catalog;

Itt látható, hogy nem közvetlenül a reader példányt szólítjuk meg, hanem az event példányt. Ezt aztán a megfelelő típusra kényszeríteni kell, és már el is érhetjük a megfelelő metódusait.

Ahhoz, hogy eldöntsük, hogy a kettő közül melyiket érdemes választani, érdemes a következőket a fejben tartani:

  • Az iterator API XMLEvent osztályai nem módosíthatóak, a parse-olás után is megőrzik az értéküket
  • Emiatt többlépéses (bővíthető, plug-inelhető) feldolgozásokat is könnyebben ki lehet dolgozni ezen példányok továbbadásával
  • A fentiekből következik, hogy a cursor API kevesebb memóriát használ, és a példányosítások hiánya miatt gyorsabb is
  • Az XMLEvent interfészt akár magunk is implementálhatjuk, akár teljesen új eseményt hozhatunk létre, akár egy meglévőt egészíthetünk ki utility metódusokkal
  • Az iterator API esetén módosítani is lehet a streamet

Összességében, ha nem a teljesítmény az elsődleges szempont, érdemes a magasabb szintű iterator API-t használni.