Erste Erfahrungen mit Spring Boot Native

Geschrieben von:

Ronny Kissing

Ronny Kissing ist seit Gründung als Backend Entwickler bei der Finatix und ist damit ein Mann der ersten Stunde. Ronnys Fokus liegt auf den JVM Sprachen und auf der Entwicklung von Microservices in Spring Boot.

Spring Boot Native (3)

Dieser Fachartikel gibt einen ersten Eindruck in die Entwicklung mit Spring Boot Native. Anhand eines simplen Beispielprojektes werden die Vor- und Nachteile des Frameworks erläutert.

Zum Entwickeln von Microservices sind die JVM Sprachen Kotlin und Java, dank Spring Boot, sehr verbreitet. Sie sind gefragt und bieten eine einfache Möglichkeit, Restful Microservices zu schreiben. Allerdings haben sie einen Nachteil, sie benötigen die JVM (Java Virtual Machine) um ausgeführt werden zu können. Das bringt sowohl höhere Anforderungen im Resourcenverbrauch als auch Einbußen in der Performance gegenüber nativen Anwendungen mit sich.

Vor einiger Zeit tauchte mit GraalVM[1] eine Lösung auf, welche in der Lage ist, JVM Code in nativen umzuwandeln. Das verringert den Verbrauch an Ressourcen erheblich und erhöht die Performance deutlich.

Bis vor kurzem war die native Kompilierung von JVM Code mit GraalVM kompliziert. GraalVM musste auf dem Rechner installiert und konfiguriert werden, was einen gewissen Aufwand mit sich brachte bzw. bringt, wenn es direkt verwenden werden soll.

Das ändert sich mit Spring Boot Native. Spring Boot Native wird ab Spring Boot 2.5.x unterstützt und kann direkt über den Spring Initialer in ein neues Projekt integriert werden. Es spielt dabei keine Rolle, welche JVM Sprache man bei der Entwicklung verwendet. Einmal angelegt, ist das Projekt einfach verwendbar.

Aktuell ist Spring Boot Native noch experimentell, was sich aber wahrscheinlich in nicht allzu ferner Zukunft ändern wird. Dadurch kommt es erstmal zu Einschränkungen, auf die ich später noch eingehen möchte.

Voraussetzungen um Spring Boot Native verwenden zu können sind:

  • Docker
  • Der User, welcher Spring Boot Native verwenden möchte, benötigt Berechtigungen für Docker (Unter Linux sollte der User der Gruppe „docker“ angehören)
  • Java, zum Zeitpunkt 1.11.2021 werden alle Version einschließlich Java 16 unterstützt
  • Gradle oder Maven
  • Im Idealfall IntelliJ IDEA

Um ein erstes Projekt zum Kennenlernen zu erstellen, bietet sich der Spring Initializr an. Zur Verwendung gibt es zwei Möglichkeiten.

Möglichkeit A: Er kann entweder über die WebSite https://start.spring.io/ aufgerufen werden

Spring Initializr

Möglichkeit B: Man verwendet den Initializr direkt in der IDEA.

IDEA-1
IDEA-2

Ein solches Projekt bringt über ein in Gradle konfiguriertes Modul bereits alles mit, was man für ein natives Kompilat benötigt. Für das Erstellen und native Kompilieren wird ein Build Docker Container heruntergeladen und verwendet. Das Ergebnis wiederum ist ebenfalls ein Docker Container, in welchem der Microservice läuft.

Um das ganze noch weiter zu vereinfachen: Alles ist über einen Gradle Task ausführbar und es muss sich nicht um die Randbedigungen bzw. Voraussetzungen gekümmert werden. Die einzige Voraussetzung ist eine funktionierende Docker Installation und die korrekt gesetzten Berechtigungen für den User, der den Build erstellen möchte.

Nachdem der eigentliche Build für das Jar Archiv durchgelaufen ist, verwendet man den Task „bootBuildImage“, um das Docker Image mit dem nativen Kompilat zu erstellen.

IDEA-3

Das über den Initializr entstandene Gradle Projekt enthält die gesamte Konfiguration um das native Docker Image zu erstellen und direkt zu starten. Man kann also mit der Entwicklung des Microservice loslegen. Führt man die Gradle Tasks:

  • clean
  • build
  • bootBuildImage

aus, kann man sich anschließend in einer Bash das Docker Image anzeigen lassen und es starten. Nehmen wir an, im Beispiel wurde ein „Hello World“ Endpunkt integriert, so kann dieser direkt aufgerufen werden.

docker-start-native-container

Anschließend ist der Aufruf mittel cURL problemlos möglich:

curl-request-3

Vergleicht man zwei Docker Container miteinander, in denen einmal ein natives Kompilat und einmal ein JVM basierter Microservice läuft, stellt man schnell fest, dass der Container mit dem nativen Kompilat wesentlich kleiner als der mit der JVM ist. Man spart hier bereits eine Menge Platz ein, was beispielsweise bei embedded Hardware sehr wertvoll sein kann.

Vergleichen wir die Containergrößen:

docker-images-new

so sieht man bereits einen Unterschied in der Größe. Der native Container ist deutlich leichtgewichtiger. Einen weiteren Unterschied sieht man in der Performance, wenn man den Start beider Container vergleicht.

docker-start-jvm-container-marker-new
docker-start-native-container-marker-new

Der native Container startet in 0.064 Sekunden, während der Container mit der JVM 1.189 Sekunden für den Start benötigt. Das erscheint erstmal nicht viel, ändert sich jedoch mit der Komplexität eines Microservice sehr schnell. In diesem Beispiel handelt es sich lediglich um einen kleinen „Hallo Welt“ Microservice zur Demonstration.

Nachdem nun einige Vorteile beleuchtet wurden, dürfen die Nachteile natürlich nicht fehlen. Davon hat Spring Boot Native aktuell leider noch viele. Durch das Kompilieren im Docker Container fällt ein zusätzlicher Nachteil auf: Die benötigte Zeit um das native Image zu erstellen beläuft sich aktuell auf etwa 5 Minuten. Um sicherzustellen, dass die aktuelle Implementierung auch wirklich mit Spring Boot native funktioniert, sollte man dies ungeachtet der Build-Zeit allerdings regelmäßig tun. Während der Entwicklung selbst gibt es vieles, was man bedenken und beachten muss.

So unterstützt GraalVM, und somit auch Spring Boot Native, keine Reflections. Es muss sichergestellt werden, dass keine Instanzen zur Laufzeit angelegt, sondern alle Beans bereits beim Start vorhanden sind. Ebenfalls ist darauf zu achten, dass keine Dependencies ins Projekt aufgenommen werden, die in irgendeiner Weise Reflections verwenden. Ein paar Beispiele hierfür sind:

  • Apache POI
  • Spring Boot Admin
  • SLF4J

Herausfinden, welche Dependencies mit Spring Boot Native kompatibel sind, ist zu diesem Zeitpunkt schwierig. Es gibt für Java Dependencies keine Kompatibilitätsliste und keine Möglichkeit, diese schnell zu prüfen. Bleibt nur ein manueller Test übrig, um herauszufinden, wie es um die Kompatiblität steht.

Eine gute Anlaufstelle um sich über Reflections und Spring Boot Native zu informieren ist die folgende Seite: https://www.graalvm.org/reference-manual/native-image/Limitations/

Ebenfalls nicht möglich bzw. problematisch ist es, statischen Content, etwa Websites, zu integrieren. Es kann lediglich der kompilierbare Microservice an sich verwendet werden.

Als Ausblick möchte ich gern mitgeben, dass es trotz der aktuellen Einschränkungen ein interessantes und wirklich nützliches Feature ist. In absehbarer Zeit werde ich das Thema erneut aufgreifen und neue Erkenntnisse teilen.