Особенности сборки native приложений в Quarkus через GraalVM
Столкнулся с некоторыми проблемами:
Ошибки типа Discovered unresolved type during parsing… и Discovered unresolved method during parsing…
Имеют свойство появляться в случайном порядке, если в приложении существует несколько мест вызывающих данную ошибку. Сначала пытался лечить путем добавления аргумента сборки
quarkus.native.additional-build-args=--initialize-at-run-time.....
но это оказалось бесполезно, т.к. при сборке, грааль сканирует весь код и находит места вызовов несуществующих классов/методов, и если найдёт, то прекращает сборку и вываливает ошибку.
На эту тему есть небольшое замечание тут.
Как скипнуть эту проблему или игнорировать такие вещи — не нашёл. Разработчики пишут, что они заботятся о нас, чтобы наша приложуха не дай бог не выкинула ошибку типа ClassNotFoundException.
Причина появления этой ошибки — Внутри одной из зависимостей/транзитивных зависимостей есть код, который:
- импортирует классы из неподключенных зависимостей
- использует версию зависимости, в которой нет вызываемого класса/метода
При сборке приложения в jar проблем не возникает, т.к. сборщик не копает внутрь работы зависимостей и не смотрит как там взаимодействуют между собой транзитивные зависимости, а просто собирает проект и всё.
При запуске приложения из jar, классы инициализируются при обращении к ним, и если бы было обращение, то вылетела бы ошибка типа ClassNotFoundException/MethodNotFoundException или тому подобные.
Конечно, следует добиваться максимальной совместимости всех используемых зависимостей, но не редки случаи, когда это сделать по каким-то причинам невозможно. Также велика вероятность несовместимости каких-то глубоких транзитивных зависимостей, код которых даже никогда не будет использоваться вашей бизнес логикой. Для исправления такой ситуации я нашёл единственное костыльное решение:
— написать свою реализацию того класса из внешней библиотеки, которая дёргает несуществующий метод, положить его в какую-то отдельную зависимость и подключить её до подключения проблемной зависимости, либо оставить лежать прям в проекте по адресу оригинального пакета. Тогда класслоадер увидит что такой класс уже есть, и не станет грузить оригинальный., и наверно проблемы не будет
При обмене сообщениями через JMS со сторонними сервисами, возникает ошибка десериализации содержимого сообщений, если в сообщениях передаются java-объекты сериализованные java-сериализацией. Причём ему абсолютно безразлично, что на объекты ставишь статический serialVersionUID, при сериализации он каким-то чудесным образом его игнорирует и вычисляет свой. Да, здесь есть проблема, что значение данного поля извлекается при помощи рефлексии, которая тоже по-умолчанию не работает и её необходимо разрешать, но всё-равно перезапись значения serialVersionUID происходит независимо от этого.
Единственным решением проблемы явился переход с java-сериализацию на json-сериализацию.
Разрешение рефлексии.
В основном, проблема возникает при попытке сериализовать/десериализовать классы в json при помощи jackson. Для его работы нужно разрешить использование рейфлексии для классов, которые предполагается использовать для отображения json в объекты. В простом приложении, в достаточно установить над ними аннотацию @RegisterForReflection и всё заработает, но что делать в более тяжелых случаях?
- @RegisterForReflection работает, но только в коде самого приложения. Если класс подключен в виде зависимости, то аннотация не работает. В таком случае поможет создание в коде проекта любого пустого класса с аннотацией @RegisterForReflection, в которой в параметре targets следует перечислить все классы из внешних пакетов, которые следует зарегистрировать для рефлексии. Да, класс может быть абсолютно пустой с единственной аннотацией @RegisterForReflection, кваркус его нормально подхватит. Нет, пакеты передавать в targets не получится, только список конкретных классов. Для этой цели есть вариант использовать @BuildStep, но у меня не получилось.
- В @RegisterForReflection.targets можно передавать интерфейсы, и они будут подхватывать все имплементации этих интерфейсов, но только при условии что и интерфейс и его реализации находятся в коде приложения. Т.е. если у вас что-то из них приходит из внешней зависимости — работать подключение через интерфейс не будет.
- Для динамически создаваемых классов, типа @ProxyGen, есть единственный вариант — использовать объявление через reflection-config.json и передавать его через аргумент сборки quarkus.native.additional-build-args = —H:ReflectionConfigurationFiles=reflection-config.json. При этом сам файл нужно размещать в корне папки resources.