Маппинг джейсонов или еще чего в модели чаще всего головная боль. Много мелочей, модели сделай, все подгони, аннотации расставь и прочее. Далее код примерно наколеночный, кому надо идею, поймет. Маппинг еще и памяти ест очень много, так как обычно 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)
ris58h
01.09.2023 17:23+9Что с оформлением? Почти весь пост до ката. Код не отформатирован. Плюс хамская реакция на комментарий выше. Факир был пьян?
Kinski
01.09.2023 17:23+1Перечитал статью два раза. Но все равно не понял смысл хака. Какая задача здесь решается?
Правильно ли я понимаю, что NestedModelConsumer внутри себя делает что то с NestedModel, а потом забывает про неё (за счёт чего и экономим память)?
А если мне понадобится дальше в коде работать с массивом NestedModel, то решение уже не подходит?
ris58h
01.09.2023 17:23+2Я так понял что автор хотел обработать определённые значения вложенных объектов за линейное время и костанту по памяти. Но код плохо оформлен и приведена только его часть, а пояснение не блещет деталями, так что сложно сказать сработает ли такой подход. Особенно для не столь тривиальных JSON-ов.
breninsul
01.09.2023 17:23+2Выглядит как борьба с непонятным смыслом и целями.
JSON это не про компактное представление (это, обычно, не важно).
В любом случае крайне важно рухнуть с ошибкой сериализации если json не корректный/мы не умеем его десереализовать.
В статье происходит какой-то кошмар, бизнес-логика вшита в десереализатор.
Если у вас огромные массивы - есть json-stream, если огромный объект - json не очень подходит. В случае если апи не наше и делать нечего - можно строить костыли, но явно не вшивая бизнес-логику в десереализаторы. Что увидит сторонний человек, получивший ваш проект?! То, что у него null приходит как объект!!!
Если так уж надо - получайте input stream в роуте/контроллере да обрабатывайте в сервисах. Но скорее всего не надо
ris58h
01.09.2023 17:23То, что у него null приходит как объект!!!
Интересно ещё, как устроен внешний десериализатор, который массив парсит. Порождает ли он в памяти огромный массив из null?
BugM
Для обычных джейсонов ходящих через обычную апишку обычной десериализации Джексоном хватает с запасом. У вас же на балансире есть лимит на максимальный размер запроса и он разумный? Срочно ставьте, если нет.
Для гигабайтных файлов джейсон это не лучший выбор. Csv, например, лучше со всех сторон. Но бывает всякое. Там лучше использовать просто другие приемы. Не мучайте Джексон и используйте более подходящие решения.
3draven Автор
Это ты энтерпрайзным цмскам расскажи ;) Которыми крупняк типа интела пользуется, умник :) Америку открыл :)
BugM
Ну и возьмите более подходящее для вашей задачи решение. Задача нестандартная и решение нестандартное. Это нормально.
Заодно и производительности тут вероятно стоит задуматься и потестировать разное. Тормозить на гигабайтных файликах просто.
3draven Автор
Есть у меня подозрение, что я лучше понимаю о чем говорю ;) Забил.
auddu_k
Ну, а если серьезно - вот прилетает гигабайтный Джейсон, какое решение нестандартное решение? Понятно, что правильно уболтать поставщика данных, но пусть это невозможно, как быть? Какое нестандартное решение посоветуете?
BugM
Вводные: он прилетает не случайно в случайную апишку, а в специально предназначенное для этого место. Обычные апишки защищаем от такого заранее.
Принципы выбора: Нужен стриминг и быстрая обработка. Удобством жертвуем. Идеальным соблюдением стандартов тоже жертвуем, таких мест немного именно для них проверяем что ок. Таких методов все еще немного, писать сеньорами можно и на тестах не экономить.
Я бы взял https://jsoniter.com Там много всяких проблем, но для задачи парсить гигабайтные джесоны фиксированного формата он отлично подойдет.
3draven Автор
В статье про гигабайтные джейсоны нигде не сказано. Синьеров хак не требует. И устроен совсем просто. Смешно то, что ты сам предположил и сам себе ответил :)
Так то самое быстрое и экономное это написать грамматику, она простая и сгенерить парсер, да и все. Но это другие предположения и условия, а в статье просто описан хак, без условий. Но нельзя было пройти мимо, надо было самовыразится :) :) :) Причем упорно развивать идею :)
BugM
Я как обычно про продакшен. Где 100500 апишек. Пишут разные люди и по разному. Максимальный разумный размер входного джейсона килобайты максимум. Ну пусть даже мегабайт.
В таких условиях хаков не должно быть вообще. Надёжность важнее всего. Скорость не принципиальна в разумных пределах (стандартный Джексон дает разумную скорость), программирование нужно максимально декларативное чтобы ошибаться негде вообще было, потоковая обработка не нужна ибо килобайты на входе. И зачем?
А особые случаи это всегда особые случаи. Их пишем отдельно по другому. Там допустимо всякое. Можно упороться и в оптимизацию и в особый код. И вероятно это даже принесет пользу. Гигабайт переварить побыстрее часто есть смысл
3draven Автор
Хак декларативный и простой. Ты о чем вообще?
3draven Автор
Хотя ладно, побеждать кого-то в интернете не в моих планах :) Дружно забьем.
ris58h
Без ТЗ - результат ХЗ. Сделать то с ним что нужно? Без абстрактного "распарсить". Какие данные в нём? Какие из них нам нужны?
auddu_k
Да вот, хоть как в Примере статьи, чем плохо?
ris58h
Задача из статьи прекрасно решается с помощью Streaming API. Решение тоже есть в самом начале статьи и не надо городить никакие поддельные десериализаторы.
Djaler
чёт слишком агрессивная реакция на комментарий к статье
3draven Автор
Никакой агрессии, я просто поржал. Бывает меня веселят коменты. Имею право :)
alex-khv
Походите мимо. Это очередной 20 летний перекладыватель json в crud ручках, который считает что он слишком гениален чтобы нормально отвечать незнакомым людям. С возрастом это проходит рано или поздно.