Spring Native Hands-on

Native-Images für Spring-Boot-Anwendungen erzeugen
5
Jul

Spring Native Hands-on

Mit dem neuen Projekt Spring Native können Spring-Boot-Anwendungen von der GraalVM-Native-Image-Technologie Gebrauch machen und auch für existierende Spring-Boot-Anwendungen Start-up-Zeiten im Millisekundenbereich erzielen. Der Artikel zeigt, wie das funktioniert, wie weit Spring Native schon ist, und wie man die Technologie für eigene Spring-Boot-Anwendungen einsetzen kann.

Die Vorteile der GraalVM-Native-Image-Technologie klingen verlockend: Start-up-Zeiten im Millisekundenbereich und ein deutlich reduzierter Verbrauch an Ressourcen (vor allem Speicher) – wer möchte das nicht?

Jedoch kommt diese Technologie mit einer Reihe von Einschränkungen daher. Reflection funktioniert beispielsweise in einem Native Image nur, wenn der Compiler darüber informiert wird, für welche Elemente (Klassen, Methoden, Attribute) er die Reflection-Informationen zur Compile-Zeit erzeugen und im Binary hinterlegen muss. Ähnliches gilt für Proxys, zusätzliche Ressourcen, JNI-Aufrufe und Dynamic Class Loading. Andere Techniken, wie zum Beispiel invokedynamic, funktionieren in einem Native Image grundsätzlich nicht.

Insofern kann es eine erhebliche Herausforderung sein, eine existierende Java-Anwendung in ein Native Image zu kompilieren. Zum einen muss der Code der eigenen Anwendung frei von nicht unterstützten Techniken sein, und zum anderen müssen passende Konfigurationsdateien erstellt werden, um beispielsweise Reflection zu ermöglichen. Gleiches gilt natürlich auch für alle von der eigenen Anwendung genutzten Libraries.

Was ist mit Spring-Boot-Anwendungen?

Auch für Spring-Boot-Anwendungen gilt: Sie lassen sich mit der GraalVM-Native-Image-Technologie in native Anwendungen kompilieren. Allerdings verwendet das Spring Framework viele der eben genannten Technologien relativ ausgiebig, sodass es mitunter mühsam werden kann, die nötigen Konfigurationen für den Compiler manuell zu erstellen. Grundsätzlich ist das aber möglich.

Was ist Spring Native?

Das Spring-Native-Projekt [1] ermöglicht es Entwicklern, Spring-Boot-Anwendungen mit der GraalVM-Native-Image-Technologie in Executable Binaries zu kompilieren, ohne dass die nötigen Konfigurationsdateien manuell erstellt werden müssen oder die Anwendung speziell angepasst werden muss. Im Idealfall lassen sich also bestehende Spring-Boot-Anwendungen ausschließlich durch wenige zusätzliche Build-Instruktionen zu Native Executables kompilieren (Kasten: „In drei einfachen Schritten zur fertigen Anwendung“).

In drei einfachen Schritten zur fertigen Anwendung


  • Projekt auf https://start.spring.io erzeugen (Spring Web | Spring Native) und auspacken.

  • ./mvnw spring-boot:build-image (Build ausführen, Native Image wird kompiliert, Container-Image wird erzeugt, benötigt nur Docker)

  • docker run –rm -p 8080:8080 demo:0.0.1-SNAPSHOT (Beispielanwendung starten)

Ob es Sinn ergibt, jede Spring-Boot-Anwendung zu einem Native Executable zu kompilieren, anstatt die Anwendung in einer JVM laufen zu lassen, sei einmal dahingestellt. Diese Entscheidung hat weniger mit Spring Boot selbst zu tun als vielmehr mit dem Einsatzkontext der Anwendung.

Erste Schritte mit Spring Native

Wie beginnt man neue Spring-Boot-Projekte? Natürlich auf https://start.spring.io (bzw. den entsprechenden Wizards in der eigenen Lieblings-IDE).

<dependency>
  <groupId>org.springframework.experimental</groupId>
  <artifactId>spring-native</artifactId>
  <version>0.9.2</version>
</dependency>
<plugin>
  <groupId>org.springframework.experimental</groupId>
  <artifactId>spring-aot-maven-plugin</artifactId>
  <version>0.9.2</version>
  <executions>
    <execution>
      <id>test-generate</id>
      <goals>
        <goal>test-generate</goal>
      </goals>
    </execution>
    <execution>
      <id>generate</id>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <image>
      <builder>paketobuildpacks/builder:tiny</builder>
      <env>
        <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
      </env>
    </image>
  </configuration>
</plugin>

Als Beispiel wähle ich hier die Starter Spring Web, Spring Boot Actuator und eben Spring Native aus. Das generierte Projekt hat dann drei verschiedene Komponenten in der pom.xml-Datei, die speziell für Spring Native hinzugefügt wurden:

  1. eine zusätzliche Dependency (Listing 1)

  2. ein Build-Plug-in, das zusätzliche Informationen zur Build-Zeit erzeugt (Listing 2)

  3. ein Build-Plug-in, um ein Container-Image zu erzeugen (Listing 3)

Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

Konfigurationen automatisch erzeugen

Die zusätzliche Dependency spring-native (aus Listing 1) beinhaltet vor allem die Spring-spezifische Erweiterung für den GraalVM-Native-Image-Compiler. Diese Erweiterung wird automatisch vom GraalVM-Native-Image-Compiler als Teil des Native-Image-Build-Prozesses ausgeführt.

Das neuartige Spring-AOT-Plug-in für den Build erzeugt die für das Native Image nötigen Konfigurationen automatisch während des Build-Vorgangs. Inhaltlich analysiert diese Build-Erweiterung die zu kompilierende Anwendung auf verwendete Spring-Komponenten und -Annotationen. Je nachdem, welche Spring-Bibliotheken und -Annotationen in der Anwendung verwendet werden, erzeugt Spring Native die passenden Konfigurationsdateien für den GraalVM-Native-Image-Compiler, sodass diese nicht manuell erstellt werden müssen.

Darüber hinaus kann die Spring-AOT-Erweiterung auch mit vielen Schaltern konfiguriert werden, um das Native Image noch genauer auf die eigenen Bedürfnisse zuzuschneiden. Beispielsweise lassen sich diverse Features von Spring komplett ausschalten und somit der dafür benötigte Code komplett aus dem Native Image entfernen.

Der etwas in die Jahre gekommene Support für Spring-XML-Config-Dateien ist ein gutes Beispiel dafür. Verwendet die Anwendung überhaupt keine Spring-XML-Config-Dateien, kann mit dieser Option der komplette XML-Support von Spring inkl. der dazu benötigten Dependencies gar nicht erst in das Native Image hineinkompiliert werden.

Die Spring-AOT-Erweiterung erlaubt es darüber hinaus, dem Native-Image-Support eigene sogenannte Hints mitzugeben. Diese „Hinweise“ geben dem Spring-Native-Support genaue Informationen darüber mit, welche Zusatzinformationen (beispielsweise zu Reflection) benötigt werden – sollten diese nicht automatisch identifiziert werden können.

Ein Beispiel dafür sind eigene Klassen, auf die zum Beispiel eine Library per Reflection zugreift, um sie in JSON zu transformieren.

Container-Images mit Native Executables

Spring Boot bringt schon seit einigen Versionen ein Maven-Build-Plug-in mit, welches automatisch ein Container-Image für die gebaute Spring-Anwendung erzeugt. Dieses Maven-Build-Plug-in nutzt im Hintergrund die Paketo Buildpacks [2], um aus kompilierten Spring-Boot-Anwendungen fertige Container-Images zu erzeugen.

Dieses Maven-Build-Plug-in (spring-boot-maven-plugin) kann so konfiguriert werden, dass vollautomatisch der GraalVM-Native-Image-Compiler verwendet und ein Native Executable erzeugt wird, welches dann in das Container-Image gelegt wird (anstatt eines JREs und den JAR-Dateien der Dependencys und der Anwendung selbst) – siehe Listing 3.

Ein großer Vorteil dieser Buildpack-basierten Methode ist, dass auf der lokalen Maschine kein passendes GraalVM SDK und keine Native-Image-Erweiterung installiert werden muss. Es reicht aus, die entsprechende Konfiguration (Listing 3) in die pom.xml-Datei zu integrieren und den Build auszuführen:

./mvnw spring-boot:build-image

Das Buildpack bringt das nötige GraalVM SDK automatisch mit. Das Resultat ist ein relativ kleines Container-Image. Es enthält weder ein vollständiges JRE noch die kompletten JAR-Dateien, sondern hauptsächlich das Binary der Anwendung.

Die eigentliche Größe des Binarys und dessen Speicherverbrauch im Betrieb hängt stark davon ab, wie gut und exakt zugeschnitten der Native-Image-Compiler konfiguriert wird. Je mehr Reflection-Informationen man beispielsweise konfiguriert, desto größer wird auch das Binary und desto mehr Speicher verbraucht es. Es kann sich also durchaus lohnen, möglichst wenig und möglichst genaue Reflection-Informationen zu konfigurieren, anstatt pauschal einfach alles.

Das gleiche gilt auch für die Erreichbarkeit von Code. Je genauer der Native-Image-Compiler analysieren kann, welcher Code nicht gebraucht wird, desto mehr Code wird er bei der Kompilierung des Binarys entfernen und desto weniger Ressourcen wird das Binary im Betrieb verbrauchen.

Sobald der Build das Container-Image mit dem Native Binary erzeugt hat, können wir den Container per Docker starten:

docker run --rm -p 8080:8080 rest-service:0.0.1-SNAPSHOT

Im Logoutput werden wir sehen: Die Spring-Boot-Anwendung startet innerhalb des Containers in wenigen Millisekunden.

Native Images lokal erzeugen

Ein Native Executable für eine Spring-Boot-Anwendung lässt sich auch ohne Buildpacks erzeugen. Wie im Artikel über die Native-Image-Technologie beschrieben, benötigt man dazu ein GraalVM SDK mit installierter Native Image Extension.

Anschließend lässt sich das GraalVM-Maven-Plug-in dem Build hinzufügen und passend für den Native-Image-Compiler konfigurieren (Listing 4). Zusätzlich sollte man in diesem Profil das Standardverhalten des Spring-Boot-Maven-Plug-ins leicht verändern (Listing 5), um einen Konflikt mit dem Repackaged JAR des Standard-Spring-Boot-Maven-Plug-ins zu vermeiden.

<profiles>
  <profile>
    <id>native-image</id>
    <build>
      <plugins>
        <plugin>
          <groupId>org.graalvm.nativeimage</groupId>
          <artifactId>native-image-maven-plugin</artifactId>
          <version>21.0.0.2</version>
          <configuration>
            <!-- The native image build needs to know the entry point to your application -->
            <mainClass>com.example.restservice.RestServiceApplication</mainClass>
          </configuration>
          <executions>
            <execution>
              <goals>
                <goal>native-image</goal>
              </goals>
              <phase>package</phase>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>
<plugins>
  <!-- ... -->
  <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
      <classifier>exec</classifier>
    </configuration>
  </plugin>
</plugins>

Auch für das lokal erzeugte Native Image muss die bereits erwähnte Spring Native Dependency ergänzt werden (Listing 1) sowie die Spring-AOT-Erweiterung (Listing 2). Diese beiden Erweiterungen im Build sind also für beide Varianten (Buildpack und lokaler Build) wichtig und sinnvoll.

Der Build wird dann (im hier beschriebenen Beispiel) ausgeführt mit:

./mvnw -Pnative-image package

Daraufhin wird im target-Directory das Native Executable abgelegt, das direkt ausgeführt werden kann:

./target/demo

Dieser lokale Native-Image-Compile-Schritt läuft direkt auf der eigenen Maschine ab und verwendet das native-image-Kommando des lokal installierten GraalVM-SDKs. Führt man diesen Build also beispielsweise auf einer Windows-Maschine aus, wird ein Windows Binary erzeugt. Das ist ein bedeutender Unterschied zur Buildpack-basierten Native-Image-Kompilierung. Das Buildpack erzeugt ein Linux-basiertes Container-Image, in dem das Native Image erzeugt wird und per Docker-Runtime ausgeführt werden kann.

Die Roadmap

Die nächsten Schritte für das Spring-Native-Projekt sind zum einen, stetig weiter den Ressourcenverbrauch über die unterschiedlichsten Projekte und Bibliotheken zu reduzieren. Aktuell lassen sich zwar schon recht viele Spring-Boot-Starter-Module mit Spring Native verwenden, aber nicht alle sind schon komplett auf Speicherverbrauch und Performance optimiert. Hier liegt noch einige Arbeit vor dem Spring-Team.

Darüber hinaus arbeitet eine Reihe von Projekten daran, möglichst viel von Spring Native zu unterstützen und automatisch zur Build-Zeit zu erzeugen. Auch hier sind viele Verbesserungen zu erwarten.

Nicht zuletzt werden mit den nächsten Releases auch kontinuierlich mehr Spring-Boot-Starter-Module und deren Dependencys unterstützt werden. Die aktuelle Liste der unterstützten Module kann man in der Dokumentation einsehen [3].

Fazit

Spring Native kann für Spring-Boot-Entwickler zu einem echten Gamechanger werden. Mit Spring Native werden Entwickler von Spring-Boot-Anwendungen in die Lage versetzt, alle Vorteile der GraalVM-Native-Image-Technologie zu nutzen, ohne die Spring-Boot-Anwendungen speziell dafür zu modifizieren oder gar auf ein anderes Framework zu portieren. Existierende und bereits seit Jahren in der Entwicklung und im Einsatz befindliche Spring-Boot-Anwendungen können mit Spring Native von der neuen GraalVM-Native-Image-Technologie profitieren – und so unter Umständen erhebliche Ressourcen einsparen.

Ohne Frage, das Spring-Native-Projekt steht noch ziemlich am Anfang. Es lassen sich noch nicht alle Spring Boot Starter damit nutzen und auch von den unterstützten Projekten sind noch nicht alle komplett für diesen Einsatz optimiert. Aber die Arbeit an dem Projekt geht mit großen Schritten voran und das Ziel ist extrem vielversprechend.

Links & Literatur

[1] Spring Native: https://github.com/spring-projects-experimental/spring-native

[2] Paketo Buildpacks und Spring Boot: https://spring.io/blog/2021/01/04/ymnnalft-easy-docker-image-creation-with-the-spring-boot-maven-plugin-and-buildpacks + https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-container-images-buildpacks

[3] https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/#support-spring-boot

Alle News der Java-Welt:
Alle News der Java-Welt:

Behind the Tracks

Agile & Culture
Teamwork & Methoden

Data Access & Machine Learning
Speicherung, Processing & mehr

Clouds, Kubernets & Serverless
Alles rund um Cloud

Core Java & JVM Languages
Ausblicke & Best Practices

DevOps & Continuous Delivery
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Web Development & JavaScript
JS & Webtechnologien

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Software-Architektur
Best Practices

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management