В предыдущей статье мы узнали, как анализировать чрезмерно большие 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>
мы можем безопасно использовать его с произвольной скоростью, не рискуя перегрузкой памяти.
Ссылка: | Потоковая передача большого файла JSON с часто задаваемыми вопросами по Jackson — RxJava от нашего партнера по JCG Томаша Нуркевича в блоге Java и соседних блогах. |