1. Обзор
В этом практическом занятии мы познакомимся с проектом Spring Native, создадим приложение, использующее его, и развернем его в Google Cloud.
Мы рассмотрим его компоненты, недавнюю историю проекта, некоторые варианты использования и, конечно же, шаги, необходимые для его применения в ваших проектах.
Проект Spring Native в настоящее время находится на экспериментальной стадии, поэтому для начала работы потребуется определенная настройка. Однако, как было объявлено на SpringOne 2021, Spring Native планируется интегрировать в Spring Framework 6.0 и Spring Boot 3.0 с полноценной поддержкой , поэтому сейчас самое подходящее время, чтобы поближе познакомиться с проектом за несколько месяцев до его релиза.
Хотя компиляция «на лету» очень хорошо оптимизирована для таких задач, как длительные процессы, существуют определенные сценарии использования, в которых приложения, скомпилированные заранее, показывают еще лучшие результаты, что мы обсудим во время практического занятия.
Вы узнаете, как
- Используйте Cloud Shell
- Включите API Cloud Run
- Создайте и разверните приложение Spring Native.
- Разверните такое приложение в Cloud Run.
Что вам понадобится
- Проект на платформе Google Cloud Platform с активным платежным аккаунтом GCP.
- Необходимо установить gcloud cli или иметь доступ к Cloud Shell.
- Базовые навыки Java + XML
- Практические навыки работы с распространенными командами Linux.
Опрос
Как вы будете использовать этот учебный материал?
Как бы вы оценили свой опыт работы с Java?
Как бы вы оценили свой опыт использования сервисов Google Cloud?
2. Предыстория
Проект Spring Native использует ряд технологий для обеспечения разработчикам производительности нативных приложений.
Для полного понимания Spring Native полезно разобраться в некоторых из этих компонентных технологий, в том, что они нам позволяют делать и как они взаимодействуют здесь.
Компиляция AOT
Когда разработчики запускают javac в обычном режиме во время компиляции, наш исходный код .java компилируется в файлы .class, написанные на байт-коде. Этот байт-код предназначен только для понимания виртуальной машиной Java, поэтому JVM должна будет интерпретировать этот код на других машинах, чтобы мы могли запустить свой код.
Именно этот процесс обеспечивает фирменную переносимость Java — позволяя нам «написать один раз и запустить везде», но он обходится дороже по сравнению с запуском нативного кода.
К счастью, большинство реализаций JVM используют компиляцию «на лету» для снижения затрат на интерпретацию. Это достигается путем подсчета вызовов функции, и если она вызывается достаточно часто, чтобы превысить пороговое значение ( по умолчанию 10 000 ), она компилируется в нативный код во время выполнения, чтобы предотвратить дальнейшую дорогостоящую интерпретацию.
Компиляция «на лету» использует противоположный подход, компилируя весь доступный код в нативный исполняемый файл во время компиляции. Это приводит к компромиссу между переносимостью и эффективностью использования памяти, а также к повышению производительности во время выполнения.

Это, конечно, компромисс, и не всегда стоит идти на него. Однако AOT-компиляция может быть полезна в определенных сценариях использования, таких как:
- Приложения с коротким сроком жизни, где время запуска имеет значение
- Среды с жесткими ограничениями по памяти, где JIT-компиляция может быть слишком затратной.
Интересный факт: AOT-компиляция была введена как экспериментальная функция в JDK 9, хотя её поддержка обходилась дорого, и она так и не прижилась, поэтому её тихо удалили в Java 17, и разработчики стали использовать GraalVM .
GraalVM
GraalVM — это высокооптимизированный дистрибутив JDK с открытым исходным кодом, отличающийся чрезвычайно быстрым запуском, компиляцией образов AOT и возможностью работы с несколькими языками, что позволяет разработчикам объединять несколько языков в одном приложении.
GraalVM находится в активной разработке, постоянно расширяет свои возможности и улучшает существующие, поэтому я призываю разработчиков следить за обновлениями.
К числу недавних достижений относятся:
- Новый, удобный для пользователя способ создания образов в собственном формате ( 18.01.2021 )
- Поддержка Java 17 ( 18.01.2022 )
- Включение многоуровневой компиляции по умолчанию для улучшения времени компиляции полиглотов ( 2021-04-20 )
Весенний местный
Проще говоря, Spring Native позволяет использовать компилятор native-image из GraalVM для преобразования приложений Spring в нативные исполняемые файлы.
Этот процесс включает в себя выполнение статического анализа вашего приложения на этапе компиляции для поиска всех методов, доступных из точки входа.
По сути, это создает концепцию «закрытого мира» для вашего приложения, где весь код считается известным на этапе компиляции, и загрузка нового кода во время выполнения не допускается.
Важно отметить, что генерация изображений нативно — это ресурсоемкий процесс, требующий больше памяти, чем компиляция обычного приложения, и накладывающий ограничения на некоторые аспекты Java.
В некоторых случаях для корректной работы приложения со Spring Native не требуется вносить изменения в код. Однако в определенных ситуациях для правильной работы необходима специфическая нативная конфигурация. В таких случаях Spring Native часто предоставляет Native Hints для упрощения этого процесса.
3. Подготовка/Предварительные работы
Прежде чем приступить к внедрению Spring Native, нам необходимо создать и развернуть наше приложение, чтобы установить базовый уровень производительности, который мы сможем позже сравнить с нативной версией.
1. Создание проекта
Начнём с загрузки нашего приложения с сайта start.spring.io:
curl https://start.spring.io/starter.zip -d dependencies=web \
-d javaVersion=11 \
-d bootVersion=2.6.4 -o io-native-starter.zip
В этом стартовом приложении используется Spring Boot 2.6.4, последняя версия, поддерживаемая проектом spring-native на момент написания статьи.
Обратите внимание, что начиная с версии GraalVM 21.0.3 , для этого примера можно использовать Java 17. Однако в данном руководстве мы по-прежнему будем использовать Java 11, чтобы минимизировать объем необходимой конфигурации.
Получив доступ к zip-архиву через командную строку, мы можем создать подкаталог для нашего проекта и распаковать папку в него:
mkdir spring-native cd spring-native unzip ../io-native-starter.zip
2. Изменения в коде
Как только проект будет запущен, мы быстро добавим признаки жизни и продемонстрируем выступление Spring Native, как только оно состоится.
Отредактируйте файл DemoApplication.java следующим образом:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.time.Instant;
@RestController
@SpringBootApplication
public class DemoApplication {
private static Instant startTime;
private static Instant readyTime;
public static void main(String[] args) {
startTime = Instant.now();
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/")
public String index() {
return "Time between start and ApplicationReadyEvent: "
+ Duration.between(startTime, readyTime).toMillis()
+ "ms";
}
@EventListener(ApplicationReadyEvent.class)
public void ready() {
readyTime = Instant.now();
}
}
На данном этапе наше базовое приложение готово к запуску, поэтому вы можете создать образ и запустить его локально, чтобы оценить время запуска, прежде чем мы преобразуем его в нативное приложение.
Для создания нашего имиджа:
mvn spring-boot:build-image
Также вы можете использовать docker images demo , чтобы получить представление о размере базового образа: 
Для запуска нашего приложения:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
3. Разверните базовое приложение.
Теперь, когда у нас есть приложение, мы его развернем и запишем время запуска, которое позже сравним со временем запуска нашего нативного приложения.
В зависимости от типа разрабатываемого приложения существует несколько различных вариантов размещения ваших данных .
Однако, поскольку наш пример представляет собой очень простое и понятное веб-приложение, мы можем упростить задачу и положиться на Cloud Run.
Если вы используете свой компьютер, убедитесь, что у вас установлен и обновлен инструмент командной строки gcloud .
Если вы используете Cloud Shell, все необходимые действия будут выполнены автоматически, и вы сможете просто запустить следующую команду в исходном каталоге:
gcloud run deploy
4. Настройка приложения
1. Настройка наших репозиториев Maven
Поскольку этот проект все еще находится на экспериментальной стадии, нам придется настроить наше приложение таким образом, чтобы оно могло находить экспериментальные артефакты, которые недоступны в центральном репозитории Maven.
Для этого потребуется добавить следующие элементы в наш файл pom.xml, что вы можете сделать в любом редакторе по вашему выбору.
Добавьте в файл pom следующие разделы repositories и pluginRepositories:
<repositories>
<repository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
2. Добавление зависимостей
Далее добавьте зависимость spring-native, необходимую для запуска приложения Spring в качестве нативного образа. Примечание: этот шаг не требуется, если вы используете Gradle.
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
3. Добавление/включение наших плагинов
Теперь добавьте плагин AOT для улучшения совместимости с нативными образами и уменьшения занимаемого ими места ( подробнее ):
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>0.11.2</version>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
Теперь обновим spring-boot-maven-plugin, чтобы включить поддержку нативных образов, и воспользуемся сборщиком paketo для сборки нашего нативного образа:
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
</plugins>
Обратите внимание, что крошечный образ сборщика — лишь один из нескольких вариантов. Он хорошо подходит для нашего случая, поскольку содержит очень мало дополнительных библиотек и утилит, что помогает минимизировать поверхность атаки.
Например, если вы разрабатываете приложение, которому необходим доступ к некоторым распространенным библиотекам C, или вы еще не уверены в требованиях вашего приложения, то полнофункциональный конструктор может подойти лучше.
5. Создайте и запустите нативное приложение.
Как только все это будет настроено, мы сможем собрать образ и запустить наше скомпилированное нативное приложение.
Перед запуском сборки следует учесть несколько моментов:
- Это займет больше времени, чем обычная сборка (несколько минут).

- Этот процесс сборки может потребовать большого объема памяти (несколько гигабайт).

- Для этого процесса сборки необходимо, чтобы демон Docker был доступен.
- В этом примере мы выполняем процесс вручную, но вы также можете настроить этапы сборки таким образом, чтобы они автоматически запускали собственный профиль сборки .
Для создания нашего имиджа:
mvn spring-boot:build-image
Как только это будет сделано, мы будем готовы увидеть нативное приложение в действии!
Для запуска нашего приложения:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
На данном этапе у нас есть прекрасная возможность рассмотреть обе стороны вопроса, касающегося нативных приложений.
Мы немного пожертвовали временем и дополнительным использованием памяти на этапе компиляции, но взамен получаем приложение, которое запускается гораздо быстрее и потребляет значительно меньше памяти (в зависимости от рабочей нагрузки).
Если мы запустим docker images demo , чтобы сравнить размер исходного образа с размером исходного, мы увидим существенное уменьшение:

Следует также отметить, что в более сложных сценариях использования требуются дополнительные модификации, чтобы сообщить компилятору AOT о том, что ваше приложение будет делать во время выполнения. По этой причине некоторые предсказуемые рабочие нагрузки (например, пакетные задания) могут очень хорошо подходить для этого, в то время как другие могут потребовать больших усилий.
6. Развертывание нашего нативного приложения
Для развертывания нашего приложения в Cloud Run нам потребуется загрузить наш нативный образ в менеджер пакетов, например, Artifact Registry .
1. Подготовка нашего репозитория Docker
Начать этот процесс можно с создания репозитория:
gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"
Далее нам нужно убедиться, что мы прошли аутентификацию, чтобы отправить данные в наш новый реестр.
Интерфейс командной строки gcloud может значительно упростить этот процесс:
gcloud auth configure-docker us-central1-docker.pkg.dev
2. Загрузка нашего образа в реестр артефактов.
Далее мы добавим теги к нашему изображению:
export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
docker tag demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
А затем мы можем использовать docker push , чтобы отправить это в реестр артефактов:
docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
3. Развертывание в облаке
Теперь мы готовы развернуть образ, сохраненный в Artifact Registry, в Cloud Run:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Поскольку мы разработали и развернули наше приложение как нативный образ, мы можем быть уверены, что наше приложение эффективно использует наши инфраструктурные затраты во время работы.
Не стесняйтесь самостоятельно сравнить время запуска нашего базового приложения с новым нативным приложением!

7. Подведение итогов/Завершение
Поздравляем с созданием и развертыванием приложения Spring Native в Google Cloud!
Надеюсь, этот урок поможет вам лучше познакомиться с проектом Spring Native и будет полезен в будущем, если он вам понадобится.
Дополнительно: Очистка и/или отключение сервиса.
Независимо от того, создали ли вы проект Google Cloud для этого практического занятия или используете существующий, постарайтесь избежать ненужных расходов на ресурсы, которые мы использовали.
Вы можете удалить или отключить созданные нами службы Cloud Run, удалить размещенный нами образ или закрыть весь проект.
8. Дополнительные ресурсы
Хотя проект Spring Native в настоящее время является новым и экспериментальным, уже существует множество полезных ресурсов, которые помогут первым пользователям устранять неполадки и принимать участие в его реализации:
Дополнительные ресурсы
Ниже приведены онлайн-ресурсы, которые могут быть полезны для данного урока:
- Узнайте больше о Native Hints
- Узнайте больше о GraalVM
- Как принять участие
- Ошибка нехватки памяти при сборке нативных образов.
- Ошибка запуска приложения
Лицензия
Данная работа распространяется под лицензией Creative Commons Attribution 2.0 Generic.