Seit knapp einem Jahrzehnt schreibe ich nun serverseitige Anwendungen mit Java. Auch der aktuelle Trend rund um Microservices hat dem keinen Abbruch getan. Anstatt einer einzelnen großen Anwendung entwickle und warte ich nun mehrere kleine. Natürlich haben sich die Bibliotheken und Frameworks weiterentwickelt, aber im Großen und Ganzen hat sich für mich nicht viel verändert.
Ich sehe allerdings schon, dass sich durch neue Paradigmen wie Functions as a Service (FaaS) und die Verbreitung von Containern und Kubernetes die Anforderungen an moderne, für die Cloud ausgelegte Anwendungen ändern. Die Eigenschaften, auf die sich Java – und die JVM – bisher konzentriert haben, nämlich langlebige Prozesse, die auf eine große Menge von CPU und Arbeitsspeicher zurückgreifen, und das Konzept „Write once, run anywhere“, sind heute nicht mehr die primären Treiber. Heute werden schnelle Startzeiten und ein geringer Arbeitsspeicherverbrauch benötigt, um horizontal, also mit mehr Instanzen und nicht mit größerer Hardware, zu skalieren. Zudem laufen Anwendungen dank der Container-Technologie sowieso überall.
Natürlich könnte ich eine andere Programmiersprache einsetzen. Neuere Sprachen wie Go oder Node.js passen von ihren Eigenschaften besser. Allerdings möchte ich das große vorhandene Ökosystem rund um Java und die JVM nicht mehr missen. Für quasi jede Anforderung gibt es eine Bibliothek, die mir hilft, mich nicht auf Technik, sondern die Fachlichkeit zu konzentrieren. Zudem ist bei den meisten Teams in meinen Projekten bereits eine Menge Wissen rund um Java und die JVM vorhanden, und diese Investition soll nicht leichtfertig weggeworfen werden.
Quarkus zielt genau hierauf ab. Ziel ist es, einen Java-Stack zur Verfügung zu stellen, der dank kurzer Startzeiten und geringem Arbeitsspeicherverbrauch ideal in der Cloud betrieben werden kann.
Ein neues Projekt erstellen
Wie und ob Quarkus diese Versprechen halten kann, überprüfen wir am besten am lebenden Objekt, also indem wir eine Quarkus-Anwendung erstellen.
Um ein initiales Projektgerüst zu erzeugen, verwenden wir einen Maven-Archetype (s. Listing 1). Das so entstandene Projekt besteht aus einer POM, einer JAX-RS-Ressource, zwei Dockerfiles, einer Konfigurationsdatei, einer statischen HTML-Seite und zwei Tests (s. Listing 2).
Nach dem Importieren in eine IDE der Wahl stellt sich die Frage, wie die Anwendung nun gestartet werden kann. Eine Main-Methode, wie etwa von Spring Boot bekannt, gibt es nicht. Brauchen wir nun noch zusätzliche Infrastruktur, etwa einen Applikationsserver? Betrachten wir doch einmal die pom.xml. Hier wird ein quarkus-maven-plugin konfiguriert. Genau dieses nutzen wir nun, um die Anwendung zu starten (s. Listing 3). Bereits nach wenigen Sekunden können wir die Anwendung nun unter http://localhost:8080/hello erreichen.
An dieser Stelle läuft die Anwendung nicht nur, sondern Quarkus prüft auch bei jedem eingehenden HTTP-Request, ob sich an den Quelldateien etwas geändert hat. Ist dies der Fall, wird der Code automatisch neu kompiliert und der HTTP-Request durchläuft sofort den bereits geänderten Code.
Ändern wir zum Beispiel den zurückgegebenen Text der hello-Methode in der HelloResource-Klasse von hello auf Hello, World! (s.Listing 4) und laden anschließend die Seite im Browser neu, so sehen wir sofort den neuen Text.
Hierdurch ist der Zyklus vom Durchführen einer Änderung bis zum Ansehen des Ergebnisses sehr kurz und befreit uns von der Bürde, die Anwendung regelmäßig neu zu starten.
Tests
Nun, da wir eine Änderung an unserem Code durchgeführt haben, sollten wir die Tests starten, um zu prüfen, ob wir nichts kaputt gemacht haben.
Hierzu können wir entweder mvn package
ausführen oder den HelloResourceTest
über unsere IDE als JUnit-Test starten. Das Ergebnis dieses Testlaufs, hier per
Maven, zeigt Listing 5. Der Test schlägt, nicht ganz unerwartet, fehl. Schauen
wir uns den Test doch einmal genauer an (s. Listing 6).
Dadurch, dass der Test mit @QuarkusTest
annotiert ist, wird vor der
Ausführung der mit @Test
annotierten Test-Methoden die komplette Anwendung
gestartet. Somit können wir innerhalb der Test-Methoden mithilfe der Bibliothek
REST-assured einen HTTP-Endpunkt der laufenden Anwendung aufrufen und das
Ergebnis dieses Aufrufes anschließend überprüfen.
Damit der Test wieder erfolgreich durchläuft, ändern wir in Zeile 11 den Text von hello in Hello, World! und lassen ihn anschließend, zur Bestätigung, erneut laufen.
Quarkus macht es uns also auch einfach, Integrationstests gegen die laufende Anwendung zu schreiben. Kombiniert mit der schnellen Startzeit der Anwendung, der gesamte Test braucht bei mir ca. 1 Sekunde, kann so eine hohe Testabdeckung erreicht werden, ohne dass die Tests anschließend mehrere Minuten brauchen.
Extensions
Anstatt der Nachricht des Endpunktes als reinen Text wollen wir allerdings JSON
ausliefern. Hierzu ändern wir den Media-Type auf APPLICATION_JSON
und geben
anstelle des Strings
eine JAX-RS Response
zurück. Für den eigentlichen Body
der Response erstellen wir eine statische Klasse Greeting
(s. Listing 7).
Versuchen wir nun jedoch, den Endpunkt aufzurufen, erhalten wir einen Fehler.
Dieser besagt, dass kein MessageBodyWriter
für unsere Greeting
-Klasse
gefunden werden konnte.
Quarkus bringt im initialen Zustand nur REST-Support, in Form von JAX-RS, mit. Um die Anwendung um zusätzliche Dinge wie JSON, Datenbankanbindung oder Sicherheitsfeatures zu erweitern, werden sogenannte Extensions genutzt. Diese entsprechen von ihrer groben Idee den eventuell von Spring Boot bekannten Startern.
Für unseren Fall benötigen wir die Extension reasteasy-jsonb. Diese muss dem Projekt als Abhängigkeit in der POM hinzugefügt werden. Um dies zu vereinfachen, können wir aber auch den Befehl aus Listing 8 nutzen.
Da über die Extension neue Bibliotheken hinzugefügt werden, müssen wir ausnahmsweise die Anwendung neu starten. Anschließend liefert uns ein Aufruf von http://localhost:8080/hello das erwartete JSON zurück.
Natürlich müssen wir nun auch den Test wieder anpassen. Hierzu nutzen wir den in REST-assured eingebauten JSON-Support und ändern die Zeile 11 aus Listing 6 erneut. Das Ergebnis ist in Listing 9 zu sehen.
Paketierung der Anwendung
Nachdem wir nun einen JSON-Endpunkt haben und die Tests erfolgreich durchlaufen, wollen wir die Anwendung paketieren. In Produktion wollen wir diese nämlich nicht per Maven und im Entwicklungsmodus von Quarkus starten. Dementsprechend müssen wir mit Maven ein Paket bauen, welches wir anschließend deployen können.
Hierzu rufen wir, wie von vielen Java-Frameworks gewohnt, mvn clean package
auf. Anschließend befindet sich unter
target/javaspektrum-quarkus-0.0.1-SNAPSHOT-runner.jar eine JAR-Datei, die wir
per java -jar starten können. Wie in Listing 10 zu sehen ist, startet die
Applikation in der Tat sehr schnell und ist auch direkt danach über HTTP
erreichbar.
Diese schnelle Startzeit erreicht Quarkus dadurch, dass bereits beim Bauen und Paketieren der Anwendung so viele Metadaten wie möglich ausgewertet werden. Zudem verzichtet Quarkus, wo immer es geht, auf den Einsatz von Reflection.
Die so gestartete Anwendung verbraucht allerdings bei mir auf dem Rechner um die 100 MB Arbeitsspeicher. Das reine Paketieren als ausführbare JAR-Datei erfüllt somit das Ziel eines deutlich reduzierten Arbeitsspeicherverbrauchs noch nicht. Um dieses zu erreichen, müssen wir die Anwendung nativ paketieren. Hierzu bedient Quarkus sich der GraalVM.
GraalVM
Über Graal und die GraalVM wurde bereits in JavaSPEKTRUM ausführlich geschrieben (s. „Polyglot Operations in Java mit GraalVM“ von M. Hunger in JAVASPEKTRUM 01/19 und „Vorcompilierte Programme mit GraalVM AOT“ von M. Hunger in JavaSPEKTRUM 02/19). Trotzdem möchte ich hier kurz die Grundlagen wiederholen.
Die Hauptidee von Graal war es, einen in Java geschriebenen Just-in-Time (JIT)-Compiler für die JVM zu entwickeln. Es hat sich allerdings herausgestellt, dass es mit Graal auch möglich ist, Java-Quelldateien Ahead-of-Time (AOT) in nativen Code zu übersetzen. Der so erzeugte native Code wird anschließend mittels SubstrateVM, einer verschlankten Version der Hotspot JVM, ausgeführt.
Die GraalVM entspricht dabei einer, auf JDK 8 basierenden, JVM, die bereits per Default Graal als JIT verwendet und um Tools zur Erstellung von nativen Images erweitert wurde. Java-Anwendungen, die mittels der GraalVM in nativen Code übersetzt werden, starten anschließend deutlich schneller und verbrauchen zur Laufzeit auch deutlich weniger Arbeitsspeicher. Um dies zu erreichen, gibt es allerdings ein paar Einschränkungen.
Während der Erstellung geht der AOT-Compiler davon aus, dass bereits alle zur Laufzeit benötigten Informationen vorhanden sind und der wirklich benötigte Java-Code auch erreichbar ist. So werden statische Initializer nun bereits zur Compile-Zeit ausgeführt und deren Ergebnis mit in den nativen Code gepackt, was der Startzeit der Anwendung zugutekommt. Nicht verwendeter Code wird erst gar nicht mit in die native Anwendung gepackt, sondern direkt verworfen. Somit stellen Classloader, Reflection und dynamische Proxies eine Herausforderung dar. All diese Features sind nur begrenzt nutzbar und erfordern häufig zusätzliche Informationen, die dem AOT-Compiler mitgeteilt werden müssen. Zudem stehen Komponenten wie JMX oder ein JIT-Compiler nicht mehr zur Verfügung.
Doch genug von Graal. Schauen wir uns an, was passiert, wenn wir unsere Quarkus-Anwendung mittels GraalVM zu einem nativen Image kompilieren.
Native Paketierung der Anwendung
Quarkus ist speziell darauf optimiert, mittels GraalVM ein natives Image der Anwendung zu erstellen. Dazu gibt es in der pom.xml bereits ein Profil native. Aktivieren wir beim Aufruf von Maven dieses Profil, entsteht als Ergebnis des Builds ein natives Image (s. Listing 11).
Damit dies funktioniert, müssen wir jedoch vorab Maven per GRAALVM_HOME-Umgebungsvariable mitteilen, wo die GraalVM installiert wurde, oder aber wir fügen beim Aufruf -Dnative-image.docker-build=true hinzu, damit das Erstellen des nativen Images per Docker erledigt wird.
Das Erzeugen des nativen Images dauert dann eine Weile, bei mir braucht ein solcher Build etwa ein bis zwei Minuten. Anschließend können wir die Anwendung allerdings direkt über das native Image starten und die Startzeit hat sich noch einmal verbessert (s. Listing 12). Zudem wurde der Verbrauch von Arbeitsspeicher auf etwa 8 MB reduziert.
Ein Problem gibt es jedoch noch. Rufen wir nun http://localhost:8080/hello auf, so erhalten wir ein leeres JSON-Objekt als Antwort. Scheinbar kann unser Objekt nicht mehr sauber serialisiert werden.
Der Grund für dieses Verhalten liegt darin, dass Graal beim Analysieren der
Anwendung nicht sehen kann, dass das Feld text der Greeting
-Klasse zur
Laufzeit per Reflection ausgelesen wird. Dies müssen wir Graal also gesondert
mitteilen. Quarkus bietet hierzu die Annotation @RegisterForReflection
an.
Annotieren wir also die Greeting
-Klasse damit, bauen das native Image neu und
starten es anschließend, sehen wir die erwartete Ausgabe.
Um solche Probleme nicht erst zur Laufzeit festzustellen, ermöglicht es Quarkus
uns, auch Tests gegen das native Image laufen zu lassen. Hierzu dient die
bereits vorhandene Testklasse NativeHelloResourceIT
. Diese erbt alle Testfälle
von der bisher genutzten Testklasse HelloResourceTest
und ist mit
@SubstrateTest
annotiert. Wenn wir nun mit Maven die verify-Phase anstelle
von package nutzen, werden die Tests zusätzlich gegen eine über das native
Image gestartete Anwendung ausgeführt (s. Listing 13).
Fazit
Mithilfe von Quarkus und der GraalVM lassen sich Java-Anwendungen bauen, die durch geringe Startzeit und geringen Arbeitsspeicherverbrauch auffallen. Auch während der Entwicklung bietet die Möglichkeit, bei eingehenden Requests „on the fly“ zu kompilieren, ein bequemes Arbeiten.
Da Quarkus auf Java-Standards und bereits länger vorhandene Bibliotheken aufsetzt, ist das Wissen für die Entwicklung schon bei vielen Menschen vorhanden. Außerdem profitiert Quarkus davon, dass die Bibliotheken bereits sehr verbreitet und dadurch in Produktion ausführlich getestet sind.
Die sich selbst gesteckten Ziele „Container First“ und „Developer Joy“ erreicht Quarkus somit meiner Meinung nach durchaus.
Allerdings befinden sich sowohl Quarkus als auch die GraalVM aktuell noch in einem sehr jungen Entwicklungsstadium. Der Einsatz für wichtige Produktivanwendungen sollte also gut bedacht werden. Auch die Anzahl der verfügbaren Extensions ist überschaubar. Mir persönlich fehlt zum Beispiel die Möglichkeit, serverseitiges HTML zu generieren.
Alles in allem bleibt Quarkus und vor allem die GraalVM aber ein spannendes Thema. Ich denke, wir werden in der Zukunft noch von beiden einiges hören.