Недавно команда, занимающаяся портированием Spring для GraalVM, выпустила первый крупный релиз - Spring Native Beta. Вместе с создателями GraalVM они смогли пофиксить множество багов как в самом компиляторе так и спринге. Теперь у проекта появилась официальная поддержка, свой цикл релизов и его можно щупать :)
Самым главным препятствием при переносе кода из JVM в бинарники является проблема использования фишек, присущих только java - рефлексия, работа с classpath, динамическая загрузка классов и т.д.
Согласно документации, ключевые различия между обычным JVM и нативной реализацией заключаются в следующем:
Статический анализ всего приложения выполняется во время сборки.
Неиспользуемые компоненты удаляются во время сборки.
Рефлексия, ресурсы и динамические прокси могут быть настроены только с помощью дополнительных конфигураций.
На время сборки фиксируются все компоненты в Classpath.
Нет ленивой загрузки класса: при загрузке все, что поставляется в исполняемых файлах, будет загружено в память. Например, чтобы вызов Class.forName ("myClass") отработал верно, нужно иметь myClass в файле конфигурации. Если в файле конфигурации не будет найден класс, который запрашивается для динамической загрузки класса, будет выбрано исключение ClassNotFoundException
Часть кода будет запущена во время сборки, чтобы правильно связать компоненты. Например, тесты.
В самом спринге рефлексия, создание прокси и ленивая инициализация встречается практически везде, поэтому все конфигурации надо было аккуратно обработать, из-за этого к релизу шли больше года.
В ходе исследований был создан новый компонент Spring AOT, который отвечает за все необходимые преобразования вашего кода в удобоваримый для Graal VM формат.
Spring AOT анализирует код и на основе него создает файлы конфигурации такие как native-image.properties
, reflection-config.json
, proxy-config.json
или resource-config.json
.
Так как Graal VM поддерживает первоначальную настройку через статические файлы, эти файлы помещаются при сборке в каталог META-INF/native-image
.
Для каждого сборщика выпущен свой плагин, который активирует работу Spring AOT. Для maven это spring-aot-maven-plugin
, соответственно для gradle - spring-aot-gradle-plugin.
Для того, чтобы добавить gradle плагин в свой проект нужна всего одна строка:
plugins {id 'org.springframework.experimental.aot' version '0.9.0'}
Плагин пытается сконфигурировать максимально возможное количество компонентов, выполняется предварительные преобразования по всем компонентам программы, необходимые для улучшения совместимости.
В случае, если ему это не удалось, вам в ручную необходимо добавить эти данные. Это можно сделать вручную поправив файлы конфигурации, либо использовать специально созданные аннотации.
Например, для случаев реализации компонентов с помощью WebClient
можно использовать аннотацию из пакета org.springframework.nativex.hint
, чтобы указать какой тип мы будем обрабатывать:
@TypeHint(types = Data.class, typeNames = "com.example.webclient.Data$SuperHero")
@SpringBootApplication
public class WebClientApplication {
// ...
}
Здесь мы указываем, что будем сериализовать класс Data
, в котором есть подкласс SuperHero
. Во время сборки для нас заранее создадут клиент, который сможет работать с этим типом данных.
Так как graavlvm не поддерживает работу с динамическими прокси, то для поддержки работы с java.lang.reflect.Proxy
создана аннотация @ProxyHint
.
Применять ее можно, например, так:
@ProxyHint(types = {
org.hibernate.Session.class,
org.springframework.orm.jpa.EntityManagerProxy.class
})
Если необходимо подтянуть какие-либо ресурсы в образ, то необходимо воспользоваться аннотацией @ResourceHint.
Например, таким образом:
@ResourceHint(patterns = "com/mysql/cj/TlsSettings.properties")
Чтобы указать какие классы / пакеты должны быть инициализированы явно во время сборки или выполнения, нужно воспользоваться аннотацией @InitializationHint:
@InitializationHint(types = org.h2.util.Bits.class,
initTime = InitializationTime.BUILD)
Для того, чтобы компактно собрать все эти аннотации воедино создана аннотация @NativeHint
:
@Repeatable(NativeHints.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface NativeHint
Все вместе это будет выглядеть, например, вот так:
@NativeHint(
trigger = Driver.class,
options = "--enable-all-security-services",
types = @TypeHint(types = {
FailoverConnectionUrl.class,
FailoverDnsSrvConnectionUrl.class,
// ...
}), resources = {
@ResourceHint(patterns = "com/mysql/cj/TlsSettings.properties"),
@ResourceHint(patterns = "com.mysql.cj.LocalizedErrorMessages",
isBundle = true)
})
В качестве тригера, мы выбираем тот класс, присутствие которого в classpath должно вызвать построение конфигурации.
Все активные аннотации учитываются во время компиляции и преобразуются в конфигурацию Graal VM плагином Spring AOT.
Spring Native уже включена в релизный цикл, забрать шаблон можно прямо со start.spring.io. Так как поддержка JPA и прочих spring компонентов уже реализована, то собрать простое CRUD приложение можно сразу. Если необходимо указать дополнительные параметры Graal VM при сборке, их можно добавить с помощью переменной среды BP_NATIVE_IMAGE_BUILD_ARGUMENTS
в плагине Spring AOT, если сборка идет через Buildpacks, или с помощью элемента конфигурации “<buildArgs>
” в pom.xml
, если вы собираете через плагин native-image-maven-plugin
.
Собственно, выполняем команды mvn spring-boot: build-image или gradle bootBuildImage
- и начнется сборка образа. Стоит отметить, что сборщику нужно более 7 Гб памяти, для того сборка завершилась успешно. На моей машине сборка, вместе с загрузкой образов заняла не более 5 минут. При этом образ получился очень компактным, всего 60 Мб. Стартовало приложение за 0.022 секунды! Это невероятный результат. Учитывая, что все большее количество компаний переходит на K8s и старт приложения, так же как и используемые ресурсы очень важны в современном мире, то данная технология позволяет Spring сделать фреймворком номер один для всех типов микросервисов, даже для реализаций FaaS, где очень важна скорость холодного старта.