В одном из проектов понадобилось обрабатывать большие XML файлы, от сотен мегабайт до десятков гигабайт.
Причем выдернуть надо было только некоторые тэги с расположенные на различной «глубине». XSLT «в лоб» ломался от недостатка памяти. Пришлось подумать и вспомнить о потоковом парсере.
Существуют несколько моделей обработки XML. Наиболее известные — DOM и SAX.
DOM грузит весь XML документ, строит его внутреннее представление и предоставляет возможность навигации по всему документу. SAX напротив, читает входной документ и при распознавании элемента вызывает handler'ы для обработки.
В моем случае DOM отпал по причине потребляемой памяти. SAX API построено на handler'ах, в результате код получается менее читабельным. StAX представляет из себя потоковый парсер (как и SAX), но API построено на принципе pull. То есть распознанные элементы «вынимаются» из потока по требованию.
Поскольку структуры данных подпадающие под обработку были весьма сложными и разнообразными, а обработка достаточно нетривиальная, решено было использовать JAXB для перевода во внутреннее представление.
Данные проекта закрыты NDA, поэтому в статье не используются.
И так, есть следующий
<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) объектом.
Автор: xlix123