Привет, любители 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/
Добавить комментарий