Содержание


Использование StAX для обработки XML

Введение в потоковый API (Streaming API) для XML (StAX)

Сначала давайте исследуем API с использованием курсора, которое запрашивает XML-данные в виде потока лексем (событий).

Comments

Серия контента:

Этот контент является частью # из серии # статей: Использование StAX для обработки XML

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Использование StAX для обработки XML

Следите за выходом новых статей этой серии.

Общая информация о StAX

С момента своего выхода стандарт Java-API for XML Processing (JAXP) предоставляет два метода обработки XML - метод объектной модели документа (DOM), который для представления XML-документов использует стандартную объектную модель, и метод SAX (Simple API for XML, SAX), который для обработки XML использует предоставляемые приложениями обработчики событий. Поточная альтернатива этим двум подходам была предложена в стандарте JSR-173: Streaming API for XML (StAX). Окончательная редакция стандарта была опубликована в марте 2004 года и стала частью JAXP 1.4 (которая будет включена в разрабатываемую версию Java 6).

Как можно понять из названия стандарта, StAX фокусируется на поточной обработке. По сути, именно возможность приложений обрабатывать данные XML в виде потока событий отличает стандарт StAX от других подходов. Идея обработки XML как набора событий не слишком свежая (по сути, она уже представлена в стандарте SAX); однако в StAX программный код может запрашивать такие события одно за другим вместо того, чтобы предоставлять обработчики, которые получали бы события от синтаксического анализатора в определяемом самим анализатором порядке.

На самом деле StAX состоит из двух наборов API для обработки XML, которые обеспечивают разные уровни абстракции. API с использованием курсора позволяет приложениям работать с XML как с потоком лексем (или событий); приложение может проверить статус анализатора и получить информацию о последней проанализированной лексеме, а затем перейти к следующей. Это, скорее, низкоуровневый API; он довольно эффективный, но не предоставляет абстракции нижележащей XML-структуры. Высокоуровневый API, использующий итераторы событий, позволяет приложению обрабатывать XML как серию объектов событий, каждый из которых взаимодействует с фрагментом XML-структуры приложения. Все, что требуется от приложения - это определить тип синтаксически разобранного события, отнести его к соответствующему конкретному типу и использовать соответствующие методы для получения информации, относящейся к событию.

Основы

Чтобы использовать любой из этих двух API, приложение сначала должно иметь существующий класс XMLInputFactory. В классическом JAXP это осуществляется при помощи шаблона Abstract Factory; класс XMLInputFactory предоставляет статические методы newInstance, которые отвечают за поиск и реализацию экземпляра конкретной фабрики. Чтобы сконфигурировать этот экземпляр, можно задать пользовательские или предварительно определенные свойства (имена которых описаны в классе XMLInputFactory). И наконец, чтобы использовать API с использованием курсора, приложение получает объект XMLStreamReader посредством вызова методов createXMLStreamReader. Альтернативно, чтобы использовать API с использованием итератора событий, приложение вызывает один из методов createXMLEventReader, чтобы создать интерфейс XMLEventReader (см. листинг 1).

Листинг 1. Создание и настройка конфигурации объекта по умолчанию XMLInputFactory
// get the default factory instance
XMLInputFactory factory = XMLInputFactory.newInstance();
// configure it to create readers that coalesce adjacent character sections
factory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
XMLStreamReader r = factory.createXMLStreamReader(input);
// ...

Оба интерфейса - и XMLStreamReader, и XMLEventReader - позволяют приложению самому выполнять итерации на нижележащем XML-потоке. Различие между двумя подходами заключается в том, как каждый из них представляет фрагменты разобранного XML InfoSet. Интерфейс XMLStreamReader действует как курсор, который размещается сразу после разобранной самой последней XML-лексемы и предоставляет методы для получения информации о ней. Этот подход очень эффективно использует память, так как не создает новых объектов. Однако разработчики бизнес-приложений могут считать интерфейс XMLEventReader несколько более интуитивно понятным, потому что он, по сути, представляет собой стандартный Java-итератор, который преобразует XML в поток объектов событий. Каждый объект события, в свою очередь, инкапсулирует информацию, имеющую отношение к конкретной XML-структуре, которую он представляет. В части 2 этой серии будет опубликовано подробное описание API с использованием итераторов событий.

Какой из стилей API использовать, зависит от ситуации. API с использованием итераторов событий является более объектно-ориентированным подходом, чем API с использованием курсора. По существу, его проще использовать в модульной архитектуре, поскольку текущее состояние синтаксического анализатора отражается в объекте события; следовательно, компоненту приложения не нужен доступ к анализатору/считывателю при обработке события. Более того, XMLEventReader можно создать из XMLStreamReader при помощи метода объекта XMLInputFactorycreateXMLEventReader(XMLStreamReader).

StAX также определяет API записи файловых объектов, функцию, которая, к сожалению, не была включена в стандартную поддержку обработки XML в Java. Этот потоковый API, как и его анализирующий аналог, поставляется в двух разновидностях - низкоуровневый интерфейс XMLStreamWriter, который работает с лексемами, и высокоуровневый интерфейс XMLEventWriter, который работает с объектами событий. XMLStreamWriter предоставляет методы для записи отдельных XML-лексем (таких как тэги открытия и закрытия или атрибуты элементов) без проверки их корректности. Интерфейс XMLEventWriter, с другой стороны, позволяет приложению добавлять в вывод целые объекты событий XML. В части 3 мы подробно рассмотрим API записи файловых объектов StAX.

Почему мы советуем использовать StAX?

Возможно, до того, как вы решите изучать новый API для обработки XML, вам хочется узнать, стоит ли тратить на это силы. По сути, подход обработки по запросу к анализатору, используемый StAX, дает несколько важных преимуществ по сравнению с другими методами. Во-первых, независимо от используемого стиля API, именно приложение вызывает считыватель (анализатор), а не наоборот. Сохраняя контроль над процессом синтаксического анализа, вы можете упростить для вызывающего кода точную обработку контента, который он ожидает, и выбрать прекращение анализа при появлении чего-либо неожиданного. Более того, поскольку этот метод не основан на обратных вызовах обработчиков, приложению не придется обслуживать эмулированное состояние анализатора, как это бывает при использовании SAX.

В StAX также сохраняются преимущества, которые есть в SAX по сравнению с DOM. Перенося фокус с результирующей объектной модели на сам анализирующий поток, приложения получают возможность обрабатывать теоретически бесконечные XML-потоки, поскольку события в своей основе являются временными и не нуждаются в накоплении в памяти. Это особенно важно для группы приложений, которые используют XML как протокол обмена сообщениями, а не для представления содержимого документов; таковы, например, web-сервисы или приложения обмена мгновенными сообщениями. Например, для сервлета маршрутизации web-сервиса не слишком много пользы в использовании DOM, если нужно всего лишь перевести его в специфическую для приложения объектную модель, а затем удалить. Использование StAX для прямого перехода в модель приложения будет более эффективным. Для клиента протокола Extensible Messaging and Presence Protocol (XMPP) использовать DOM просто невозможно - клиент/серверный поток XMPP постепенно генерируется в режиме реального времени из вводимых пользователем сообщений. Ждать закрывающего тэга потока (чтобы завершить построение DOM) равносильно ожиданию завершения общения. Обрабатывая XML как серию событий, приложение может реагировать на каждое событие наиболее подходящим образом (например, отображать входящее мгновенное сообщение и т. д.)

Благодаря своей двунаправленной природе, StAX также поддерживает сцепленную обработку, особенно на уровне событий. Способность принимать события (от любого источника) инкапсулирована в интерфейсе XMLEventConsumer, который является расширением интерфейса XMLEventWriter. Следовательно, можно создать приложение в виде модулей, которые будут считывать XML-события из XMLEventReader (который является обычным итератором и может рассматриваться именно как итератор), обрабатывать их, а затем передавать потребителю события (который может впоследствии и далее продолжать цепочку обработки, если нужно). Как вы узнаете из части 2, можно также настроить интерфейс XMLEventReader при помощи фильтров, предоставляемых приложениями (класс, реализующий интерфейс EventFilter), или посредством декорирования существующего интерфейса XMLEventReader при помощи EventReaderDelegate.

Если смотреть в перспективе, StAX больше приближает приложение к нижележащим XML-данным, чем DOM или SAX. При помощи StAX приложение может не только построить нужную объектную модель (вместо того, чтобы иметь дело со стандартной моделью DOM), но и сделать это тогда, когда это будет удобно приложению, а не только после получения обратного вызова от синтаксического анализатора.

В следующем разделе мы углубимся в детали API с использованием курсора и способов использования этого API для эффективной обработки XML-потоков.

API с использованием курсора

Работая с API, в котором используется курсор, приложение обрабатывает XML, перемещая логический курсор через поток XML-лексем. Анализатор, использующий курсор, по существу представляет собой конечный автомат, выполняющий переход от одного точно определенного состояния в другое в результате наступления какого-либо события. В этом случае запускающее событие представляет собой лексему XML, которая анализируется, когда приложение перемещает анализатор вместе с потоком лексем при помощи соответствующих методов. В каждом состоянии можно использовать набор методов, чтобы получить информацию о самом последнем событии. Как правило, не все методы можно применять во всех состояниях.

Чтобы применить подход с использованием курсора, приложение сначала должно получить считыватель XMLStreamReader от класса XMLInputFactory, вызвав один из его методов createXMLStreamReader. Существует несколько версий этого метода, каждый из которых поддерживает различные типы ввода. Например, можно создать XMLStreamReader для синтаксического разбора plain java.io.InputStream, java.io.Reader, а можно и JAXP Source (javax.xml.transform.Source). Теоретически, последний вариант должен упростить взаимодействие с другими технологиями JAXP, например, SAX или DOM.

Листинг 2. Создание XMLStreamReader для синтаксического анализа InputStream
URL url = new URL(uri);
InputStream input = url.openStream();
XMLInputFactory factory = XMLInputFactory.newInstance();
XMLStreamReader r = factory.createXMLStreamReader(uri, input);
// process the stream
// ...
r.close();
input.close();

Интерфейс XMLStreamReader, в основном, описывает API с использованием курсора (хотя постоянные лексемы описываются в его супертипе, интерфейсе XMLStreamConstants). Такой интерфейс называется интерфейсом с использованием курсора, потому что считыватель действует подобно курсору в нижележащем потоке лексем. Приложение может перемещать курсор вперед вместе с потоком лексем и проверять лексемы в точке расположения курсора.

Интерфейс XMLStreamReader предоставляет несколько методов перемещения по потоку лексем. Для определения типа лексемы (или события), на которые в данный момент указывает курсор, приложение может вызвать метод getEventType(). Этот метод возвращает одну из постоянных лексемы, описанных в интерфейсе XMLStreamConstants. Для перемещения к следующей лексеме приложение может вызвать метод next(). Этот метод также возвращает тип разобранной лексемы - то же значение, которое было бы возвращено последующим вызовом getEventType(). Последний метод (и другие считывающие/перемещающие методы) могут быть вызваны, только если значение, возвращенное методом hasNext(), будет равно true (то есть, еще остались лексемы, которые нужно анализировать).

Листинг 3. Часто используемый шаблон для обработки XML при помощи XMLStreamReader
// create an XMLStreamReader
XMLStreamReader r = ...;
try {
      int event = r.getEventType();
      while (true) {
            switch (event) {
            case XMLStreamConstants.START_DOCUMENT:
            // add cases for each event of interest
            // ...
            }

            if (!r.hasNext())
                  break;
            
            event = r.next();
      }
} finally {
      r.close();
}

Несколько других методов могут вызвать перемещение считывателя. Метод nextTag() пропустит все пробелы, комментарии или инструкции по обработке, пока не дойдет до элементов START_ELEMENT или END_ELEMENT. Этот метод полезен при синтаксическом анализе только элементов содержимого; если он обнаружит, что перед тегом имеется текст, не являющийся пробелом (и не являющийся комментарием или инструкцией по обработке), он вызовет исключительную ситуацию. Метод getElementText() возвратит все текстовое содержимое элемента между открывающим и закрывающим тэгами (то есть, между START_ELEMENT и END_ELEMENT). Он обработает исключительную ситуацию, если не обнаружит вложенных элементов.

Мы видим, что в этом контексте термины "лексемы" и "события" взаимозаменяемы. Хотя в документации по API с использованием курсора говорится о событиях, проще представить источник ввода в виде потока лексем. Кроме всего прочего, это избавляет от лишней путаницы, поскольку существует еще и совсем другой стиль API с использованием событий (в котором события являются настоящими объектами). Однако не все события XMLStreamReader представляют собой лексемы. Например, события START_DOCUMENT и END_DOCUMENT не требуют соответствующих лексем. Первое событие происходит до начала синтаксического анализа, а второе - когда синтаксический анализ больше не может выполняться (например, после разбора закрывающего тэга последнего элемента, считыватель находится в состоянии END_ELEMENT; однако после попытки продолжить анализ лексем считыватель обнаруживает, что их больше нет, и переходит в состояние END_DOCUMENT).

Обработка XML

В каждом состоянии анализатора приложение может использовать подходящий метод, чтобы получить информацию о нем. Например, методы getNamespaceContext() и getNamespaceURI() могут возвратить, соответственно, контекст текущего пространства имен и URI пространства имен, актуальный на настоящий момент, независимо от типа текущего события. Аналогично, метод getLocation() может дать информацию о размещении текущего события. Методы hasName() и hasText() могут выяснить, имеет ли текущее событие имя (как элемент или атрибут) или текст (например, символы, комментарии или CDATA) Методы isStartElement(), isEndElement(), isCharacters() и isWhiteSpace() представляют собой ярлыки последовательности для определения характера текущего события. И наконец, метод require(int, String, String) может оценить ожидаемое состояние синтаксического анализатора; он не вызовет исключительную ситуацию только в том случае, если текущее событие будет событием определенного типа, а локальное имя и пространство имен, если они заданы, будут соответствовать текущему событию.

Листинг 4. Использование методов, относящихся к атрибутам, которые доступны, если текущее событие - START_ELEMENT
if (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
      System.out.println("Start Element: " + reader.getName());
      for(int i = 0, n = reader.getAttributeCount(); i < n; ++i) {
            QName name = reader.getAttributeName(i);
            String value = reader.getAttributeValue(i);
            System.out.println("Attribute: " + name + "=" + value);
      }
}

Сразу после создания считыватель XMLStreamReader запускается в состоянии START_DOCUMENT (то есть, метод getEventType() возвращает значение START_DOCUMENT). Имейте это в виду при обработке лексем. В отличие от итератора, курсор необходимо сначала переместить (используя метод next()), чтобы он оказался в действующем состоянии. Аналогично, приложению не следует пытаться перемещать курсор после перехода считывателя в его конечное состояние -- END_DOCUMENT. После того как считыватель перейдет в это состояние, метод hasNext() будет возвращать значение false.

Событие START_DOCUMENT предоставляет методы для получения информации о самом документе; это такие методы, как getEncoding(), getVersion() и isStandalone(). Приложение также может получить значения именованных свойств посредством вызова метода getProperty(String); однако некоторые свойства описаны только в определенных состояниях (например, свойства javax.xml.stream.notations и javax.xml.stream.entities возвращают все нотации и объявления объекта, соответственно, если текущее событие представляет собой DTD).

В START_ELEMENT и END_ELEMENT можно использовать методы, относящиеся к имени элемента и пространству имен (такие как getName(), getLocalName(), getPrefix() и getNamespaceXXX()); методы, относящиеся к атрибутам (getAttributeXXX()), также доступны в START_ELEMENT.

ATTRIBUTE и NAMESPACE также считаются изолированными событиями, хотя их вряд ли можно встретить при синтаксическом анализе типичного XML-документа. Однако с ними можно встретиться, когда в качестве результата запроса XPath возвращается узел ATTRIBUTE или NAMESPACE.

В текстовых событиях ( CHARACTERS, CDATA, COMMENT и SPACE) текст получают при помощи различных методов getTextXXX(). Можно извлечь назначение и данные объекта PROCESSING_INSTRUCTION при помощи методов getPITarget() и getPIData() соответственно. ENTITY_REFERENCE и DTD также поддерживают метод getText(); ENTITY_REFERENCE поддерживает также метод getLocalName().

После завершения синтаксического анализа приложение закрывает считыватель, чтобы высвободить все ресурсы, которые были задействованы в процессе обработки. Обратите внимание, что при этом нижележащий источник ввода не закрывается.

В листинге 5 показан готовый пример применения API с использованием курсора для обработки XML-документа. Сначала получаем экземпляр класса XMLInputFactory и создаем считыватель XMLStreamReader для выполнения синтаксического анализа потока ввода. Затем состояние считывателя итеративно проверяется и, в зависимости от типа текущего события, генерируется отчет, содержащий определенную информацию (например, имя элемента и его атрибуты, если определяется состояние START_ELEMENT). Наконец, считыватель завершает работу по достижении события END_DOCUMENT.

Листинг 5. Готовый пример использования XMLStreamReader для синтаксического анализа XML-документа
XMLInputFactory factory = XMLInputFactory.newInstance();
XMLStreamReader r = factory.createXMLStreamReader(input);
try {
      int event = r.getEventType();
      while (true) {
            switch (event) {
            case XMLStreamConstants.START_DOCUMENT:
                  out.println("Start Document.");
                  break;
            case XMLStreamConstants.START_ELEMENT:
                  out.println("Start Element: " + r.getName());
                  for(int i = 0, n = r.getAttributeCount(); i < n; ++i)
                        out.println("Attribute: " + r.getAttributeName(i) 
                              + "=" + r.getAttributeValue(i));
                  
                  break;
            case XMLStreamConstants.CHARACTERS:
                  if (r.isWhiteSpace())
                        break;
                  
                  out.println("Text: " + r.getText());
                  break;
            case XMLStreamConstants.END_ELEMENT:
                  out.println("End Element:" + r.getName());
                  break;
            case XMLStreamConstants.END_DOCUMENT:
                  out.println("End Document.");
                  break;
            }
            
            if (!r.hasNext())
                  break;

            event = r.next();
      }
} finally {
      r.close();
}

Дополнительные применения XMLStreamReader

Существует также возможность создать интерфейс XMLStreamReader с фильтром посредством вызова метода класса XMLInputFactory' createFilteredReader с базовым считывателем и фильтром, определенным приложением (то есть, экземпляр класса, реализующий StreamFilter). В процессе перемещения считывателя с фильтром, фильтр применяется при каждом перемещении считывателя к следующей лексеме. Если фильтр пропускает текущее событие, то оно будет представлено фильтрующим считывателем. Если нет, то считыватель пропустит данную лексему и протестирует следующую, и так далее. Такой подход позволяет разработчикам создать обработчики XML с использованием курсора, которые будут обрабатывать облегченные подмножества разобранного контента и повторно использовать их в сочетании с фильтрами для различных моделей расширяемого контента.

Для выполнения более сложных манипуляций с потоками, определите подкласс StreamReaderDelegate и отмените соответствующие методы. Экземпляр этого подкласса впоследствии может использоваться для упаковки базового считывателя XMLStreamReader, благодаря чему приложение получит измененное представление базового XML-потока. Эту методику можно использовать для выполнения простых преобразований XML-потока, например, фильтрации или замещения отдельных лексем, или даже добавления к потоку нового потока.

В листинге 6 мы упакуем базовый XMLStreamReader при помощи пользовательского StreamReaderDelegate и отменим метод next(), чтобы пропустить события COMMENT и PROCESSING_INSTRUCTION. При использовании считывателя, полученного в результате, приложению не придется беспокоиться о том, что ему встретятся эти типы лексем.

Листинг 6. Использование пользовательского StreamReaderDelegate для фильтрации комментариев и инструкций по обработке
URL url = new URL(uri);
InputStream input = url.openStream();

XMLInputFactory f = XMLInputFactory.newInstance();
XMLStreamReader r = f.createXMLStreamReader(uri, input);
XMLStreamReader fr = new StreamReaderDelegate(r) {
      public int next() throws XMLStreamException {
            while (true) {
                  int event = super.next();
                  switch (event) {
                  case XMLStreamConstants.COMMENT:
                  case XMLStreamConstants.PROCESSING_INSTRUCTION:
                        continue;
                  default:
                        return event;
                  }
            }
      }
};

try {
      int event = fr.getEventType();
      while (true) {
            switch (event) {
            case XMLStreamConstants.COMMENT:
            case XMLStreamConstants.PROCESSING_INSTRUCTION:
                  // this should never happen
                  throw new IllegalStateException("Filter failed!");
            default:
                  // process XML normally
            }

            if (!fr.hasNext())
                  break;

            event = fr.next();
      }
} finally {
      fr.close();
}

input.close();

О том, что скрывается за пределами обработки с использованием курсора

Как вы могли убедиться, API с использованием курсора эффективен во многих случаях. Вся информация о состояниях доступна непосредственно из считывателя потока, при этом не создаются дополнительные объекты. Это особенно полезно в тех приложениях, для которых производительность и низкое потребление памяти имеют большое значение.

Преимущества синтаксического анализа XML на основе запросов известны уже давно. По сути, сама технология StAX выросла из подхода под названием XML Pull Parsing. API XML Pull Parser аналогичен API с использованием курсора, который предоставляется StAX; состояние анализатора может быть проверено для получения информации о последнем разобранном событии, затем выполняется перемещение к следующему событию, и так далее. В этом API не было альтернативного варианта с использованием итератора событий. Он не требователен к ресурсам и особенно подходит для сред с ограниченными ресурсами, таких как J2ME. Однако лишь небольшое число реализаций предоставляли функции корпоративного уровня, такие, как проверка корректности, поэтому XML Pull никогда не был популярным среди Java-разработчиков.

Учитывая опыт предыдущих реализаций, использующих запросы к синтаксическому анализатору, создатели StAX решили включить в него объектно-ориентированную альтернативу API с использованием курсора. Несмотря на то, что интерфейс XMLEventReader кажется обманчиво простым, подход с использованием итератора событий предлагает существенное преимущество по сравнению с методом, в котором используется курсор. Посредством превращения событий синтаксического анализатора в объекты первого уровня, он позволяет приложению обрабатывать их объектно-ориентированным способом. Это способствует улучшенной модульности и многократному использованию кода в нескольких компонентах приложения.

Листинг 7. Синтаксический анализ XML при помощи считывателя StAX XMLEventReader
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
XMLEventReader reader = inputFactory.createXMLEventReader(input);
try {
      while (reader.hasNext()) {
            XMLEvent e = reader.nextEvent();
            if (e.isCharacters() && ((Characters) e).isWhiteSpace())
                  continue;
            
            out.println(e);
      }
} finally {
      reader.close();
}

Заключение

В этой статье вы познакомились со стандартом StAX и его низкоуровневым API с использованием курсора. В части 2 данной серии статей мы более подробно рассмотрим API с использованием итератора событий.


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи StAX'ing up XML, Part 1: An introduction to Streaming API for XML (StAX);
  • JSR 173: Streaming API for XML (StAX): Ознакомьтесь с требованиями спецификации Java, предлагающей Java-API для обработки XML на основе запросов;
  • XML Pull Parsing: Ознакомьтесь с этим сайтом, созданным для распространения и преподавания синтаксического анализа XML на основе запросов;
  • BEA Dev2Dev Online: StAX: Посетите web-страницу BEA, посвященную StAX, со ссылкой на реализацию StAX компании WebLogic;
  • All about JAXP, Part 1 (Все о JAXP, часть 1) (Брет Мак-Лафлин (Brett McLaughlin), сайт developerWorks, май 2005 г.): Узнайте о том, как работать с функциями синтаксического анализа и проверки корректности API JAXP;
  • Техническая библиотека XML: Посетите раздел сайта developerWorks XML Zone и найдите широкий диапазон технических статей и советов, учебных руководств, стандартов и справочников Red Book IBM;
  • Узнайте все об XML в разделе сайта developerWorks XML Zone..

Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML, Технология Java
ArticleID=209194
ArticleTitle=Использование StAX для обработки XML: Введение в потоковый API (Streaming API) для XML (StAX)
publish-date=04132007