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

mapper.readValue(inputStream,Model.class)

В итоге если модель большая маппер ее всю в памяти построит за раз, прочитав опять же весь json из стрима. Хуже когда даже json сначала в строку читают конечно. Потом приходит очередной ругатель и заявляет, что это java виновата. Что бы этого не делать, придумали ObjectMapper Streaming API. Что то вроде такого:

while (jParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jParser.getCurrentName();
if ("name".equals(fieldname)) {
jParser.nextToken();
parsedName = jParser.getText();
}

Но фактически руками парсить json это тоже головняк. Есть хак, который позволяет и модели сразу получать и стриминг использовать. Может кому пригодится. Предположим у нас есть json, который содержит в себе массив объектов:

{result:[{"name":"test"}]}

Делаем две модели. Первая это общий объект:

public class Model {
private Set<NestedModel> result;
}

Вторая это вложенный объект:

public class NestedModel {
private String name;
}

Далее делаем десериализатор, который десериализует модель класса NestedModel. При этом данный десериализатор должен в конструкторе принимать обработчик моделей NestedModel и возвращать null вместо результата. То есть он обработчиком модель обработает и вернет пустоту. В итоге ObjectMapper вернет Model с одним null элементом, который нам и не нужен, так как в процессе десериализации всех NestedModel мы их уже и так все обработали. В памяти при этом в момент времени хранится всего одна NestedModel и писать ручного кода не нужно вовсе. Десериализатор:

public class NestedModelDeserializer extends StdDeserializer<NestedModel> {
private final Consumer<NestedModel> nestedModelConsumer;
private final ObjectMapper innerMapper;
protected NestedModelDeserializer(Class<NestedModel> vc, Consumer<NestedModel> nestedModelConsumer) {
super(vc);
this.nestedModelConsumer = nestedModelConsumer;
this.innerMapper = new ObjectMapper();
}
@Override public NestedModel deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
nestedModelConsumer.accept(innerMapper.readValue(p,NestedModel.class));
return null;
}
}

Второй ObjectMapper нужен потому, что если отдать десериализацию NestedModel первому, он в рекурсию попадет бесконечную. В итоге ручного парсинга джейсона не надо, десериализатор пишется очень просто, и памяти ест мало, по дороге можно как угодно обработать данные. Десериализатор конечно надо зарегистрировать в первом ObjectMapper. Это экономит много сил и упрощает код, так как ручной парсинг дело неудобное и там где родной Streaming API вынуждает тебя спуститься на уровень токенов данный подход позволяет остаться на уровне моделей и не задумываться как он там разберет json.

Комментарии (22)


  1. BugM
    01.09.2023 17:23
    +2

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

    Для гигабайтных файлов джейсон это не лучший выбор. Csv, например, лучше со всех сторон. Но бывает всякое. Там лучше использовать просто другие приемы. Не мучайте Джексон и используйте более подходящие решения.


    1. 3draven Автор
      01.09.2023 17:23
      -10

      Это ты энтерпрайзным цмскам расскажи ;) Которыми крупняк типа интела пользуется, умник :) Америку открыл :)


      1. BugM
        01.09.2023 17:23

        Ну и возьмите более подходящее для вашей задачи решение. Задача нестандартная и решение нестандартное. Это нормально.

        Заодно и производительности тут вероятно стоит задуматься и потестировать разное. Тормозить на гигабайтных файликах просто.


        1. 3draven Автор
          01.09.2023 17:23
          -5

          Есть у меня подозрение, что я лучше понимаю о чем говорю ;) Забил.


        1. auddu_k
          01.09.2023 17:23

          Ну, а если серьезно - вот прилетает гигабайтный Джейсон, какое решение нестандартное решение? Понятно, что правильно уболтать поставщика данных, но пусть это невозможно, как быть? Какое нестандартное решение посоветуете?


          1. BugM
            01.09.2023 17:23
            +1

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

            Принципы выбора: Нужен стриминг и быстрая обработка. Удобством жертвуем. Идеальным соблюдением стандартов тоже жертвуем, таких мест немного именно для них проверяем что ок. Таких методов все еще немного, писать сеньорами можно и на тестах не экономить.

            Я бы взял https://jsoniter.com Там много всяких проблем, но для задачи парсить гигабайтные джесоны фиксированного формата он отлично подойдет.


            1. 3draven Автор
              01.09.2023 17:23
              -4

              В статье про гигабайтные джейсоны нигде не сказано. Синьеров хак не требует. И устроен совсем просто. Смешно то, что ты сам предположил и сам себе ответил :)

              Так то самое быстрое и экономное это написать грамматику, она простая и сгенерить парсер, да и все. Но это другие предположения и условия, а в статье просто описан хак, без условий. Но нельзя было пройти мимо, надо было самовыразится :) :) :) Причем упорно развивать идею :)


              1. BugM
                01.09.2023 17:23
                +2

                Я как обычно про продакшен. Где 100500 апишек. Пишут разные люди и по разному. Максимальный разумный размер входного джейсона килобайты максимум. Ну пусть даже мегабайт.

                В таких условиях хаков не должно быть вообще. Надёжность важнее всего. Скорость не принципиальна в разумных пределах (стандартный Джексон дает разумную скорость), программирование нужно максимально декларативное чтобы ошибаться негде вообще было, потоковая обработка не нужна ибо килобайты на входе. И зачем?

                А особые случаи это всегда особые случаи. Их пишем отдельно по другому. Там допустимо всякое. Можно упороться и в оптимизацию и в особый код. И вероятно это даже принесет пользу. Гигабайт переварить побыстрее часто есть смысл


                1. 3draven Автор
                  01.09.2023 17:23
                  -4

                  Хак декларативный и простой. Ты о чем вообще?


                  1. 3draven Автор
                    01.09.2023 17:23
                    -3

                    Хотя ладно, побеждать кого-то в интернете не в моих планах :) Дружно забьем.


          1. ris58h
            01.09.2023 17:23

            Ну, а если серьезно - вот прилетает гигабайтный Джейсон

            Без ТЗ - результат ХЗ. Сделать то с ним что нужно? Без абстрактного "распарсить". Какие данные в нём? Какие из них нам нужны?


            1. auddu_k
              01.09.2023 17:23

              Да вот, хоть как в Примере статьи, чем плохо?


              1. ris58h
                01.09.2023 17:23

                Задача из статьи прекрасно решается с помощью Streaming API. Решение тоже есть в самом начале статьи и не надо городить никакие поддельные десериализаторы.


      1. Djaler
        01.09.2023 17:23
        +5

        чёт слишком агрессивная реакция на комментарий к статье


        1. 3draven Автор
          01.09.2023 17:23
          -8

          Никакой агрессии, я просто поржал. Бывает меня веселят коменты. Имею право :)


        1. alex-khv
          01.09.2023 17:23

          Походите мимо. Это очередной 20 летний перекладыватель json в crud ручках, который считает что он слишком гениален чтобы нормально отвечать незнакомым людям. С возрастом это проходит рано или поздно.


  1. ris58h
    01.09.2023 17:23
    +9

    Что с оформлением? Почти весь пост до ката. Код не отформатирован. Плюс хамская реакция на комментарий выше. Факир был пьян?


  1. Kinski
    01.09.2023 17:23
    +1

    Перечитал статью два раза. Но все равно не понял смысл хака. Какая задача здесь решается?

    Правильно ли я понимаю, что NestedModelConsumer внутри себя делает что то с NestedModel, а потом забывает про неё (за счёт чего и экономим память)?

    А если мне понадобится дальше в коде работать с массивом NestedModel, то решение уже не подходит?


    1. ris58h
      01.09.2023 17:23
      +2

      Я так понял что автор хотел обработать определённые значения вложенных объектов за линейное время и костанту по памяти. Но код плохо оформлен и приведена только его часть, а пояснение не блещет деталями, так что сложно сказать сработает ли такой подход. Особенно для не столь тривиальных JSON-ов.


    1. breninsul
      01.09.2023 17:23

      видимо, коллбэком из десереализатора получать это чудо!

      Какой-то кошмар


  1. breninsul
    01.09.2023 17:23
    +2

    Выглядит как борьба с непонятным смыслом и целями.

    JSON это не про компактное представление (это, обычно, не важно).

    В любом случае крайне важно рухнуть с ошибкой сериализации если json не корректный/мы не умеем его десереализовать.

    В статье происходит какой-то кошмар, бизнес-логика вшита в десереализатор.

    Если у вас огромные массивы - есть json-stream, если огромный объект - json не очень подходит. В случае если апи не наше и делать нечего - можно строить костыли, но явно не вшивая бизнес-логику в десереализаторы. Что увидит сторонний человек, получивший ваш проект?! То, что у него null приходит как объект!!!

    Если так уж надо - получайте input stream в роуте/контроллере да обрабатывайте в сервисах. Но скорее всего не надо


    1. ris58h
      01.09.2023 17:23

      То, что у него null приходит как объект!!!

      Интересно ещё, как устроен внешний десериализатор, который массив парсит. Порождает ли он в памяти огромный массив из null?