Десериализация JSON в GraalVM Native Image

Одной из главных профессиональных целей, которые я ставил перед собой на этот уже уходящий 2022 год, было изучение перспективной GraalVM, в особенности сборки нативных образов (Native Image), а так же внедрение этого всего в свои проекты. В общих чертах я уже был знаком с GraalVM, и даже экспериментировал со сборкой нативных образов из проектов, основанных на Spring Boot, но ждал релиза Spring Boot 3. И, дождавшись его, начал переводить существующие проекты на новую версию стека Spring и заодно пробовать собирать нативные образы.

Разработчики GraalVM и Spring проделали большую работу с момента первого публичного релиза GraalVM, и собрать исполняемый нативный образ из проекта на Spring Boot в настоящее время значительно проще, чем это было раньше, но проблемы всё же встречаются. С одной из таких проблем — десериализацией JSON при помощи Jackson я и столкнулся.

Самый популярный способ разработки REST-сервисов на Spring — с использованием аннотированных контроллеров. И в этом случае проблем с конвертацией тела запроса в объект нужного типа не возникает, так как передаётся он через аргументы метода аннотированного контроллера, и этого достаточно для AoT-анализа.

Однако я в своих проектах для реализации REST-сервисов уже достаточно давно использую функциональные обработчики HTTP-запросов (см. HandlerFunction). И в этом случае AoT-анализатор не знает, что у экземпляра ServerRequest при помощи метода .body() будет запрашиваться тело запроса. Поэтому в процессе тестирования впервые собранного нативного образа я столкнулся с тем, что Jackson ObjectMapper не может определить, каким образом он должен создать экземпляр требуемого класса.

Пример кода, который нужно заставить работать:

JsonDeserializer

Первый вариант, пришедший мне в голову — написать реализацию JsonDeserializer для класса, в который требуется преобразовать JSON-строку. И этот вариант ожидаемо оказался рабочим, так как не обращается к API рефлексии, но с одним «но» — десериализаторы нужно написать для всех требуемых классов, а в том проекте это порядка тридцати классов. По-этому от JsonDeserializer в данном случае я сознательно отказался, хотя отказ от использования API рефлексии с пользу JsonDeserializer на мой взгляд логичен, хоть и добавляет кода.

-H:ReflectionConfigurationFiles

Второй вариант появился из документации GraalVM по использованию рефлексии в нативных образах. В данном способе для каждого класса нужно описать методы и свойства, к которым нужен рефлективный доступ. Итоговый объём кода должен быть ощутимо меньше, чем в случае с десериализаторами, но всё равно его достаточно много, плюс к этому нужно знать формат файла конфигурации рефлексии.

@RegisterReflectionForBinding

И вот читая обновлённую документацию по Spring Framework я наткнулся на раздел AoT-оптимизаций, где среди прочего написано про аннотацию @RegisterReflectionForBinding, при помощи которой можно подсказать AoT-анализатору, для каких классов нужно добавлять описание рефлективного доступа к методам и свойствам.

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

Я стараюсь не использовать рефлексию в своих проектах, хотя понимаю, что сторонние решения, среди которых есть Spring, Hibernate, Jackson и многие другие фреймворки и библиотеки, её используют, и полностью отказаться от использования API рефлексии, скорее всего, не получится в ближайшем будущем. Возможно, в будущих новых проектах я буду сразу внедрять использование JsonDeserializer.