Spring-Boot-3-Anwendung auf AWS Lambda – Teil 5

GraalVM Native Image mit Spring Cloud

GraalVM Native Image mit Spring Cloud

Spring-Boot-3-Anwendung auf AWS Lambda – Teil 5

GraalVM Native Image mit Spring Cloud


Bisher haben wir die Konzepte hinter dem AWS Serverless Java Container Framework und dem AWS Lambda Web Adapter Tool kennengelernt, uns mit Spring Cloud Function beschäftigt, eine Spring-Boot-3-Anwendung auf AWS Lambda damit entwickelt, sie mit der verwalteten Java-21-Lambda-Laufzeitumgebung und dem benutzerdefinierten Docker Image bereitgestellt und jeweils die Performance der Lambda-Funktion gemessen und optimiert [1], [2], [3], [4]. Diesmal wollen wir unsere Anwendung als Custom Runtime mit GraalVM Native Image umsetzen.

Dieser Artikel setzt Vorkenntnisse über GraalVM und dessen Native-Image-Fähigkeiten voraus. Einen kompakten Überblick darüber und wie man beides installiert, finden Sie unter [5], [6], [7].

Zur Erläuterung verwenden wir unsere Beispielanwendung mit Spring Cloud Function und ihren AWS-Lambda-Adapter aus dem dritten Teil dieser Serie; Abbildung 1 zeigt die Architekturskizze [3], [8].

kazulkin_spring_5_1

Abb 1: Architektur der Beispielanwendung

Da in der Zwischenzeit Spring Boot Version 3.4 und GraalVM 23 veröffentlicht wurden, aktualisieren wir unsere Anwendung auf diese Versionen. Für unser Szenario, die Anwendung als GraalVM Native Image zu bauen und sie als AWS Lambda Custom Runtime bereitzustellen, nehmen wir ein paar weitere Anpassungen vor, die wir im Folgenden erläutern. Die finale Variante ist unter [9] zu finden.

Spring Boot 3 bietet seit der Version 3.0 direkten Spring Boot GraalVM Native Image Support [10]. Damit unsere Beispielanwendung als GraalVM Native Image läuft, müssen wir alle Klassen deklarieren, deren Objekte per Reflection instanziiert werden sollen. Diese Klassen müssen dem AOT-Compiler zur Kompilierzeit bekannt sein. Dafür existieren mehrere Optionen, die unter [11] beschrieben sind. Wir werden diese Anforderung mit der Spring-AOT-Unterstützung von Spring Boot in der eigens geschriebenen Klasse ApplicationConfiguration umsetzen [12].

Im folgenden Code sind die Klassen, die zur Laufzeit per Reflection instanziiert werden, mittels der Annotation @RegisterReflectionForBinding deklariert. Dazu gehören eigene Entitätenklassen wie Product und Products, einige AWS-Abhängigkeiten zum API Gateway Proxy Event Request (aus der ArtifactId aws-lambda-java-events aus pom.xml), die DateTime-Klasse für die Konvertierung des Zeitstempels von JSON in ein Java-Objekt und einige andere Klassen. Manchmal sind mehrere Versuche nötig, damit die Applikation läuft, und falls wir zur Laufzeit die ClassNotFound Exception bekommen, ist das ein Indiz dafür, dass die Klasse in die Liste derjenigen aufgenommen werden muss, die mit der Annotation @RegisterReflectionForBinding deklariert sind.

@RegisterReflectionForBinding({DateTime.class, HashSet.class, APIGatewayProxyRequestEvent.class , APIGatewayProxyRequestEvent.ProxyRequestContext.class, APIGatewayProxyRequestEvent.RequestIdentity.class, Product.class, Products.class})

Im Listing 1 haben wir einen benutzerdefinierten RuntimeHintsRegistrar als innere Klasse von ApplicationConfiguration implementiert, der es uns ermöglicht, öffentliche Methoden aufzurufen, öffentliche Felder zu setzen und Konstruktoren für unsere Entitätsobjekte (Product und Products) aufzurufen.

Listing 1

public static class ApplicationRuntimeHintsRegistrar implements RuntimeHintsRegistrar {

  @Override
  public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
    hints.reflection()
      .registerType(
        Product.class,
PUBLIC_FIELDS, INVOKE_PUBLIC_METHODS, INVOKE_PUBLIC_CONSTRUCTORS
      ).registerType(
        Products.class,
PUBLIC_FIELDS, INVOKE_PUBLIC_METHODS, INVOKE_PUBLIC_CONSTRUCTORS
      );
  }
}

Dieser Registrar muss in der Klasse ApplicationConfiguration mit der Annotation @ImportRuntimeHints importiert werden. Für unseren Fall sieht das so aus: @ImportRuntimeHints(ApplicationConfiguration.ApplicationRuntimeHintsRegistrar.class). Die ganzen Registrar- und Hint-Abhängigkeiten sind dem von Spring Boot bereitgestellten AOT-Support zuzuschreiben [13].

Die einzige mir bekannte Möglichkeit, eine Lambda-Funktion als GraalVM Native Image bereitzustellen, ist, sie als Custom Runtime zu deployen [14]. Dafür müssen wir alles Notwendige für die Ausführung in ein Deployment-Artefakt als ZIP-Datei verpacken, die die Datei bootstrap enthält. Sie kann entweder das GraalVM Native Image oder Anweisungen enthalten, wie das Image aus einer anderen Datei aufgerufen werden kann.

Im Folgenden sehen wir, wie das GraalVM Native Image erstellt werden kann. Wir werden es mit Hilfe von Plug-ins und Profilen erstellen, die in pom.xml definiert sind [15]. Zuallererst definieren wir dort das native Profil wie in Listing 2.

Listing 2

<profiles>
  <profile>
    <id>native</id>
    <activation>
      <property>
        <name>native</name>
      </property>
    </activation>
...
  </profile>
</profiles>

Weiter ist es notwendig, Spring AOT auszuführen. Dabei handelt es sich um einen Prozess, der unsere Anwendung zur Build-Zeit analysiert und eine optimierte Version davon erzeugt. Es ist ein obligatorischer Schritt, um einen Spring ApplicationContext in einem nativen Image auszuführen. Um unsere Anwendung so zu konfigurieren, dass sie diese Funktion nutzt, müssen wir das Plug-in im Build-Abschnitt der pom.xml definieren und ein Ausführungsziel process-aot hinzufügen (Listing 3). So wird es vom Spring-Boot-AOT-Maven-Plug-in empfohlen [16].

Listing 3

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>process-aot</id>
      <goals>
        <goal>process-aot</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Als letzten Schritt müssen wir ein weiteres Plug-in, das native-image-maven-plugin, in der Build-Sektion von pom.xml definieren (Listing 4).

Listing 4

<plugin>
  <groupId>org.graalvm.buildtools</groupId>
  <artifactId>native-maven-plugin</artifactId>
  <configuration>
    <mainClass>software.amazonaws.Application</mainClass>
    <buildArgs>
      --enable-url-protocols=http
      -H:+AddAllCharsets
    </buildArgs>
  </configuration>
  <executions>
    <execution>
      <id>build-native</id>
      <goals>
        <goal>compile-no-fork</goal>
      </goals>
      <phase>package</phase>
    </execution>
  </executions...