Статьи

Разбор JSON в стиле PHP в Java с помощью Jsoniter

JSON возник из слабо типизированного и динамического языка Javascript. Существует несоответствие импеданса между динамической природой JSON и жесткой типизацией Java. Я обнаружил, что существующие решения слишком сфокусированы на концепции привязки данных, которая в некоторых обстоятельствах является слишком тяжелой. Сравните это с PHP, где у PHP есть Array данных типа «все в одном», и только одной строкой json_decode мы можем проанализировать сложный документ JSON. Jsoniter — это новая библиотека, написанная на Java, решившая сделать синтаксический анализ JSON на Java таким же простым, как и в PHP, с помощью аналогичного типа данных: Any . Самая замечательная особенность — это базовый метод ленивого анализа, который делает анализ не только легким, но и очень быстрым.

Почему JSON сложно обрабатывать в Java

Есть три причины, по которым документы JSON сложно обрабатывать с использованием существующих анализаторов. Я называю это «несоответствием импеданса JSON».

Причина 1: несоответствие типов

Когда JSON используется в качестве формата обмена данными между Java и динамическими языками, такими как PHP, типы полей объекта могут стать проблемой. Например, посмотрите на этот JSON:

 { "order_id": 100098, "order_details": {"pay_type": "cash"} } 

В 99% случаев код PHP может возвращать точную структуру, которую мы ожидаем. Но он также может вернуть немного другой JSON для разных условий ввода, потому что большинству разработчиков PHP не важно, является ли переменная строковой или int.

 { "order_id": "100098", "order_details": [] } 

Почему order_details пустой массив вместо пустого объекта? Это распространенная проблема при работе с PHP, где все является массивом. Массив, используемый в качестве непустой карты, будет закодирован как {"key":"value"} но пустая карта — это просто пустой массив, который будет закодирован как [] вместо {} .

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

Причина 2: гетерогенные данные

В Jave мы привыкли к однородным данным. Например, [1, 2, 3] является массивом int , ["1", "2", "3"] массивом String . Но как вы представляете [1, 2, "3"] в Java? С массивом Object[] работать неудобно. Как насчет [1, ["2", 3]] ? В Java нет удобного контейнера для хранения данных такого типа.

Более того, в JSON очень часто встречаются слегка отличающиеся структуры, представляющие одно и то же. Например, успешный ответ:

 { "code": 0, "data": "Success" } 

Но для ответа об ошибке:

 { "code": -1, "error": {"msg": "Wrong Parameter", "stacktrace": "…"} } 

Если мы хотим получить данные или сообщение об ошибке, мы должны сделать несколько нулевых проверок. Предполагая, что ответ представлен как Map<String, Object> , код для извлечения сообщения об ошибке будет выглядеть следующим образом:

 Object errorObj = response.get("error"); if (errorObj == null) return "N/A"; Map<String, Object> error = (Map<String, Object>)errorObj; Object msgObj = errorObj.get("msg"); if (msgObj == null) return "N/A"; return (String)msgObj; 

Приведение типов и проверка на нуль совсем неинтересны. К сожалению, распространено извлечение значения из JSON глубиной в пять уровней!

Причина 3: баланс производительности и гибкости

Переходя на JSON, мы уже выбрали гибкость вместо чистой производительности. Однако все еще плохо анализировать документ JSON как Map<String, Object> , зная, что это будет очень дорого. Я не утверждаю, что мы должны выбирать производительность, а не выразительность. Но вина за умышленное компромиссное исполнение постоянно беспокоит меня. Это дилемма, с которой я часто сталкиваюсь:

  • Разобрать JSON как Map<String, Object> и прочитать значения из него. Избавляет от необходимости определения класса схемы, но мы должны удалить все байты, независимо от того, нужны они нам или нет.
  • Определите класс и используйте привязку данных. Он может пропустить ненужную работу разбора, и доступ к объекту быстрее, чем к хеш-карте. Но стоит ли беспокоиться каждый раз?
  • Некоторые парсеры JSON поставляются с потоковым API, но это считается слишком низким уровнем.

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

Разбор JSON в Java как в PHP с Jsoniter

Как Jsoniter решает проблему несоответствия JSON

Jsoniter — это новая библиотека JSON для Java, разработанная с учетом вышеуказанных проблем. (Отказ от ответственности: я являюсь его автором.) Jsoniter реагирует на несоответствие импеданса JSON следующими методами:

  • Привязка данных поддерживает «нечеткую» типизацию с помощью предопределенных декодеров, таких как MaybeStringLongDecoder .
  • Тип данных Any представляет объект JSON, аналогично тому, как это делает массив PHP.
  • Ленивый анализ обрабатывает только запрошенные поля и оставляет другие байты без изменений.

Чтобы продемонстрировать, как использовать Jsoniter, давайте сначала установим его. Добавьте следующую зависимость в ваш pom.xml (при условии, что вы используете Maven):

 <dependency> <groupId>com.jsoniter</groupId> <artifactId>jsoniter</artifactId> <version>0.9.8</version> </dependency> 

Или вы можете скачать банку напрямую .

Jsoniter — гибкий парсер, с 3 API, которые вы можете выбрать

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

Привязка данных

Jsoniter не заставляет вас использовать тип Any . Во многих случаях привязка данных по-прежнему является наиболее удобным API.

Простые Классы

Давайте свяжем этот простой пример:

 { "order_id": 100098, "order_details": {"pay_type": "cash"} } 

Для этого мы разработаем такой класс:

 public class Order { public long order_id; public OrderDetails order_details; } public class OrderDetails { public String pay_type; } 

Для десериализации ввода JSON мы будем использовать JsonIterator :

 Order order = JsonIterator.deserialize(input, Order.class); 

Ввод может быть String или byte[] . Если вам нужно использовать InputStream качестве входных данных, он будет немного более подробным:

 JsonIterator iter = JsonIterator.parse(input); Order order = Iter.read(Order.class); // you can close the underlying InputStream via Iter // or directly (it does not have its own resource to dispose) Iter.close(); 

Кейс для аннотаций

Все знают, как работает простое связывание. Но как насчет грязного ввода?

 { "order_id": "100098", "order_details": [] } 

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

 JsoniterAnnotationSupport.enable(); 

Это нужно сделать только один раз, вы можете поместить его в основную функцию или статический инициализатор. Теперь добавьте аннотации к классу Order

 public class Order { @JsonProperty(decoder = MaybeStringLongDecoder.class) public long order_id; @JsonProperty(decoder = MaybeEmptyArrayDecoder.class) public OrderDetails order_details; } 

Используя декодер Maybe, мы можем сделать привязку нечеткой в ​​отношении типов данных в некоторых случаях. Если сама структура является «динамической», то лучше использовать Any .

Any тип данных

Вместо определения класса, описывающего схему данных, мы можем использовать тип данных Any . Это в значительной степени замена Map<String, Object> или List<Object> . Давайте прочитаем тот же JSON, что и раньше:

 { "order_id": 100098, "order_details": {"pay_type": "cash"} } 

Это код для этого:

 Any order = JsonIterator.read(input); String payType = order.toString("order_details", "pay_type"); 

Метод toString может выглядеть странно, поэтому позвольте мне объяснить:

  • Получить "order_details"
  • Затем получите "pay_type" из "order_details"
  • Затем преобразуйте значение "pay_type" из любого типа в строку

Даже в следующем случае код все еще работает, потому что он преобразует 5 в "5" :

 { "order_details": {"pay_type": 5} } 

Что делать, если ввод не то, что мы ожидаем, например:

 { "order_details": [] } 

Код toString("order_tails", "pay_type") не будет генерировать toString("order_tails", "pay_type") NullPointerException , а будет возвращать пустую строку. Большую часть времени мы ожидаем пустую строку.

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

Итератор API

API итератора представляет поток данных JSON в качестве итератора. Вы можете использовать следующие методы для управления процессом итерации:

  • whatIsNext : посмотрите на тип следующего значения. Он возвращает экземпляр перечисления ValueType , к которому я вернусь позже. Использование этого метода не является обязательным — если вы знаете, что следующее значение должно быть, например, строкой, вы можете напрямую вызвать readString без предварительной проверки whatIsNext .
  • readObject : Читать следующее поле объекта, возвращая имя поля.
  • readArray : прочитать следующий элемент массива и вернуть false, если достигнут конец массива.
  • readString : чтение отдельного значения в виде строки.

Давайте использовать этот пример ввода:

 {"numbers": ["1", "2", ["3", "4"]]} 

Я написал тест JUnit для демонстрации API итератора:

 JsonIterator iter = JsonIterator.parse( "{'numbers': ['1', '2', ['3', '4']]}" .replace('\'', '"')); // start reading the first object ("number") assertEquals("numbers", iter.readObject()); // start reading the array assertTrue(iter.readArray()); assertEquals("1", iter.readString()); assertTrue(iter.readArray()); assertEquals("2", iter.readString()); // start reading the inner array assertTrue(iter.readArray()); // you can know the type of next value before reading it assertEquals(ValueType.ARRAY, iter.whatIsNext()); assertTrue(iter.readArray()); assertEquals(ValueType.STRING, iter.whatIsNext()); assertEquals("3", iter.readString()); assertTrue(iter.readArray()); assertEquals("4", iter.readString()); // end inner array assertFalse(iter.readArray()); // end outer array assertFalse(iter.readArray()); // end object "number" assertNull(iter.readObject()); 

На самом деле это то, что предлагает его имя, итератор: вы вызываете метод, и он движется вперед.

Веселье с Any

Any веселье, давайте больше.

Любой контейнер

Any — это контейнер, который может содержать все виды значений:

  • ленивый объект
  • ленивый массив
  • ленивый шнур
  • ленивый двойной
  • ленивый
  • не ленивое значение (массив, объект, строка, число с плавающей запятой, double, long, int, true, false, null)

Если содержащееся значение является объектом или массивом, мы можем извлечь элементы без преобразования в List или Map .

Например_

 [{"score":100}, {"score":102}] 

Мы можем извлечь значение, используя только путь:

 // will be 100 JsonIterator.deserialize(input).toInt(0, "score") 

Первый аргумент 0 получает первый элемент из массива. Вторым аргументом "score" получают оценку из объекта.

Или мы можем перебрать значение как коллекцию:

 Any records = JsonIterator.deserialize(input); for (Any record : records) { Any.EntryIterator entryIterator = record.entries(); while (entryIterator.next()) { System.out.println( entryIterator.key() + " / " + entryIterator.value()); } } // output is: // score / 100 // score / 102 

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

Мы даже можем использовать подстановочные знаки в пути извлечения:

 Any records = JsonIterator.deserialize(input); // [100, 102] records.get('*', "score") 

Это извлечет Any со значением списка, содержащим счет каждой записи.

Недостающее значение

Давайте еще раз посетим предыдущий пример

 { "order_details": [] } 

Как я объяснил ранее, toString("order_tails", "pay_type") вернет пустую строку. Вот как toString обрабатывает пропущенное значение. Если мы изменим на get("order_details", "pay_type") , это может сказать нам, что значение фактически отсутствует:

 Any payType = order.get("order_details", "pay_type"); if (payType.valueType() == ValueType.INVALID) { // not found } 

Если вы попытаетесь использовать недействительный экземпляр Any , будет сгенерировано исключение. В этом случае Any очень похож на Optional в Java 8 . Возможные типы значений:

  • INVALID
  • STRING
  • NUMBER
  • NULL
  • BOOLEAN
  • ARRAY
  • OBJECT

Мы видим, что даже «нулевой» JSON на самом деле не является null в смысле Java. Он будет представлен как экземпляр Any с valueType() == ValueType.NULL . Удаление нуля из возможных возвращаемых значений делает извлечение значений из глубоко вложенной структуры намного более удобным, так как проверка нуля на всем протяжении больше не нужна.

Извлечение пути из подстановочных знаков также поддерживает пропущенное значение

 // input is [{"score":100}, {"value":102}] Any records = JsonIterator.deserialize(input); // [100] records.get('*', "score") 

Поскольку «оценка» не найдена во второй записи, она будет исключена из результата.

Преобразование типов

Метод toString является лишь одним из поддерживаемых преобразований, другие:

  • toInt
  • toLong
  • toDouble
  • toFloat
  • toBoolean

Каждое преобразование будет делать все возможное, чтобы преобразовать исходное значение в нужный вам тип.

Помимо простых типов, вы можете преобразовать значение в сложный тип с помощью привязки данных. Например, мы можем извлечь значение, используя Any , затем связать в объект.

 // {"numbers": ["1", "2", ["3", "4"]]} String[] numbers = JsonIterator .deserialize(input) .get("numbers", 2) .as(String[].class); 

API as использует привязку данных, как объяснено ранее, для привязки ["3", "4"] к объекту String[] .

Частичная обработка JSON без схемы

Any также изменчив и может быть сериализован обратно в JSON. Если вы хотите только немного изменить исходный ввод и затем записать его обратно, Any будет очень кстати. Он будет захватывать входные данные в виде необработанных байтов и записывать обратно в JSON как есть, что экономит не только стоимость десериализации, но и стоимость сериализации. Основная оптимизация происходит автоматически без вашего участия — вы пишете код так же, как если бы вы работали с Map<String, Object> или List<Object> :

 List numbers = JsonIterator.deserialize("[1,[2, 3],4]").asList(); numbers.add(5); // will be [1,[2, 3],4,5] JsonStream.serialize(numbers); 

Это частичная обработка — трудно заметить, где магия.

  • Когда asList , список будет содержать 3 Any элемента, представляющих 1 , [2, 3] и 4 в исходном байтовом массиве, которые не анализируются.
  • Когда добавляется 5 , первые 3 элемента списка остаются типа Any а 4-й — типа java.lang.Integer .
  • Когда мы сериализуем список обратно в форму JSON, первые 3 элемента не будут иметь затрат на сериализацию, поскольку они все еще находятся в форме байтового массива и будут напрямую скопированы в байтах. Только 4-й элемент будет преобразован из целого числа в строку.

Этот метод дает совершенно новый способ обработки JSON. Традиционно мы пишем нашу логику в следующем виде:

  • JSON => Граф объекта => Граф модифицированного объекта => JSON

С помощью Any мы можем сохранить множество объектов:

  • JSON => Граф отложенных объектов => Частично проанализированный и измененный граф объектов => JSON

Резюме

JSON — это гибкий формат, и вывод, создаваемый кодом, написанным на таких языках, как PHP, часто трудно обрабатывать в Java. В отличие от большинства существующих синтаксических анализаторов, Jsoniter предпочитает использовать динамическую природу, а не скрывать ее. Инновационный тип данных Any упрощает синтаксический анализ JSON с неопределенными типами и неопределенной структурой в Java. С ленивым анализом этот анализ стиля без схемы еще более привлекателен. Будучи чрезвычайно гибким, производительность не ставится под угрозу.