Перевод статьи подготовлен в преддверии старта курса «Разработчик на Spring Framework».
Привет, любители Spring’а! Добро пожаловать в очередной выпуск Spring Tips. Сегодня мы поговорим о недавно реализованной поддержке компиляции Spring Boot-приложений в GraalVM. Мы уже говорили о GraalVM и нативных приложениях в другом выпуске Spring Tips в теме про Spring Fu.
Немного вспомним, что такое GraalVM. GraalVM — замена стандартного компилятора C1 в OpenJDK. Подробнее об использовании GraalVM вы можете послушать в моем подкасте Bootiful Podcast с Крисом Талингером (Chris Thalinge) — контрибьютором GraalVM и инженером Twitter. При определенных условиях GraalVM позволяет быстрее запускать обычные Spring-приложения и, хотя бы по этой причине, он заслуживает внимания.
Но мы не будем говорить об этом. Мы посмотрим на другие компоненты GraalVM: native image builder и SubstrateVM. SubstrateVM позволяет создавать нативные исполняемые файлы для вашего Java-приложения. Кстати, об этом и других использованиях GraalVM был подкаст с Олегом Шелаевым из Oracle Labs. Native image builder — это испытание на поиск компромиссов. Если вы предоставите GraalVM достаточно информации о поведении вашего приложения в runtime (динамически связанные библиотеки, рефлексия, прокси и т. д.), то он сможет превратить ваше Java-приложение в статически линкованый бинарник, наподобие приложения на C или Golang. Честно говоря, этот процесс может быть довольно болезненным. Но если вы это сделаете, то сможете сгенерировать нативный код, который будет невероятно быстрым. В результате приложение будет занимать гораздо меньше оперативной памяти и запускаться менее чем за секунду. Меньше секунды. Довольно заманчиво, не правда ли? Конечно!
Однако следует помнить, что необходимо учитывать некоторые моменты. Полученные GraalVM-бинарники — это не Java-приложения. Они даже не запускаются на обычной JVM. Разработкой GraalVM занимается Oracle Labs и между командами Java и GraalVM есть какое-то взаимодействие, но я бы не назвал это Java. Полученный бинарник не будет кроссплатформенным. При работе приложения не используется JVM. Оно работает в другой среде выполнения, которая называется SubstrateVM.
Таким образом, здесь много компромиссов, но, тем не менее, я думаю, что у GraalVM есть большой потенциал, особенно для облачных приложений, где первостепенное значение имеют масштабирование и эффективность.
Давайте начнем. Устанавливаем GraalVM. Вы можете скачать его здесь, или установить с помощью SDKManager. Для установки дистрибутивов Java мне нравится использовать SDKManager. GraalVM немного отстает от последних версий Java и в настоящее время поддерживает Java 8 и 11. Поддержка Java 14 или 15 или более поздней (какая там будет версия, когда вы будете это читать) отсутствует.
Чтобы установить GraalVM для Java 8 запустите:
Я рекомендую использовать Java 8, а не Java 11, так как в Java 11 есть некоторые непонятные ошибки, с которыми я еще не разобрался.
После этого необходимо установить компонент native image builder. Запустите:
Наконец, проверьте, что
Теперь, когда вы все настроили, давайте посмотрим на наше приложение. Перейдите к Spring Initializr и сгенерируйте новый проект с использованием Lombok, R2DBC, PostgreSQL и Reactive Web.
Подобный код вы видели миллион раз, поэтому я не буду его разбирать, а просто приведу его здесь.
Полный код вы можете посмотреть здесь.
Единственная особенность этого приложения в том, что мы используем Spring Boot-атрибут
Итак, идем дальше. Я уже упоминал ранее, что мы должны сказать GraalVM о некоторых моментах, которые могут быть в нашем приложении во время выполнения и что он может не понять при выполнении нативного кода. Это такие вещи как рефлексия, прокси и т. д. Для этого есть несколько способов. Можно описать конфигурацию вручную и включить ее в сборку. GraalVM автоматически подхватит ее. Другой способ заключается в том, чтобы запустить программу с Java-агентом, который отслеживает, что делает приложение и, после завершения работы приложения, записывает всё в конфигурационные файлы, которые затем могут быть переданы компилятору GraalVM.
Еще вы можете использовать GraalVM feature. (Прим. переводчика: “feature“ — термин GraalVM обозначающий плагин для нативной компиляции, создающий нативный бинарник из class-файла). GraalVM feature похожа на Java-агента. Она может делать какой-то анализ и передавать информацию в компилятор GraalVM. Feature знает и понимает, как работает Spring-приложение. Ей известно, когда Spring-бины являются прокси. Она знает, как динамически в runtime создаются классы. Она знает, как работает Spring, и знает, чего хочет GraalVM, по крайней мере, большую часть времени (в конце концов, это ранний релиз!)
Также нужно настроить сборку. Вот мой
Здесь обратим внимание на плагин
Мы хотим, чтобы для компилятора GraalVM было как можно больше способов предоставления информации о том, как должно работать приложение: java-агент, GraalVM Feature, параметры командной строки. Всё это вместе дает GraalVM достаточно информации, чтобы успешно превратить приложение в статически скомпилированный нативный бинарник. В долгосрочной перспективе наша цель — проекты Spring. И Spring GraalVM feature предоставляет все необходимое для их поддержки.
Теперь, когда мы все настроили, давайте соберем приложение:
Если все прошло без ошибок, то в каталоге
Приложение запускается, что видно из вывода, подобного этому.
Неплохо? GraalVM native image builder отлично подходит для работы в паре с облачной платформой, такой как CloudFoundry или Kubernetes. Вы можете легко собрать приложение в контейнер и запустить его в облаке с минимальными ресурсами.
Как всегда, мы будем рады вас услышать. Подходит ли эта технология вам? Вопросы? Комментарии? Twitter (@springcentral).
Узнать о курсе подробнее
Привет, любители Spring’а! Добро пожаловать в очередной выпуск Spring Tips. Сегодня мы поговорим о недавно реализованной поддержке компиляции Spring Boot-приложений в GraalVM. Мы уже говорили о GraalVM и нативных приложениях в другом выпуске Spring Tips в теме про Spring Fu.
Немного вспомним, что такое GraalVM. GraalVM — замена стандартного компилятора C1 в OpenJDK. Подробнее об использовании GraalVM вы можете послушать в моем подкасте Bootiful Podcast с Крисом Талингером (Chris Thalinge) — контрибьютором GraalVM и инженером Twitter. При определенных условиях GraalVM позволяет быстрее запускать обычные Spring-приложения и, хотя бы по этой причине, он заслуживает внимания.
Но мы не будем говорить об этом. Мы посмотрим на другие компоненты GraalVM: native image builder и SubstrateVM. SubstrateVM позволяет создавать нативные исполняемые файлы для вашего Java-приложения. Кстати, об этом и других использованиях GraalVM был подкаст с Олегом Шелаевым из Oracle Labs. Native image builder — это испытание на поиск компромиссов. Если вы предоставите GraalVM достаточно информации о поведении вашего приложения в runtime (динамически связанные библиотеки, рефлексия, прокси и т. д.), то он сможет превратить ваше Java-приложение в статически линкованый бинарник, наподобие приложения на C или Golang. Честно говоря, этот процесс может быть довольно болезненным. Но если вы это сделаете, то сможете сгенерировать нативный код, который будет невероятно быстрым. В результате приложение будет занимать гораздо меньше оперативной памяти и запускаться менее чем за секунду. Меньше секунды. Довольно заманчиво, не правда ли? Конечно!
Однако следует помнить, что необходимо учитывать некоторые моменты. Полученные GraalVM-бинарники — это не Java-приложения. Они даже не запускаются на обычной JVM. Разработкой GraalVM занимается Oracle Labs и между командами Java и GraalVM есть какое-то взаимодействие, но я бы не назвал это Java. Полученный бинарник не будет кроссплатформенным. При работе приложения не используется JVM. Оно работает в другой среде выполнения, которая называется SubstrateVM.
Таким образом, здесь много компромиссов, но, тем не менее, я думаю, что у GraalVM есть большой потенциал, особенно для облачных приложений, где первостепенное значение имеют масштабирование и эффективность.
Давайте начнем. Устанавливаем GraalVM. Вы можете скачать его здесь, или установить с помощью SDKManager. Для установки дистрибутивов Java мне нравится использовать SDKManager. GraalVM немного отстает от последних версий Java и в настоящее время поддерживает Java 8 и 11. Поддержка Java 14 или 15 или более поздней (какая там будет версия, когда вы будете это читать) отсутствует.
Чтобы установить GraalVM для Java 8 запустите:
sdk install java 20.0.0.r8-grl
Я рекомендую использовать Java 8, а не Java 11, так как в Java 11 есть некоторые непонятные ошибки, с которыми я еще не разобрался.
После этого необходимо установить компонент native image builder. Запустите:
gu install native-image
gu
— это утилита из GraalVM. Наконец, проверьте, что
JAVA_HOME
указывает на GraalVM. На моей машине (Macintosh с SDKMAN) мой JAVA_HOME
выглядит так:export JAVA_HOME=$HOME/.sdkman/candidates/java/current/
Теперь, когда вы все настроили, давайте посмотрим на наше приложение. Перейдите к Spring Initializr и сгенерируйте новый проект с использованием Lombok, R2DBC, PostgreSQL и Reactive Web.
Подобный код вы видели миллион раз, поэтому я не буду его разбирать, а просто приведу его здесь.
package com.example.reactive;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.stream.Stream;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
@Log4j2
@SpringBootApplication(proxyBeanMethods = false)
public class ReactiveApplication {
@Bean
RouterFunction<ServerResponse> routes(ReservationRepository rr) {
return route()
.GET("/reservations", r -> ok().body(rr.findAll(), Reservation.class))
.build();
}
@Bean
ApplicationRunner runner(DatabaseClient databaseClient, ReservationRepository reservationRepository) {
return args -> {
Flux<Reservation> names = Flux
.just("Andy", "Sebastien")
.map(name -> new Reservation(null, name))
.flatMap(reservationRepository::save);
databaseClient
.execute("create table reservation ( id serial primary key, name varchar(255) not null )")
.fetch()
.rowsUpdated()
.thenMany(names)
.thenMany(reservationRepository.findAll())
.subscribe(log::info);
};
}
public static void main(String[] args) {
SpringApplication.run(ReactiveApplication.class, args);
}
}
interface ReservationRepository extends ReactiveCrudRepository<Reservation, Integer> {
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Reservation {
@Id
private Integer id;
private String name;
}
Полный код вы можете посмотреть здесь.
Единственная особенность этого приложения в том, что мы используем Spring Boot-атрибут
proxyBeanMethods
для того, чтобы убедиться, что в приложении не будут использоваться cglib и другие, отличные от JDK прокси. GraalVM не поддерживает не-JDK прокси. Хотя даже и с JDK-прокси придется повозиться, чтобы GraalVM узнал о них. Этот атрибут, новый для Spring Framework 5.2, отчасти предназначен для поддержки GraalVM.Итак, идем дальше. Я уже упоминал ранее, что мы должны сказать GraalVM о некоторых моментах, которые могут быть в нашем приложении во время выполнения и что он может не понять при выполнении нативного кода. Это такие вещи как рефлексия, прокси и т. д. Для этого есть несколько способов. Можно описать конфигурацию вручную и включить ее в сборку. GraalVM автоматически подхватит ее. Другой способ заключается в том, чтобы запустить программу с Java-агентом, который отслеживает, что делает приложение и, после завершения работы приложения, записывает всё в конфигурационные файлы, которые затем могут быть переданы компилятору GraalVM.
Еще вы можете использовать GraalVM feature. (Прим. переводчика: “feature“ — термин GraalVM обозначающий плагин для нативной компиляции, создающий нативный бинарник из class-файла). GraalVM feature похожа на Java-агента. Она может делать какой-то анализ и передавать информацию в компилятор GraalVM. Feature знает и понимает, как работает Spring-приложение. Ей известно, когда Spring-бины являются прокси. Она знает, как динамически в runtime создаются классы. Она знает, как работает Spring, и знает, чего хочет GraalVM, по крайней мере, большую часть времени (в конце концов, это ранний релиз!)
Также нужно настроить сборку. Вот мой
pom.xml
.<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.M4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>reactive</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<start-class>
com.example.reactive.ReactiveApplication
</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-graal-native</artifactId>
<version>0.6.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<finalName>
${project.artifactId}
</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
<profiles>
<profile>
<id>graal</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>20.0.0</version>
<configuration>
<buildArgs>
-Dspring.graal.mode=initialization-only -Dspring.graal.dump-config=/tmp/computed-reflect-config.json -Dspring.graal.verbose=true -Dspring.graal.skip-logback=true --initialize-at-run-time=org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils --initialize-at-build-time=io.r2dbc.spi.IsolationLevel,io.r2dbc.spi --initialize-at-build-time=io.r2dbc.spi.ConstantPool,io.r2dbc.spi.Assert,io.r2dbc.spi.ValidationDepth --initialize-at-build-time=org.springframework.data.r2dbc.connectionfactory -H:+TraceClassInitialization --no-fallback --allow-incomplete-classpath --report-unsupported-elements-at-runtime -H:+ReportExceptionStackTraces --no-server --initialize-at-build-time=org.reactivestreams.Publisher --initialize-at-build-time=com.example.reactive.ReservationRepository --initialize-at-run-time=io.netty.channel.unix.Socket --initialize-at-run-time=io.netty.channel.unix.IovArray --initialize-at-run-time=io.netty.channel.epoll.EpollEventLoop --initialize-at-run-time=io.netty.channel.unix.Errors
</buildArgs>
</configuration>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Здесь обратим внимание на плагин
native-image-maven-plugin
. Он принимает параметры через командную строку, которые помогают ему понять, что нужно делать. Все эти параметры в buildArgs
необходимы, чтобы приложение могло запуститься. (Я должен выразить огромную благодарность Энди Клементу (Andy Clement) — мейнтейнеру Spring GraalVM Feature, — за то, что он помог разобраться со всеми этими параметрами!)<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-graal-native</artifactId>
<version>0.6.0.RELEASE</version>
</dependency>
Мы хотим, чтобы для компилятора GraalVM было как можно больше способов предоставления информации о том, как должно работать приложение: java-агент, GraalVM Feature, параметры командной строки. Всё это вместе дает GraalVM достаточно информации, чтобы успешно превратить приложение в статически скомпилированный нативный бинарник. В долгосрочной перспективе наша цель — проекты Spring. И Spring GraalVM feature предоставляет все необходимое для их поддержки.
Теперь, когда мы все настроили, давайте соберем приложение:
- Скомпилируйте Java-приложение обычным способом
- Запустите Java-приложение с Java-агентом для сбора информации. На данном этапе мы должны убедиться, что приложение работает. Необходимо пройтись по всем возможным сценариям использования. Кстати, это очень хороший кейс для использования CI и тестов! Все постоянно говорят о тестировании приложения и улучшении производительности. Теперь, с GraalVM, вы можете сделать и то и другое!
- Затем пересоберите приложение, на этот раз с активным профилем graal, чтобы скомпилировать в нативное приложение, использовав информацию, собранную при первом запуске.
mvn -DskipTests=true clean package
export MI=src/main/resources/META-INF
mkdir -p $MI
java -agentlib:native-image-agent=config-output-dir=${MI}/native-image -jar target/reactive.jar
## it's at this point that you need to exercise the application: http://localhost:8080/reservations
## then hit CTRL + C to stop the running application.
tree $MI
mvn -Pgraal clean package
Если все прошло без ошибок, то в каталоге
target
вы увидите откомпилированное приложение. Запустите его../target/com.example.reactive.reactiveapplication
Приложение запускается, что видно из вывода, подобного этому.
2020-04-15 23:25:08.826 INFO 7692 --- [ main] c.example.reactive.ReactiveApplication : Started ReactiveApplication in 0.099 seconds (JVM running for 0.103)
Неплохо? GraalVM native image builder отлично подходит для работы в паре с облачной платформой, такой как CloudFoundry или Kubernetes. Вы можете легко собрать приложение в контейнер и запустить его в облаке с минимальными ресурсами.
Как всегда, мы будем рады вас услышать. Подходит ли эта технология вам? Вопросы? Комментарии? Twitter (@springcentral).
Узнать о курсе подробнее