Java XML API: выбираем правильно. StAX: работаем с удовольствием

  • Tutorial
Здравствуйте!
Несмотря на снижение популярности формата XML с начала 2000х, он прочно занял свои ниши. Я сталкивался с обработкой XML ~ в 60% проектов и посвятил ей занятие своей стажировки Masterjava. Наиболее частые его применения: XHTML, SOAP, различные конфигурации (например Tomcat, SoapUI, IntelliJ IDEA, Spring XML конфигурация), импорт-экспорт данных.

В Java есть несколько API для работы с XML и для разработчика важно понимать, какое из API требуется выбрать в каждой конкретной ситуации. В этой статье я кратко перечислю все Java XML API, их назначение и примеры использования, и подробнее остановлюсь на работе с достаточно редкой, но в ряде случаев единственно верной технологией StAX. Предполагается что с элементами XML вы уже знакомы.

Java XML API: выбираем правильно


  • JAXP (Java API for XML Processing) это набор API (SAX + DOM + валидация DTD + XSLT). Xerces и Xalan — стандартные реализации этих API. Кроме XSLT все API устарели: на смену SAX пришел StAX, DOM сменил JAXB, DTD заменил XSD.
  • DOM и JAXB — API для полного зачитывания XML и получения в приложении его готового представления в объектах Java. Для DOM это коллекции реализаций интерфейса org.w3c.dom.Node (атрибуты, элементы, текст, ..). Для JAXB задается мапинг, наподобие тому, как в ORM задаются отображения таблиц базы данных в объекты Java. Т.е. из XML получаются готовые и удобные в использовании Java бины. JAXB — наиболее удобный и часто используемый API для работы с XML, когда требуется почитать весь XML (он должен поместиться в память JVM) и выполнить с ним требуемые действия.
  • DTD (устаревший) и XSD — схемы, задающие проверку структуры XML (порядок следования элементов, обязательность или опциональность элемента, наличие атрибутов у элемента). К XML можно привязать схему проверки структуры, и тогда инструменты, работающие с XML (например IntelliJ IDEA) могут делать автодополнение и показывать ошибки схемы. Также можно провалидировать XML документ из приложения. Формат XSD более современный и сам является XML документом.
  • XPath язык запросов к XML. Грубо можно сравнить с запросом SQL к базе данных, кода нам нужно достать из XML какой-то определенный элемент (или элементы) или подсчитать их количество. Я видел их применение, например, в ESB: данные между системами передаются в XML формате и в конфигурации каждая система через XPath «выкусывает» из XML нужные ей данные. Чаще всего XPath используется при трансформации XML.
  • XSL, XSLT — преобразования XML в любой другой формат. Одно из применений, например, отдать с сервера в браузер ответ в виде XML и задать его XSL-преобразование в HTML. Все современные браузеры поддерживают XSLT преобразования, т.е. на стороне клиента самостоятельно трансформируют XML по заданной XSL к требуемому виду.
  • SAX и StAX — последовательное чтение из источника XML.Документ читается последовательно по кусочкам (событиям). API между собой отличаются тем, что SAX основан на модели push, а Stax на модели pull. Если вы когда-то делали на javascript несколько последовательных AJAX запросов, то сталкивались с callback hell. Выполнение кода делается асинхронно и код пишется не последовательно, а ступеньками в функциях возврата. При использовании SAX происходит нечто подобное: нужно задавать функции для обработки определенных событий в XML (начало тэга, конец тэга, текст внутри тэга, комментарий) и внутри этих обработчиков задавать новые обработчики. Работать со StAX гораздо удобнее. Мы последовательно читаем из документа события, анализируем их и обрабатываем подходящие. Эти API используются при очень больших документах, из которых зачитываются списки объектов по одному по мере их появления в документе, либо когда нам интересен не весь документ, а его определенная часть и модель всего документа не нужна.

В сравнительной табличке API по их возможностям Easy of Use для SAX/StAX говорит о том,что автор не умеет работать со StAX и оставшаяся часть статьи будет о том, как «правильно его готовить».

StAX: работаем с удовольствием


Прежде всего хочу отметить, что со StAX можно работать через 2 API: низкоуровневое XMLStreamReader, возвращающая примитивы и высокоуровневое XMLEventReader, которое возвращает объекты и расходует больше памяти. Далее я буду работать с XMLStreamReader. Сделав над ним обертку работа с XML будет простой и удобной.Разберем небольшой пример: есть простой XML с городами и юзерами:
    <Payload>
        <Cities>
            <City id="spb">Санкт-Петербург</City>
            <City id="mow">Москва</City>
            ...
        </Cities>
        ...
        <Users>
            <User city="mow">
                <email>gmail@gmail.com</email>
                <fullName>Gmail User</fullName>
            </User>
            <User city="spb">
                <email>admin@javaops.ru</email>
                <fullName>Admin</fullName>
            </User>
            ...
        </Users>
        ...
    </Payload>
В реальности этот XML может содержать сотни городов и сотни тысяч / миллионы юзеров. Все, что требуется: распечатать список городов. В данном случае StAX API — единственно верный выбор. Добавляем в проект вспомогательный класс StaxStreamProcessor:
public class StaxStreamProcessor implements AutoCloseable {
    private static final XMLInputFactory FACTORY = XMLInputFactory.newInstance();

    private final XMLStreamReader reader;

    public StaxStreamProcessor(InputStream is) throws XMLStreamException {
       reader = FACTORY.createXMLStreamReader(is);
    }

    public XMLStreamReader getReader() {
       return reader;
    }

    @Override
    public void close() {
       if (reader != null) {
          try {
             reader.close();
          } catch (XMLStreamException e) { // empty
          }
       }
    }
}
Далее мы последовательно идем по XML, считываем все интересные нам события и выводим требуемую информацию:
try (StaxStreamProcessor processor = new StaxStreamProcessor(Files.newInputStream(Paths.get("payload.xml")))) {
     XMLStreamReader reader = processor.getReader();
     while (reader.hasNext()) {       // while not end of XML
         int event = reader.next();   // read next event
         if (event == XMLEvent.START_ELEMENT && 
"City".equals(reader.getLocalName())) {
             System.out.println(reader.getElementText());
         }
     }
}
Чтобы постоянно не дублировать в программе часто повторяющийся код поиска нужного события в XML, мы можем добавить его в StaxStreamProcessor:
public boolean doUntil(int stopEvent, String value) throws XMLStreamException {
   while (reader.hasNext()) {
      int event = reader.next();
      if (event == stopEvent && value.equals(reader.getLocalName())) {
         return true;
      }
   }
   return false;
}
Пользоваться утильным классом станет не просто, а очень просто:
while (processor.doUntil(XMLEvent.START_ELEMENT, "City")){
    System.out.println(reader.getElementText());
}
Недостаток этого кода — мы совершенно бесполезно потратим ресурсы на прохождение по сотням тысяч ненужных нам юзеров вместо завершения программы. Нужно добавить условие прекращение сканирования XML. Обычно это конец тэга родительского элемента (в нашем случае Cities). Добавляем в StaxStreamProcessor еще один утильный метод, который сканирует XML либо до конца тэга родителя, либо до заданного элемента:
public boolean startElement(String element, String parent) throws XMLStreamException {
    while (reader.hasNext()) {
       int event = reader.next();
       if (parent != null && event == XMLEvent.END_ELEMENT &&
 parent.equals(reader.getLocalName())) {
          return false;
       }
       if (event == XMLEvent.START_ELEMENT && 
element.equals(reader.getLocalName())) {
          return true;
       }
    }
    return false;
}
Добавим методы чтения атрибута и текста:
public String getAttribute(String name) throws XMLStreamException {
   return reader.getAttributeValue(null, name);
}
public String getText() throws XMLStreamException {
   return reader.getElementText();
}
Код вызова останется суперпростым и мы прекратим обработку XML сразу после конца тега Cities:
while (processor.startElement("City", "Cities")) {
   System.out.println(processor.getAttribute("id") +":" + processor.getText());
}
StAX API требует аккуратность при чтении событий. Если в выводе мы помяняем местами чтение атрибута и текста, но код станет нерабочим: после прочтения из XML названия города атрибут останется позади и будет недоступен. Также следует помнить, что, в зависимости от текущего положения XML, нам доступны одни методы API чтения из XML и недоступны другие.Используя startElement можно добираться до элементов XML любой степени вложенности и, по мере необходимости, дополнять StaxStreamProcessor другими утильными методами. Надеюсь что с данным подходом работа с StAX покажется вам легкой и удобной.

Спасибо за внимание и приятного кодинга!
  • +9
  • 13,8k
  • 6
Поделиться публикацией
Комментарии 6
    +1
    С этими новыми API такая ситуация. У всех уже куча кода для работы через SAX или DOM. Когда в очередной раз возникает задача что-то распарсить, то народ не заморачивается, а переиспользует свой старый проверенный код. Поэтому новый API оказался не очень востребован.
      +1
      JAXB — API для полного зачитывания XML

      Вообще-то есть JAXB Fragment (SO — JAXB Fragmented Marshalling) чтобы все не читать (работает поверх Stax)
      0
      Забыли упомянуть про XStream, в некоторых ситуациях до сих пор удобнее его использовать.
        0

        Я писал про стандартные API, которые входят в Java SE. Стронииих библиотек огромное количество. XStream не разу не юзал. В каких случаях им пользоваться удобнее?

          0
          Когда нету времени возиться с биндингами. Когда нужно читать файл несколько раз. Когда скорость важна, на 2ГБ файлах и более маршалинг добавляет тормозов.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое