Про GraalVM я уже рассказывал в предыдущей статье. Из набора технологий GraalVM интересным была технология предварительной компиляции Ahead-of-Time compilation (AOT compilation), которая была на время экспериментально добавлена в обычный Java JDK. В Java 17 эта функция была выпилена. Ходят слухи, что в Java 21 она снова появится.
В GraalVM функция предкомпиляции позволяет так называемые нативные приложения (native images). То есть все ваше Java-приложение вместе со всеми потрохами Java SDK заворачивается в исполняемый файл и этот файл можно запускать, на любой другой машине даже если там нет Java JDK. Разумеется, exe-файл получается внушительный, но 1) GraalVM весьма умно при компиляции понимает, что надо в него заворачивать, а что не понадобится 2) когда это нас стали пугать exe файлы размером в 11 мегабайт?
Давно хотел поиграться с этой функцией, чтобы хотя бы в общих чертах понять сам процесс. Я решил попробовать собрать простейшее тестовое Spring-приложение в native-image не меняя в нем ничего, чтобы разобраться, какие танцы с бубном понадобятся, чтобы все заработало само собой просто из коробки.
В интернете полно видео и документации, но некоторые вещи из документации и видео не очевидны. Авторы опускают важные детали, из-за чего легко зайти в тупик и потратить много времени на поиск решения очевидных проблем. С бубном конечно пришлось немножко поплясать, поэтому делюсь опытом для того, чтобы неприятные грабли, на которые наступал я, не мешали вам придти к приятному результату.
Что нам понадобится
- сам GraalVM SDK для вашей операционной системы, который можно скачать с официального сайта
- Microsoft Visual Studio. Так как мы планируем создавать нативное приложение под Windows, нам нужен весь tool-chain для этого. А он доступен бесплатно в составе Microsoft Visual Studio.
- Spring Boot CLI, если вы предпочитаете быстренько создать простое Spring-приложение
- Micronaut, если есть желание поиграться с новеньким конкурентом Spring
- Maven, если вы предпочитаете работать с ним а не с Gradle
Доступ ко всем утилитам должен присутствовать в PATH, а путь к папке bin там, где установлен GraalVM, должен быть прописан в JAVA_HOME. Для удобства я создал переменную GRAALVM. В окне компиляции я даю команду
set JAVA_HOME=%GRAALVM%
и для данной сессии в данном окне путь JAVA_HOME указывает на папку, где установлен GraalVM. Это очень удобно, так как я не хочу менять JAVA_HOME для всех приложений (он у меня выставлен на Java JDK 20), если мне нужно только поиграться с GraalVM.
GraalVM разные!
Дистрибутивы GraalVM представлены в двух редакциях: CE (Community Edition) и EE (Enterprise Edition). CE (Community Edition) распространяется свободно и бесплатно, но набор функционала там меньше. EE (Enterprise Edition) распространяется по лицензии, но для любительских игр эта лицензия — чистая формальность.
Обращаю ваше внимание, что и внутри каждой редакций есть отличия. Дистрибутив под Windows не имеет поддержи некоторых языков (python, R), они есть только в Linux-версии. То есть поиграться с многоязычными (polyglot) приложениями на GraalVM можно на любой ОС, если это не Windows.
Добавляем native-image компонент к GraalVM
Сам GraalVM SDK еще не содержит всех компонентов, которые нам нужны. Для их установки воспользуемся утилитой gu, что идет в комплекте GraalVM SDK. Команда
gu list
покажем нам список установленных компонентов. Команда
gu available
покажет нам список компонентов, которые мы можем установить. Среди них есть компонент под названием native-image. Чтобы его установить, надо набрать команду
gu install native-image
Утилита gu загрузит компонент из интернета и установит его в вашем GraalVM SDK.
Создаем native image из Spring-приложения
В командной строке в отдельной папочке пишем:
spring init -d=web demo --build maven
В результате spring-boot cli создаст простейшее веб-приложение со встроенным веб-сервером и сконфигурирует его для работы с Maven. Если параметр —build пропустить, проект по умолчанию будет создан под работу с Gradle.
Проверим как проект компилируется. В новой папке demo, где уже есть pom.xml файл, пишем:
mvn clean install
Если все настроено верно, мы в результате в под-папке target получим толстый JAR-файл, который можно запустить так:
java -jar demo.jar
Наше веб-приложение запустится и будет ждать запросов на порту :8080. Это порт у меня на рабочей машине занят, поэтому единственное изменение, которое я сделал в данном простейшем тестовом приложении, это поменял порт в src/main/resrources/application.properties:
server.port=8888
После пересборки и запуска веб-приложение стало ждать запросы на порту :8888. Закроем приложение и создадим из него native image, т.е. exe-файл.
Для этого нам надо открыть специальное командное окно. Когда вы устанавливали Microsoft Visual Studio, вызов этого окна прописался в вашем стартовом меню. Называется это окно «x64 Native Tools Command Prompt for VS 2022«:

Это все та же командная cmd-строка, но сконфигурированная так, что из нее видны все утилиты Windows SDK tool-chain для компиляции Windows-приложений. Запомните это! Из обычного cmd-окна ничего не заработает: будут вылетать ошибки, что не найдены библиотеки и файлы!
Переходим в этом окне в нашу папку, где находится Spring-проект. В окне даем команду, о которой я упоминал выше, чтобы переключиться на GraalVM SDK:
set JAVA_HOME=%GRAALVM%
После этого набираем:
mvn -Pnative native:compile
Если все было сконфигурировано верно, запустится процесс сборки JAR-файла, а затем процесс его преобразования в exe. Весь процесс занял примерно 2 минуты на моей не очень мощной машинке. В результате в папке target вы найдете файл demo.exe, который можно запустить и на экране вы увидите все те же строки Spring-приложения, только на этот раз ваше веб-приложение запустится в 100 раз быстрее — 99 миллисекунд вместо 2.5 секунды для JAR-файла! И это естественно, ведь это реальный машинный код!

Создаем native image из Micronaut-приложения
Порядок действий почти такой же. В отдельной папке создаем простейшее Micronaut-приложение:
mn create-app demo —build maven
В новой под-папке demo, где уже лежит pom.xml, набираем:
mvn clean install
и потом запускаем:
mvn mn:run
Приложение опять таки будет ждать запросов на порту :8080. Чтобы поменять порт, пропишите в файле src/main/resources/application.yml:
micronaut: server: port: 8888
Для того чтобы собрать native-image этого простейшего приложения надо опять открыть окно «x64 Native Tools Command Prompt for VS 2022«. Перейти в нем в папку проекта. Дать команду:
set JAVA_HOME=%GRAALVM%
а потом запустить сборку:
mvn package -Dpackaging=native-image
В результате вы опять получите exe-файл demo.exe, который можно запустить как обычное Windows-приложение.
А на Linux?
На Linux native-image собирается чуть проще. Эксперимент я проводил с помощью Vagrant (и VirtualBox). Файл проекта можете найти в моем репозитории. На Linux вам вместо Microsoft Visual Studio понадобится gcc (Vagrant-скрипт установит его автоматически), а в остальном — все то же самое.
Выводы
Надеюсь, эта статья вам поможет сделать первые шаги в исследовании возможностей GraalVM и его функции предварительной компиляции.
Для чего это все может понадобиться? Native image функционал может понадобиться для создания микросервисов, которые размещаются в облаке в кластерах Kubernetes, где не имеет смысла выкатывать целый паровоз JDK только для того, чтобы запустить маленький микросервис. При использовании native-image экономится дисковое пространство, память, ускоряется запуск и работа самого микросервиса. Разумеется, есть нюансы. Но сама технология — очень интересна, а ее разработчики достойны всяческих похвал!