Nous compilons l'application Spring Boot en natif à l'aide de GraalVM

Une traduction de l'article a été préparée avant le début du cours "Developer on the Spring Framework" .



Bonjour les amoureux du printemps! Bienvenue dans la prochaine version de Spring Tips. Aujourd'hui, nous allons parler de la prise en charge récemment mise en œuvre pour la compilation des applications Spring Boot dans GraalVM. Nous avons déjà parlé de GraalVM et des applications natives dans une autre version de Spring Tips sur le sujet de Spring Fu.



Rappelons ce qu'est GraalVM. GraalVM remplace le compilateur C1 standard dans OpenJDK. Vous pouvez en savoir plus sur l'utilisation de GraalVM dans mon podcast Bootiful Podcast avec Chris Thalinge, contributeur GraalVM et ingénieur Twitter. Dans certaines conditions, GraalVM vous permet d'exécuter des applications Spring régulières plus rapidement et, au moins pour cette raison, il mérite l'attention.

Mais nous n'en parlerons pas. Nous examinerons d'autres composants GraalVM: constructeur d'images natives et SubstrateVM. SubstrateVM vous permet de créer des exécutables natifs pour votre application Java. À propos, à propos de cela et d'autres utilisations de GraalVM, il y avait un podcast avec Oleg Shelaev d'Oracle Labs. Le générateur d'images natives est un test de compromis. Si vous fournissez à GraalVM suffisamment d'informations sur le comportement de votre application lors de l'exécution (bibliothèques liées dynamiquement, réflexion, proxys, etc.), il pourra transformer votre application Java en un binaire lié statiquement, comme une application C ou Golang. Honnêtement, ce processus peut être assez douloureux. Mais si vous le faites, vous pouvez générer du code natif qui sera incroyablement rapide. En conséquence, l'application prendra beaucoup moins de RAM et fonctionnera en moins d'une seconde. Moins d'une seconde. Assez tentant, non? Bien sûr!

Cependant, il convient de rappeler que certains points doivent être pris en compte. Les binaires GraalVM résultants ne sont pas des applications Java. Ils ne fonctionnent même pas sur une machine virtuelle Java standard. GraalVM est développé par Oracle Labs et il existe une sorte d'interaction entre les équipes Java et GraalVM, mais je ne l'appellerais pas Java. Le binaire résultant ne sera pas multiplateforme. L'application n'utilise pas la JVM. Il s'exécute dans un autre environnement d'exécution appelé SubstrateVM.

Il y a donc beaucoup de compromis ici, mais néanmoins, je pense que GraalVM a un grand potentiel, en particulier pour les applications basées sur le cloud où l'évolutivité et l'efficacité sont primordiales.

Commençons. Installez GraalVM. Vous pouvez le télécharger ici ou l'installer à l'aide de SDKManager. Pour installer des distributions Java, j'aime utiliser SDKManager. GraalVM est légèrement derrière les dernières versions de Java et prend actuellement en charge Java 8 et 11. La prise en charge de Java 14 ou 15 ou version ultérieure (quelle version sera là lorsque vous lirez ceci) est manquante.

Pour installer GraalVM pour Java 8, exécutez:

sdk install java 20.0.0.r8-grl

Je recommande d'utiliser Java 8 plutôt que Java 11, car il y a des erreurs obscures dans Java 11 que je n'ai pas encore découvert.

Après cela, vous devez installer le composant de création d'image native. Exécuter:

gu install native-image
gu- il s'agit d'un utilitaire de GraalVM.

Enfin, vérifiez ce qui JAVA_HOMEpointe vers GraalVM. Sur ma machine (Macintosh avec SDKMAN), la mienne JAVA_HOMEressemble à ceci:

export JAVA_HOME=$HOME/.sdkman/candidates/java/current/

Maintenant que vous avez tout configuré, jetons un œil à notre application. Accédez à Spring Initializr et générez un nouveau projet à l'aide de Lombok, R2DBC, PostgreSQL et Reactive Web.

Vous avez vu un code similaire un million de fois, donc je ne le démonterai pas, mais donnez-le ici.

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;
}

Vous pouvez voir le code complet ici .

La seule caractéristique de cette application est que nous utilisons l'attribut Spring Boot proxyBeanMethodspour nous assurer que l'application n'utilisera pas cglib et d'autres proxys autres que le JDK. GraalVM ne prend pas en charge les proxys non JDK. Bien que même avec le proxy JDK, vous devez le bricoler pour que GraalVM en sache plus. Cet attribut, nouveau dans Spring Framework 5.2, est en partie destiné à prendre en charge GraalVM.

Alors continuons. J'ai mentionné plus tôt que nous devons informer GraalVM de certains points qui peuvent se trouver dans notre application au moment de l'exécution et qu'il peut ne pas comprendre lors de l'exécution de code natif. Ce sont des choses comme la réflexion, les procurations, etc. Il y a plusieurs façons de procéder. Vous pouvez décrire la configuration manuellement et l'inclure dans l'assemblage. GraalVM le récupérera automatiquement. Une autre façon consiste à exécuter le programme avec un agent Java qui surveille ce que fait l'application et, une fois l'application terminée, écrit tout dans les fichiers de configuration, qui peuvent ensuite être transférés vers le compilateur GraalVM.

Vous pouvez également utiliser la fonction GraalVM. (Remarque Traducteur: «fonctionnalité» - le terme GraalVM désigne un plug-in pour la compilation native, créant un binaire natif à partir d'un fichier de classe ) . La fonction GraalVM est similaire à un agent Java. Il peut effectuer une sorte d'analyse et transmettre des informations au compilateur GraalVM. La fonctionnalité connaît et comprend le fonctionnement de l'application Spring. Elle sait quand les haricots de printemps sont des mandataires. Elle sait créer dynamiquement des classes en runtime. Elle sait comment Spring fonctionne, et elle sait ce que GraalVM veut, au moins la plupart du temps (après tout, c'est une version anticipée!)

Vous devez également configurer la build. Voici le mienpom.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>

Ici, nous prêtons attention au plugin native-image-maven-plugin. Il prend les paramètres via la ligne de commande, ce qui l'aide à comprendre ce qui doit être fait. Tous ces paramètres sont buildArgsnécessaires au démarrage de l'application. (Je dois exprimer ma profonde gratitude à Andy Clement, le responsable de la fonctionnalité Spring GraalVM, pour m'avoir aidé à trouver toutes ces options!)

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-graal-native</artifactId>
    <version>0.6.0.RELEASE</version>
</dependency>

Nous voulons que le compilateur GraalVM ait autant de façons que possible de fournir des informations sur le fonctionnement de l'application: agent java, fonction GraalVM, paramètres de ligne de commande. Tout cela ensemble donne à GraalVM suffisamment d'informations pour transformer avec succès l'application en un binaire natif compilé statiquement. À long terme, notre objectif est les projets du printemps. Et la fonction Spring GraalVM fournit tout ce dont vous avez besoin pour les prendre en charge.

Maintenant que nous avons tout configuré, créons une application:

  • Compilez l'application Java de la manière habituelle
  • Lancez une application Java avec un agent Java pour collecter des informations. À ce stade, nous devons nous assurer que l'application fonctionne. Vous devez parcourir tous les cas d'utilisation possibles. Au fait, c'est un très bon cas pour utiliser l'IC et les tests! Tout le monde parle constamment de tester l'application et d'améliorer les performances. Maintenant, avec GraalVM, vous pouvez faire les deux!
  • Reconstruisez ensuite l'application, cette fois avec le profil de graal actif, pour la compiler dans l'application native, en utilisant les informations collectées au premier démarrage.

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

Si tout s'est passé sans erreur, alors dans le répertoire, targetvous verrez une application compilée. Exécuter.

./target/com.example.reactive.reactiveapplication

L'application démarre, comme le montre une sortie similaire à celle-ci.

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)

Pas mal? Le générateur d'images natives GraalVM est idéal pour être associé à une plate-forme cloud comme CloudFoundry ou Kubernetes. Vous pouvez facilement assembler l'application dans un conteneur et l'exécuter dans le cloud avec un minimum de ressources.
Comme toujours, nous serons heureux de vous entendre. Cette technologie vous convient-elle? Des questions? Commentaires? Twitter (@springcentral) .



En savoir plus sur le cours

All Articles