Pull to refresh

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

Level of difficultyMedium
Reading time5 min
Views5.5K

С проблемой загрузки больших 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. Знаем где какой вариант лучше подойдет :)

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

Tags:
Hubs:
Total votes 11: ↑5 and ↓6+3
Comments19

Articles