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

от автора

Перевод статьи подготовлен в преддверии старта курса «Разработчик на 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).


Узнать о курсе подробнее

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/503328/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *