Статьи

Написание больших файлов JSON с Джексоном

Иногда вам нужно экспортировать много данных в файл JSON. Может быть, это «экспорт всех данных в JSON» или «Право на мобильность» GDPR, где вам нужно сделать то же самое.

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

К счастью, это довольно просто сделать с помощью SequenceWriter Джексона и, возможно, потоковых каналов. Вот как это будет выглядеть:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private ObjectMapper jsonMapper = new ObjectMapper();
    private ExecutorService executorService = Executors.newFixedThreadPool(5);
 
    @Async
    public ListenableFuture<Boolean> export(UUID customerId) {
        try (PipedInputStream in = new PipedInputStream();
                PipedOutputStream pipedOut = new PipedOutputStream(in);
                GZIPOutputStream out = new GZIPOutputStream(pipedOut)) {
         
            Stopwatch stopwatch = Stopwatch.createStarted();
 
            ObjectWriter writer = jsonMapper.writer().withDefaultPrettyPrinter();
 
            try(SequenceWriter sequenceWriter = writer.writeValues(out)) {
                sequenceWriter.init(true);
             
                Future<?> storageFuture = executorService.submit(() ->
                       storageProvider.storeFile(getFilePath(customerId), in));
 
                int batchCounter = 0;
                while (true) {
                    List<Record> batch = readDatabaseBatch(batchCounter++);
                    for (Record record : batch) {
                        sequenceWriter.write(entry);
                    }
                }
 
                // wait for storing to complete
                storageFuture.get();
            
 
            logger.info("Exporting took {} seconds", stopwatch.stop().elapsed(TimeUnit.SECONDS));
 
            return AsyncResult.forValue(true);
        } catch (Exception ex) {
            logger.error("Failed to export data", ex);
            return AsyncResult.forValue(false);
        }
    }

Код делает несколько вещей:

  • Использует SequenceWriter для непрерывной записи записей. Он инициализируется с помощью OutputStream, в который все записано. Это может быть простой FileOutputStream или конвейерный поток, как описано ниже. Обратите внимание, что именование здесь немного вводит в заблуждение — writeValues(out) звучит так, как будто вы даете указание автору написать что-то сейчас; вместо этого он настраивает его для использования определенного потока позже.
  • SequenceWriter инициализируется значением true , что означает «перенос в массив». Вы пишете много одинаковых записей, поэтому они должны представлять массив в окончательном JSON.
  • Использует PipedOutputStream и PipedOutputStream чтобы связать SequenceWriter с InputStream который затем передается службе хранения. Если бы мы явно работали с файлами, в этом не было бы необходимости — достаточно просто передать FileOutputStream . Однако вы можете захотеть сохранить файл по-другому, например, в Amazon S3, и там для вызова putObject требуется InputStream, из которого можно прочитать данные и сохранить их в S3. Таким образом, в сущности, вы пишете в OutputStream, который напрямую записывается в InputStream, который, когда его запрашивают для чтения, получает все записанное в другой OutputStream
  • Сохранение файла вызывается в отдельном потоке, поэтому запись в файл не блокирует текущий поток, целью которого является чтение из базы данных. Опять же, это не будет необходимо, если используется простой FileOutputStream.
  • Весь метод помечается как @Async (spring), чтобы он не блокировал выполнение — он вызывается и завершается, когда готов (используя внутренний сервис-исполнитель Spring с ограниченным пулом потоков)
  • Код пакетного чтения базы данных здесь не показан, так как он варьируется в зависимости от базы данных. Суть в том, что вы должны получать данные в пакетном режиме, а не SELECT * FROM X.
  • OutputStream заключен в GZIPOutputStream, поскольку текстовые файлы, такие как JSON с повторяющимися элементами, значительно выигрывают от сжатия

Основная работа выполняется SequenceWriter Джексона, и (вроде бы очевидный) момент, который стоит взять домой, — не предполагайте, что ваши данные поместятся в памяти. Это почти никогда не происходит, так что делайте все партиями и добавочными записями.

Опубликовано на Java Code Geeks с разрешения Божидара Божанова, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Написание больших файлов JSON с Джексоном

Мнения, высказанные участниками Java Code Geeks, являются их собственными.