Pull to refresh

JAXB и XSLT с использованием StAX

Reading time 5 min
Views 21K
В одном из проектов понадобилось обрабатывать большие XML файлы, от сотен мегабайт до десятков гигабайт.
Причем выдернуть надо было только некоторые тэги с расположенные на различной «глубине». XSLT «в лоб» ломался от недостатка памяти. Пришлось подумать и вспомнить о потоковом парсере.

Существуют несколько моделей обработки XML. Наиболее известные — DOM и SAX.
DOM грузит весь XML документ, строит его внутреннее представление и предоставляет возможность навигации по всему документу. SAX напротив, читает входной документ и при распознавании элемента вызывает handler'ы для обработки.

В моем случае DOM отпал по причине потребляемой памяти. SAX API построено на handler'ах, в результате код получается менее читабельным. StAX представляет из себя потоковый парсер (как и SAX), но API построено на принципе pull. То есть распознанные элементы «вынимаются» из потока по требованию.

Поскольку структуры данных подпадающие под обработку были весьма сложными и разнообразными, а обработка достаточно нетривиальная, решено было использовать JAXB для перевода во внутреннее представление.

Данные проекта закрыты NDA, поэтому в статье не используются.

И так, есть следующий
XML документ
<data>
    <dtype_one>
        <p1>p1_data_1</p1>
        <p2>p1_data_1</p2>
        <p3>p1_data_1</p3>
        <p4>p1_data_1</p4>
        <p5>p1_data_1</p5>
    </dtype_one>
    <dtype_two>
        <p1>p1_data_2</p1>
        <p2>p1_data_2</p2>
        <p3>p1_data_2</p3>
        <p4>p1_data_2</p4>
        <p5>p1_data_2</p5>
    </dtype_two>
    <WS>
        <dtype_three>
            <p1>p1_data_3</p1>
            <p2>p1_data_3</p2>
            <p3>p1_data_3</p3>
            <p4>p1_data_3</p4>
            <p5>p1_data_3</p5>
        </dtype_three>
    </WS>
</data>

из него нужно выделить и обработать тэги dtype_one, dtype_two и dtype_three. Тэги повторяются в документе. Берем
схему документа
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="data" type="dataType"/>
    <xs:element name="dtype_one" type="dtype_oneType"/>
    <xs:element name="dtype_two" type="dtype_twoType"/>
    <xs:element name="dtype_three" type="dtype_threeType"/>

    <xs:complexType name="dtype_oneType">
        <xs:sequence>
            <xs:element type="xs:string" name="p1"/>
            <xs:element type="xs:string" name="p2"/>
            <xs:element type="xs:string" name="p3"/>
            <xs:element type="xs:string" name="p4"/>
            <xs:element type="xs:string" name="p5"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="dataType">
        <xs:sequence>
            <xs:element type="dtype_oneType" name="dtype_one"/>
            <xs:element type="dtype_twoType" name="dtype_two"/>
            <xs:element type="WSType" name="WS"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="WSType">
        <xs:sequence>
            <xs:element type="dtype_threeType" name="dtype_three"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="dtype_twoType">
        <xs:sequence>
            <xs:element type="xs:string" name="p1"/>
            <xs:element type="xs:string" name="p2"/>
            <xs:element type="xs:string" name="p3"/>
            <xs:element type="xs:string" name="p4"/>
            <xs:element type="xs:string" name="p5"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="dtype_threeType">
        <xs:sequence>
            <xs:element type="xs:string" name="p1"/>
            <xs:element type="xs:string" name="p2"/>
            <xs:element type="xs:string" name="p3"/>
            <xs:element type="xs:string" name="p4"/>
            <xs:element type="xs:string" name="p5"/>
        </xs:sequence>
    </xs:complexType>
</xs:schema>


и убеждаемся что в нем есть элементы «element» нужных нам тэгов:
    <xs:element name="dtype_one" type="dtype_oneType"/>
    <xs:element name="dtype_two" type="dtype_twoType"/>
    <xs:element name="dtype_three" type="dtype_threeType"/>

если схемы нет, IDEA отлично может сгенерить ее по xml файлу.

Это нужно для того, что бы XJC сгенерил аннотацию @XmlRootElement. Проект собирается maven, для вызова XJC используется maven-jaxb2-plugin. Для генерации @XmlRootElement для всех «element» в файле схемы, необходимо добавить следующие строки в файл bindings.xjb:
<jaxb:bindings>
        <jaxb:globalBindings >
            <xjc:simple/>
        </jaxb:globalBindings>
    </jaxb:bindings>

и подключить его в конфигурации плагина maven-jaxb2-plugin, в pom.xml
<bindingDirectory>${project.basedir}/xjb</bindingDirectory>

Теперь собственно к коду, класс TagEngine хранит список обработчиков тэгов и занимается разбором:
    public void process(InputStream inputStream) throws FileNotFoundException,
            XMLStreamException, TransformerException {
// Создаем XMLStreamReader, поток разбора
        XMLInputFactory factory = XMLInputFactory.newFactory();
        XMLStreamReader streamReader = factory.createXMLStreamReader(inputStream);
// Стэк тэгов
        Stack<String> tagStack = new Stack<String>();
// Пока есть тэги
        while (streamReader.hasNext()) {
// Берем следующий
            int eventType = streamReader.next();
// Если старт элемента
            if(eventType == XMLStreamConstants.START_ELEMENT) {
// Помещаем в стэк
                tagStack.push(streamReader.getName().toString());
// Ищем совпадение с обработчиком
                TagProcessor t = processorMap.get(tagStack);

                if(t != null) {
// Нашли, обрабатываем
                    t.process(streamReader);
                    tagStack.pop();
                }
            } else if(eventType == XMLStreamConstants.END_ELEMENT) {
                tagStack.pop();
            }
        }
    }

Класс JAXBProcessor занимается unmarshalling'ом выделенных элементов. Класс XSLTProcessor вызывает XSLT преобразования. Вот так выглядит класс выполняющий полезную работу:
public class DataOne extends JAXBProcessor<DtypeOne> {
    private static final String TAG_NAME = "data/dtype_one";

// Конструктор 
    public DataOne() throws JAXBException, SAXException {
        super(DtypeOne.class, TAG_NAME);
    }
// Конструктор с подключением схемы для валидации
    public DataOne(String schemaFileName) throws JAXBException, SAXException {
        super(DtypeOne.class, TAG_NAME, schemaFileName);
    }
// Здесь обрабатываем полученный из XML объект
    @Override
    public void doWork(DtypeOne element) {
//        System.out.println(element.getP1());
    }
}

Пример применения XSLT DataThreeXSLT.

Пример запуска (эмулируется обработка 277 мегабайтного файла):
JAXB unmarshall without schema validation
Runtime: 8034ms, 277000015 bytes processed
Used Memory:80MB
JAXB unmarshall with schema validation
Runtime: 66180ms, 277000015 bytes processed
Used Memory:56MB
XSLT processing
Runtime: 10604ms, 277000015 bytes processed
Used Memory:231MB

С памятью все хорошо, валидация конечно сильно тормозит обработку.

PS. Для тестов использовал Mockito (раньше использовал jmock). Понравилась возможность spy — перехват вызовов и их параметров при работе с живым (не mock) объектом.
PPS. Код проекта на GitHub.
Tags:
Hubs:
+10
Comments 2
Comments Comments 2

Articles