Wir kompilieren die Spring Boot-Anwendung mit GraalVM in native

Vor Beginn des Kurses "Developer on the Spring Framework" wurde eine Übersetzung des Artikels erstellt .



Hallo Frühlingsliebhaber! Willkommen zur nächsten Version von Spring Tips. Heute werden wir über die kürzlich implementierte Unterstützung für das Kompilieren von Spring Boot-Anwendungen in GraalVM sprechen. Wir haben bereits in einer anderen Spring Tips-Version zum Thema Spring Fu über GraalVM und native Anwendungen gesprochen.



Erinnern wir uns, was GraalVM ist. GraalVM ist ein Ersatz für den Standard-C1-Compiler in OpenJDK. Weitere Informationen zur Verwendung von GraalVM finden Sie in meinem Bootiful Podcast-Podcast mit Chris Thalinge, einem GraalVM-Mitarbeiter und Twitter-Ingenieur. Mit GraalVM können Sie unter bestimmten Umständen reguläre Spring-Anwendungen schneller ausführen, und zumindest aus diesem Grund verdient es Aufmerksamkeit.

Aber wir werden nicht darüber reden. Wir werden uns andere GraalVM-Komponenten ansehen: den nativen Image Builder und SubstrateVM. Mit SubstrateVM können Sie native ausführbare Dateien für Ihre Java-Anwendung erstellen. Über diese und andere Anwendungen von GraalVM gab es übrigens einen Podcast mit Oleg Shelaev von Oracle Labs. Der native Image Builder ist ein Test für Kompromisse. Wenn Sie GraalVM genügend Informationen zum Verhalten Ihrer Anwendung zur Laufzeit bereitstellen (dynamisch verknüpfte Bibliotheken, Reflection, Proxys usw.), kann Ihre Java-Anwendung in eine statisch verknüpfte Binärdatei wie eine C- oder Golang-Anwendung umgewandelt werden. Ehrlich gesagt kann dieser Prozess ziemlich schmerzhaft sein. Wenn Sie dies jedoch tun, können Sie nativen Code generieren, der unglaublich schnell ist. Infolgedessen benötigt die Anwendung viel weniger RAM und wird in weniger als einer Sekunde ausgeführt. Weniger als eine Sekunde. Ziemlich verlockend, nicht wahr? Na sicher!

Es ist jedoch zu beachten, dass einige Punkte berücksichtigt werden müssen. Die resultierenden GraalVM-Binärdateien sind keine Java-Anwendungen. Sie laufen nicht einmal auf einer regulären JVM. GraalVM wird von Oracle Labs entwickelt und es gibt eine Art Interaktion zwischen den Java- und GraalVM-Teams, aber ich würde es nicht Java nennen. Die resultierende Binärdatei ist nicht plattformübergreifend. Die Anwendung verwendet die JVM nicht. Es läuft in einer anderen Laufzeit namens SubstrateVM.

Daher gibt es hier viele Kompromisse, aber ich denke dennoch, dass GraalVM ein großes Potenzial hat, insbesondere für Cloud-basierte Anwendungen, bei denen Skalierung und Effizienz von größter Bedeutung sind.

Lasst uns beginnen. Installieren Sie GraalVM. Sie können es hier herunterladen oder mit SDKManager installieren. Für die Installation von Java-Distributionen verwende ich gerne SDKManager. GraalVM liegt etwas hinter den neuesten Versionen von Java und unterstützt derzeit Java 8 und 11. Die Unterstützung für Java 14 oder 15 oder höher (welche Version wird verfügbar sein, wenn Sie dies lesen) fehlt.

Führen Sie Folgendes aus, um GraalVM für Java 8 zu installieren:

sdk install java 20.0.0.r8-grl

Ich empfehle die Verwendung von Java 8 anstelle von Java 11, da es in Java 11 einige obskure Fehler gibt, die ich noch nicht herausgefunden habe.

Danach müssen Sie die native Image Builder-Komponente installieren. Ausführen:

gu install native-image
gu- Dies ist ein Dienstprogramm von GraalVM.

Überprüfen Sie JAVA_HOMEabschließend , welche Punkte auf GraalVM verweisen. Auf meinem Computer (Macintosh mit SDKMAN) JAVA_HOMEsieht mein Computer folgendermaßen aus:

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

Nachdem Sie alles eingerichtet haben, werfen wir einen Blick auf unsere Anwendung. Gehen Sie zu Spring Initializr und generieren Sie ein neues Projekt mit Lombok, R2DBC, PostgreSQL und Reactive Web.

Sie haben einen ähnlichen Code millionenfach gesehen, daher werde ich ihn nicht zerlegen, sondern nur hier angeben.

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

Den vollständigen Code finden Sie hier .

Die einzige Funktion dieser Anwendung besteht darin, dass wir das Spring Boot-Attribut verwenden, proxyBeanMethodsum sicherzustellen, dass die Anwendung keine cglib und andere Proxys als das JDK verwendet. GraalVM unterstützt keine Nicht-JDK-Proxys. Obwohl Sie selbst mit dem JDK-Proxy daran basteln müssen, damit GraalVM davon erfährt. Dieses in Spring Framework 5.2 neue Attribut soll teilweise GraalVM unterstützen.

Also, lass uns weitermachen. Ich habe bereits erwähnt, dass wir GraalVM über einige Punkte informieren müssen, die zur Laufzeit in unserer Anwendung enthalten sein können, und dass dies bei der Ausführung von nativem Code möglicherweise nicht verstanden wird. Dies sind Dinge wie Reflexion, Proxies usw. Es gibt verschiedene Möglichkeiten, dies zu tun. Sie können die Konfiguration manuell beschreiben und in die Baugruppe aufnehmen. GraalVM nimmt es automatisch auf. Eine andere Möglichkeit besteht darin, das Programm mit einem Java-Agenten auszuführen, der überwacht, was die Anwendung tut, und nach Abschluss der Anwendung alles in die Konfigurationsdateien schreibt, die dann an den GraalVM-Compiler übertragen werden können.

Sie können auch die GraalVM-Funktion verwenden. (Hinweis Übersetzer: „Feature“ - der Begriff GraalVM bezeichnet eine Plug-in für die native Kompilierung, eine native binary aus einer Klasse - Datei erstellen ) . Die GraalVM-Funktion ähnelt einem Java-Agenten. Es kann eine Art Analyse durchführen und Informationen an den GraalVM-Compiler übergeben. Feature weiß und versteht, wie die Spring-Anwendung funktioniert. Sie weiß, wann Frühlingsbohnen Stellvertreter sind. Sie weiß, wie man zur Laufzeit dynamisch Klassen erstellt. Sie weiß, wie Spring funktioniert, und sie weiß zumindest die meiste Zeit, was GraalVM will (schließlich ist dies eine frühe Version!).

Sie müssen auch den Build konfigurieren. Hier ist meinspom.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>

Hier achten wir auf das Plugin native-image-maven-plugin. Er nimmt Parameter über die Befehlszeile, die ihm helfen zu verstehen, was zu tun ist. Alle diese Parameter sind buildArgserforderlich, damit die Anwendung gestartet werden kann. (Ich muss Andy Clement, dem Spring GraalVM Feature Maintainer, meinen tiefen Dank dafür aussprechen, dass er mir dabei geholfen hat, all diese Optionen herauszufinden!)

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

Wir möchten, dass der GraalVM-Compiler so viele Möglichkeiten wie möglich hat, um Informationen über die Funktionsweise der Anwendung bereitzustellen: Java-Agent, GraalVM-Funktion, Befehlszeilenparameter. All dies zusammen gibt GraalVM genügend Informationen, um die Anwendung erfolgreich in eine statisch kompilierte native Binärdatei umzuwandeln. Langfristig ist unser Ziel Frühlingsprojekte. Die Spring GraalVM-Funktion bietet alles, was Sie zur Unterstützung benötigen.

Nachdem wir alles eingerichtet haben, stellen wir eine Anwendung zusammen:

  • Kompilieren Sie die Java-Anwendung wie gewohnt
  • Starten Sie eine Java-Anwendung mit einem Java-Agenten, um Informationen zu sammeln. In dieser Phase müssen wir sicherstellen, dass die Anwendung funktioniert. Sie müssen alle möglichen Anwendungsfälle durchgehen. Dies ist übrigens ein sehr guter Fall für die Verwendung von CI und Tests! Alle reden ständig davon, die Anwendung zu testen und die Leistung zu verbessern. Mit GraalVM können Sie jetzt beides tun!
  • Erstellen Sie dann die Anwendung neu, diesmal mit dem aktiven Graal-Profil, um sie unter Verwendung der beim ersten Start gesammelten Informationen in die native Anwendung zu kompilieren.

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

Wenn alles fehlerfrei gelaufen ist, sehen Sie im Verzeichnis targeteine kompilierte Anwendung. Starte es.

./target/com.example.reactive.reactiveapplication

Die Anwendung wird gestartet, wie aus einer ähnlichen Ausgabe hervorgeht.

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)

Nicht schlecht? Der native GraalVM-Image-Builder eignet sich hervorragend für die Kombination mit einer Cloud-Plattform wie CloudFoundry oder Kubernetes. Sie können die Anwendung einfach zu einem Container zusammenfügen und mit minimalen Ressourcen in der Cloud ausführen.
Wie immer freuen wir uns, von Ihnen zu hören. Ist diese Technologie für Sie geeignet? Fragen? Bemerkungen? Twitter (@springcentral) .



Erfahren Sie mehr über den Kurs

All Articles