Компилируем Spring Boot-приложение в нативное с помощью GraalVM

Автор оригинала: Josh Long (@starbuxman)
  • Перевод
Перевод статьи подготовлен в преддверии старта курса «Разработчик на 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 запустите:

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).



Узнать о курсе подробнее
OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 1

    0
    Я вас очень очень очень прошу. когда вы вставляете видео — прячьте его ПОД КАТ. вы портите RSS ленту

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое