Статьи

Потоковая передача большого файла JSON с Джексоном — RxJava FAQ

В предыдущей статье мы узнали, как анализировать чрезмерно большие XML-файлы и превращать их в потоки RxJava. На этот раз давайте посмотрим на большой файл JSON. Мы будем основывать наши примеры на крошечных цветах. Json, содержащих почти 150 записей такого формата:

1
2
3
4
5
6
7
{
  "aliceblue": [240, 248, 255, 1],
  "antiquewhite": [250, 235, 215, 1],
  "aqua": [0, 255, 255, 1],
  "aquamarine": [127, 255, 212, 1],
  "azure": [240, 255, 255, 1],
  //...

Малоизвестный факт: лазурь — это тоже цвет, а питон — змея. Но вернемся к RxJava. Этот файл крошечный, но мы будем использовать его для изучения некоторых принципов. Если вы будете следовать им, вы сможете загружать и непрерывно обрабатывать произвольно большие, даже бесконечно длинные файлы JSON. Во-первых, стандартный способ « Джексона » похож на JAXB: загрузка всего файла в память и сопоставление его с Java-бинами. Однако, если ваш файл в мегабайтах или гигабайтах (потому что каким-то образом вы нашли JSON лучшим форматом для хранения гигабайтов данных …), этот метод не сработает. К счастью, Джексон обеспечивает потоковый режим, похожий на StAX.

Загрузка файлов JSON токен-токен с использованием Jackson

Нет ничего плохого в стандартном ObjectMapper который принимает JSON и превращает его в коллекцию объектов. Но чтобы избежать загрузки всего в память, мы должны использовать низкоуровневый API, используемый ObjectMapper внизу. Давайте снова посмотрим на пример JSON:

1
2
3
4
{
  "aliceblue": [240, 248, 255, 1],
  "antiquewhite": [250, 235, 215, 1],
  //...

С точки зрения диска и памяти, это одномерный поток байтов, который мы можем логически объединить в токены JSON:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
START_OBJECT        '{'
FIELD_NAME          'aliceblue'
START_ARRAY         '['
VALUE_NUMBER_INT    '240'
VALUE_NUMBER_INT    '248'
VALUE_NUMBER_INT    '255'
VALUE_NUMBER_INT    '1'
END_ARRAY           ']'
FIELD_NAME          'antiquewhite'
START_ARRAY         '['
VALUE_NUMBER_INT    '250'
VALUE_NUMBER_INT    '235'
VALUE_NUMBER_INT    '215'
VALUE_NUMBER_INT    '1'
END_ARRAY           ']'
...

Вы поняли идею. Если вы знакомы с теорией компилятора, это один из первых шагов во время компиляции. Компилятор преобразует исходный код из символов в токены.
Но, если вы знаете теорию компилятора, вы, вероятно, не разбираете JSON на жизнь. Так или иначе! Библиотека Джексона работает таким образом, и мы можем использовать ее без прозрачного отображения объектов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
  
JsonParser parser = new JsonFactory().createParser(new File("colors.json"));
parser.nextToken(); // JsonToken.START_OBJECT;
while (parser.nextToken() != JsonToken.END_OBJECT) {
    final String name = parser.getCurrentName();
    parser.nextToken(); // JsonToken.START_ARRAY;
    parser.nextValue();
    final int red = parser.getIntValue();
    parser.nextValue();
    final int green = parser.getIntValue();
    parser.nextValue();
    final int blue = parser.getIntValue();
    parser.nextValue();
    parser.getIntValue();
    System.out.println(name + ": " + red + ", " + green + ", " + blue);
    parser.nextToken(); // JsonToken.END_ARRAY;
}
parser.close();

… или если вы избавитесь от некоторого дублирования и сделаете код немного легче для чтения:

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
import lombok.Value;
  
  
JsonParser parser = new JsonFactory().createParser(new File("colors.json"));
parser.nextToken(); // JsonToken.START_OBJECT;
while (parser.nextToken() != JsonToken.END_OBJECT) {
    System.out.println(readColour(parser));
}
parser.close();
  
//...
  
private Colour readColour(JsonParser parser) throws IOException {
    final String name = parser.getCurrentName();
    parser.nextToken(); // JsonToken.START_ARRAY;
    final Colour colour = new Colour(
            name,
            readInt(parser),
            readInt(parser),
            readInt(parser),
            readInt(parser)
    );
    parser.nextToken(); // JsonToken.END_ARRAY;
    return colour;
}
  
private int readInt(JsonParser parser) throws IOException {
    parser.nextValue();
    return parser.getIntValue();
}
  
@Value
class Colour {
    private final String name;
    private final int red;
    private final int green;
    private final int blue;
    private final int alpha;
}

Какое это имеет отношение к RxJava? Вы, вероятно, можете догадаться — мы можем читать этот файл JSON по запросу, по частям. Это позволяет механизму противодавления работать бесперебойно:

1
2
3
4
final Flowable colours = Flowable.generate(
        () -> parser(new File("colors.json")),
        this::pullOrComplete,
        JsonParser::close);

Позвольте мне объяснить, что делают эти три лямбда-выражения. Первый устанавливает JsonParser — наше изменяемое состояние, которое будет использоваться для производства (получения) большего количества элементов:

1
2
3
4
5
private JsonParser parser(File file) throws IOException {
    final JsonParser parser = new JsonFactory().createParser(file);
    parser.nextToken(); // JsonToken.START_OBJECT;
    return parser;
}

Ничего фантастического. Второе лямбда-выражение имеет решающее значение. Он вызывается каждый раз, когда подписчик желает получить больше товаров. Если он запрашивает 100 элементов, это лямбда-выражение будет вызываться 100 раз:

1
2
3
4
5
6
7
8
private void pullOrComplete(JsonParser parser, Emitter<Colour> emitter) throws IOException {
    if (parser.nextToken() != JsonToken.END_OBJECT) {
        final Colour colour = readColour(parser);
        emitter.onNext(colour);
    } else {
        emitter.onComplete();
    }
}

Конечно, если мы достигаем END_OBJECT (закрытие всего файла JSON), мы сигнализируем, что поток закончен. Последнее лямбда-выражение просто позволяет очистить состояние, например, закрыв JsonParser и лежащий в его основе File . Теперь представьте, что размер этого файла JSON составляет сотни гигабайт. Имея Flowable<Colour> мы можем безопасно использовать его с произвольной скоростью, не рискуя перегрузкой памяти.