Универсальный загрузчик XML на java. Или как загрузить файлы ГАР на 250 гб и остаться при памяти

от автора

С проблемой загрузки больших XML столкнулся при переходе с КЛАДР и ФИАС на справочники ГАР — Государственный адресный реестр (Федеральная информационная адресная система).

Справочник ГАР содержит более подробную информацию чем предыдущие классификаторы. В том числе информацию по муниципальным делениям. В связи с чем справочник после распаковки занимет около 250 ГБ, что примерно в 3 раза больше чем тот же ФИАС.

Предыдущая загрузка работала на DOM-модели, т.е. весь XML-файл считывался в память. Соответственно при попытке загрузить ГАР таким же способом стали стабильно получать OutOfMemory. А значит настало время менять подход к загрузке)

Немного теории:

DOM (Document Object Model) — это стандартный интерфейс для работы с документами в формате XML (Extensible Markup Language). DOM-модель представляет XML-документ в виде дерева объектов, где каждый элемент и атрибут документа является узлом дерева.

SAX (Simple API for XML) является событийно-ориентированным API для чтения XML-документа. Он предоставляет возможность читать XML-документ последовательно и обрабатывать события, такие как начало и конец элемента, содержимое элемента и т.д.

StAX (Streaming API for XML) также является API для последовательного чтения и записи XML-документов. Он предоставляет потоковый доступ к XML-документу, позволяя читать его и записывать по частям. StAX предоставляет возможность читать и записывать XML-документы в виде потока событий, аналогично SAX, но также предоставляет возможность читать и записывать XML-документы в виде итерируемых наборов событий. StAX позволяет эффективно обрабатывать большие XML-документы и не требует реализации обработчиков событий.

Другими словами:

Загрузка XML-документа с помощью DOM-модели довольно медленная, особенно для больших документов, поскольку требует создания полной структуры DOM в памяти. Однако, DOM-модель позволяет легко и удобно работать с XML-документами, поэтому она широко используется в Java-приложениях.

SAX и StAX позволяет обрабатывать XML-документы любого размера, поскольку он не хранит всю структуру в памяти. Однако, для работы необходимо реализовать обработчики событий.

Одним из главных преимуществ использования StAX является его скорость работы и эффективность. И и отличие от DOM, который загружает весь XML-документ в память перед его обработкой, StAX-парсер обрабатывает XML-документ по одному элементу за раз, что позволяет работать с большими XML-файлами.

Понятно. Останавливаемся на StAX 🙂

Реализуем класс для универсальной загрузки XML. Будем читать данные комфортными порциями и мапить их на произвольные объекты. Класс объекта передаем в загрузчик.

public class XMLAttributeReader {      private Logger logger = LoggerFactory.getLogger(XMLAttributeReader.class);      private InputStream inputStream;     private String attr;     private XMLEventReader eventReader;     private ObjectMapper mapper;     private final Integer RECORDS_COUNT;      private void configure() {         mapper = new ObjectMapper();         mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);         mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);         try {             XMLInputFactory factory = XMLInputFactory.newInstance();             eventReader = factory.createXMLEventReader(inputStream);         } catch (XMLStreamException e) {             logger.error(e.getMessage());         }     }      public void close(){         try {             eventReader.close();         } catch (XMLStreamException e) {             e.printStackTrace();         }     }      public XMLAttributeReader(InputStream inputStream, String attr, Integer RECORDS_COUNT) {         this.inputStream = inputStream;         this.attr = attr;         this.RECORDS_COUNT = RECORDS_COUNT;         configure();     }      public Boolean hasNext() {         return eventReader != null ? eventReader.hasNext() : false;     }      public <T> List<T> getNextPart(Class<T> valueType) {         List<T> valueList = new ArrayList<>();         int count = 0;         try {             while (eventReader.hasNext() && count < RECORDS_COUNT) {                 XMLEvent event = eventReader.nextEvent();                 switch (event.getEventType()) {                     case XMLStreamConstants.START_ELEMENT:                         StartElement startElement = event.asStartElement();                         String qName = startElement.getName().getLocalPart();                         if (qName.equalsIgnoreCase(attr)) {                             Map map = attributesToMap(startElement.getAttributes());                             T value = mapper.convertValue(map, valueType);                             valueList.add(value);                             count++;                         }                         break;                 }             }         } catch (XMLStreamException e) {             logger.error(e.getMessage());         }         return valueList;     }      private static Map attributesToMap(Iterator<Attribute> attributes) {         Map<String, String> map = new HashMap<>();         while (attributes.hasNext()) {             Attribute attr = attributes.next();             map.put(attr.getName().toString(), attr.getValue());         }         return map;     } }

Структура справочника состоит из нескольких типов XML-файлов. Для каждой создадим таблицу в БД, опишим сущности. Пример для адресных объектов:

package com.example.XMLToBase.db.entity;  import javax.persistence.*; import java.util.Date;  //<OBJECT ID="1178934" OBJECTID="948460" OBJECTGUID="b6ea12e7-eb66-46e4-9329-fb3dbfd09827" //        CHANGEID="2615278" //        NAME="Ветеран квартал 6" //        TYPENAME="снт" LEVEL="7" //        OPERTYPEID="50" PREVID="1178870" //        NEXTID="1909861" UPDATEDATE="2021-05-07" STARTDATE="2016-09-29" ENDDATE="2021-05-07" //        ISACTUAL="0" ISACTIVE="0" /><OBJECT ID="1909471" OBJECTID="101148944" //        OBJECTGUID="73104935-cc12-4bc7-b2d1-70c431aa7005" CHANGEID="192807935" //        NAME="Южный" TYPENAME="пер" LEVEL="8" OPERTYPEID="10" PREVID="0" NEXTID="0" //        UPDATEDATE="2021-05-05" STARTDATE="2021-05-05" ENDDATE="2079-06-06" ISACTUAL="1" //        ISACTIVE="1" /><OBJECT ID="1909861" OBJECTID="948460" OBJECTGUID="b6ea12e7-eb66-46e4-9329-fb3dbfd09827" //        CHANGEID="192832273" NAME="Ветеран квартал 6" TYPENAME="снт" LEVEL="7" OPERTYPEID="30" PREVID="1178934" NEXTID="0" //        UPDATEDATE="2021-05-07" STARTDATE="2021-05-07" ENDDATE="2079-06-06" ISACTUAL="1" ISACTIVE="0" /></ADDRESSOBJECTS> @Data @Entity @Table(name = "gar_addressobject") public class GarAddressobject {     @Id     @Column(name = "id")     private Long id;      @Column(name = "objectid")     private Long objectid;      @Column(name = "objectguid")     private String objectguid;      @Column(name = "changeid")     private Long changeid;      @Column(name = "name")     private String name;      @Column(name = "typename")     private String typename;      @Column(name = "level")     private String level;      @Column(name = "opertypeid")     private Long opertypeid;      @Column(name = "previd")     private Long previd;      @Column(name = "nextid")     private Long nextid;      @Column(name = "updatedate")     private Date updatedate;      @Column(name = "startdate")     private Date startdate;      @Column(name = "enddate")     private Date enddate;      @Column(name = "isactual")     private Long isactual;      @Column(name = "isactive")     private Long isactive; } 

Читаем нашим XML загрузчиком адреса пачками в структуру GarAddressobject и тут же производим сохранение в БД.

 private void processAddr(File file){         logger.info("Start loading " + file.getParent() + "/" + file.getName());         try (InputStream is = new FileInputStream(file)) {             XMLAttributeReader xmlReader = new XMLAttributeReader(is, "OBJECT", RECORDS_PER_ITERATION);             int i = 0; int j = 1;             List<GarAddressobject> list;             while (xmlReader.hasNext()) {                 list = xmlReader.getNextPart(GarAddressobject.class);                 garAddressobjectRepository.saveAll(list);                 i += Math.min(RECORDS_PER_ITERATION, list.size());                 if ((i / RECORDS_PER_ITERATION) != j){                     j = i / RECORDS_PER_ITERATION;                     logger.info("saved records: " + i);                 }                 list.clear();             }             logger.info("saved records: " + i);             xmlReader.close();         } catch (IOException e) {             e.printStackTrace();         }     }

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

В итоге имеем:

  • Регламентная загрузка ГАР перестала падать изза нехватки памяти

  • Можно управлять кол-вом строк, которые за раз вычитывает XML-загрузчик

  • Сам загрузчик довольно универсальный, его можно переиспользовать в других задачах

  • Понимаем отличия в подходах DOM и SAX. Знаем где какой вариант лучше подойдет 🙂

Всем спасибо! Комментарии приветствуются ?


ссылка на оригинал статьи https://habr.com/ru/post/724324/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *