spring - JAX https://jax.de/tag/spring/ Java, Architecture & Software Innovation Fri, 18 Oct 2024 13:04:11 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Welche Möglichkeiten bietet GraalVM in Spring? https://jax.de/blog/welche-moeglichkeiten-bietet-graalvm-in-spring/ Mon, 20 Feb 2023 15:25:14 +0000 https://jax.de/?p=88357 Dieser Artikel behandelt GraalVM und ihre Möglichkeiten bzw. Integration in Spring. Wir starten mit einer kleinen Historie, um zu verstehen, warum es Graal überhaupt gibt bzw. woher es kommt. Danach sehen wir uns die Funktionalitäten von Graal für Java im Allgemeinen an und danach, inwiefern Spring 6 bzw. Spring Boot 3 Unterstützung für GraalVM anbietet.

The post Welche Möglichkeiten bietet GraalVM in Spring? appeared first on JAX.

]]>
Graal selbst bietet eine Vielzahl von Features an. Bekannt ist es aber vor allem durch das Native Image, das es uns erlaubt, Java-Programme ohne Runtime wie ein klassisches Executable auszuführen. Dadurch hat man deutliche Geschwindigkeitsvorteile, das Programm startet wesentlich schneller und auch der Speicherverbrauch ist auf einem absoluten Minimum. Allerdings muss man bei länger laufenden Anwendungen auf die Optimierungen der JVM verzichten.

Historie: GraalVM

Im Gegensatz zu anderen Gebieten (J***Script) in der Softwareentwicklung ist das Java-Ökosystem auf Beständigkeit ausgerichtet. Man möchte keine Experimente und schon gar nicht alle paar Monate ein neues Tool einsetzen. Java-Programme sind (fast) für die Ewigkeit! Aus diesem Grund ist die Frage des Graal-Ursprungs eine wesentliche. Woher kommt Graal? Ist das irgendein „fancy“ Open-Source-Projekt, dessen Hype in ein paar Monaten vorbei ist? Nein. Von Kurzfristigkeit ist hier weit und breit keine Spur.

Die Anfänge von Graal begannen mit der Überlegung, dass man die Virtual Machine – gemäß dem Motto „eat your own dog food“ – in Java neuschreibt. Normalerweise verwendet man dafür C++. Das Projekt dafür hieß Maxine, hatte seinen initialen Release bereits 2005 und kam aus der Schmiede von Sun Labs, das bekanntlich von Oracle gekauft und mittlerweile auf Oracle Labs umgetauft wurde. Ein Teil dieses Vorhabens war auch das Neuschreiben des C1-Compilers in Java. Dieser läuft in der VM und kompiliert den zuvor durch javac in Bytecode transformierten Code in Maschinencode. Es gab nun die Idee, die Java-Version C1 von Maxine in die „handelsübliche“ HotSpot zu überführen. Nach diesem erfolgreichen Experiment wollte man eine Stufe höher gehen und sich den C2-Compiler vornehmen. Der C2-Compiler gilt als auch als der „Servercompiler“. Im Gegensatz zu C1-„Client Compiler“ liegt der Fokus auf Optimierung, weswegen C2 deutlich mehr Ressourcen und auch Zeit benötigt.

Die Rückführung der Compiler von Maxine in HotSpot nannte man intern die „heilige Mission“ („Holy Quest“). Die Königsdisziplin, also der Ersatz von C2, war dann dementsprechend passend der Gral. Und damit haben wir auch die Herkunft des Namens Graal behandelt, um final die Begriffe richtig einzuordnen. Mit Graal war am Anfang der Compiler genannt und die Integration bzw. was dann schlussendlich eine eigene VM werden sollte, ist GraalVM.

GraalVM emanzipierte sich von Maxine und wurde über die Jahre hinweg innerhalb von Oracle Labs weiterentwickelt. Maxine hingegen ist seit 2017 unter der Obhut der University of Manchester. In weiterer Folge verbesserten sich die GraalVM und ihr Compiler immer weiter. Es kamen neue Features hinzu, die wir im nächsten Abschnitt behandeln. Im Mai 2019 war es schließlich soweit, dass GraalVM als production-ready [1] eingestuft wurde.

GraalVM basiert intern auf HotSpot, verwendet allerdings ihren eigenen Compiler. Gibt es hierbei Inkompatibilitäten? Nein, man muss sich hier keine Sorgen machen, dass Bestandteile der Programmiersprache nicht unterstützt oder anders ausgeführt werden. Für das Native Image, das nicht der VM oder dem Compiler zuzurechnen ist, verhält sich die Sache etwas anders. Dazu aber später mehr.

Man sieht also, dass GraalVM eine sehr lange Entwicklungsgeschichte hat, dass dahinter Oracle steht und es somit eine moderne, performantere Alternative aus eigenem Haus bietet.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Das Framework “Truffle” für die GraalVM

Die GraalVM umfasst somit eine VM und einen Compiler. Das ist jedoch nicht alles. Es gibt noch zwei weitere sehr wichtige Features. Der erste Teil, der uns als Spring-Entwickler:in in den meisten Fällen nicht sehr stark tangieren wird, ist Truffle. Es ist ein Framework, mit dem man eigene oder bereits bestehende Programmiersprachen in GraalVM integrieren kann (Abb. 1). Unterstützung des „Who is who“ der Programmiersprachen ist bereits in GraalVM vorhanden. Unter anderem Python, Ruby oder auch JavaScript. Es ist auch möglich, diese Teile untereinander zu mixen. So kann beispielsweise ein in JavaScript geschriebener Programmteil auch von Java aufgerufen werden. Was JavaScript angeht, ist noch wichtig anzumerken, dass es bereits mit Nashorn eine von Oracle entwickelte JavaScript Engine gab, die allerdings mit Java 11 als deprecated erklärt wurde.

Auf Truffle aufsetzend gibt es noch die LLVM Runtime. Extrem vereinfacht ausgedrückt kann man sagen: Was Bytecode für Java, Kotlin oder Scala ist, ist die LLVM für C/C++ oder Rust. Zusammengefasst bedeutet das, dass wir sehr viele Anwendungen, die nicht in Java geschrieben wurden, mit GraalVM sehr einfach in einer JVM zum Laufen bekommen.

Abb. 1: GraalVM-Architektur mit Truffle

Native Image in der GraalVM

Der performante Compiler und die vielen Möglichkeiten, die Truffle mitbringt, machen die GraalVM bereits zu einem Werkzeug, das man in vielen Java-Projekten sofort einsetzen möchte. Der Teil, für den Graal wahrscheinlich bei den meisten der Java-Entwickler:innen bekannt ist, ist jedoch Native Image. Native Image bedeutet nichts anderes als eine Kompilierung zu Maschinencode, wie man es von C++ zum Beispiel kennt, und dem Wegfall der VM. Das ist jedoch mit einigen Einschränkungen versehen, die im Folgenden zu nennen sind.

Bei der Kompilierung zu einem nativen Image muss Graal unsere Codebasis durchforsten und fügt alle Elemente hinzu, die es aus dem reinen Programmcode herauslesen kann. Es startet also bei der public static void main und geht den ganzen Programmfluss durch. Das heißt, dass alle Klassen, die von main aus erreichbar sind, in die Kompilierung mit aufgenommen werden. Sollten irgendwelche kontextuellen Daten, die zum Beispiel während der Laufzeit erst durch das Einlesen von Umgebungsvariablen bekannt sind, dabei sein, werden diese in der Kompilierung fehlen. Konsequenterweise fällt darunter auch die Verwendung von Reflection, wobei wir beispielsweise Methoden aufrufen, oder das dynamische Laden von Klassen. Im fachtechnischen Jargon spricht man hier von der „Closed-World Assumption“, da eben nach der Kompilierung die Tür geschlossen ist und man nachträglich keine weiteren Dinge mehr hinzufügen kann. Bezüglich der Erreichbarkeit der einzelnen Elemente spricht man von Reachability.

Das Native Image ist grundsätzlich immer schneller als die Interpretation des Bytecodes. Es muss jedoch beachtet werden, dass die sogenannte Peak Performance bei Verwendung der VM nicht erreicht werden kann. Die Peak Performance gewinnt die VM, indem sie durch sorgfältiges Profiling unsere Anwendung zur Laufzeit wesentlich besser kompilieren kann, da es weiß, wie die Anwendung verwendet wird. Das setzt allerdings eine langläufige Anwendung voraus. In der neuen Cloudwelt, in der die Instanzen nur eine kurze Lebensdauer haben, wird die Peak Performance ohnehin nicht erreicht.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Set-up GraalVM

Schauen wir uns das Ganze einmal selbst an und installieren uns die GraalVM auf unserer Maschine. Dazu navigieren wir nach [2]. Unter den ersten Punkten sehen wir die Downloads, in denen wir die Option zwischen Community und Enterprise haben. Wir downloaden nicht die Community Edition, sondern die Enterprise Edition, zum Zeitpunkt dieses Artikels Version 22.3.

Die geneigte Leserschaft wird sich nun sicherlich fragen, wieso die – wahrscheinlich kostenpflichtige – Enterprise Edition, wenn doch die Community Edition gratis zu beziehen sein wird? Der Grund liegt darin, dass die Enterprise Edition für Entwicklungstätigkeiten gratis ist, wir allerdings eine wesentlich höhere Performance bekommen als von der Community. Erst für den produktiven Einsatz sind Gebühren bei der Enterprise Edition notwendig, aber da können wir wieder auf die Community Edition ausweichen.

Wir klicken also auf Download (Abb. 2), was uns zu einem weiteren Bildschirm führt, wo wir Java-Plattform, Betriebssystem und Architecture auswählen. In diesem Artikel wurde Java 17, macOS und aarch64 verwendet. Zum Austesten nehmen wir eine Abwandlung von den offiziellen Graal-Beispielen (Listing 1, Listing 2).

Abb. 2: Downloadbildschirm

public class Main {
  public static void main(String[] args) throws Exception {
    var blender = new Blender();
    for (int j = 0; j < 10; j++) {
      long t = System.nanoTime();
      for (int i = 0; i < 100; i++) {
        blender.run();
      }
      long d = System.nanoTime() - t;
      System.out.println(d / 1_000_000 + " ms");
    }
  }
}
public class Blender implements Runnable {
 
  public static final Colour[][][] colours = new Colour[100][100][100];
 
  @Override
  public void run() {
    var id = new Colour(0, 0, 0);
    for (int x = 0; x < colours.length; x++) {
      Colour[][] plane = colours[x];
      for (int y = 0; y < plane.length; y++) {
        Colour[] row = plane[y];
        for (int z = 0; z < row.length; z++) {
          Colour colour = new Colour(x, y, z);
          colour.add(id);
          if ((colour.r + colour.g + colour.b) % 42 == 0) {
            row[z] = colour;
          }
        }
      }
    }
  }
 
  public static class Colour {
    double r, g, b;
 
    Colour(double r, double g, double b) {
      this.r = r;
      this.g = g;
      this.b = b;
    }
 
    public void add(Colour other) {
      r += other.r;
      g += other.g;
      b += other.b;
    }
  }
}

Führen wir diesen Code einmal mit dem aktuellen OpenJDK 17.0.5 aus (Abb. 3), dauert es auf der Maschine des Autors 58 Sekunden. Tauschen wir nun OpenJDK durch GraalVM aus, reduziert sich die Zeit auf 53 Sekunden. Man sieht, dass die Ausführung mit der GraalVM (Abb. 4) schneller ist, doch nicht unbedingt in einem Ausmaß, das einen vom Hocker haut. Es handelt sich hier allerdings lediglich um eine andere VM mit einem besseren Compiler. Das Native Image ist noch nicht im Einsatz.

Fairerweise ist hier auch anzumerken, dass ein Beispiel, das wir von der GraalVM-Seite nehmen, natürlich für GraalVM optimiert ist. Aber es steht der geschätzten Leserschaft frei, eigene Codebeispiele zu nehmen.

Abb. 3: Ausführung OpenJDK

Abb. 4: Ausführung GraalVM

Native Image allgemein

Es wird Zeit, Gas zu geben. Wir bleiben bei unserem Blender-Beispiel und wandeln es in ein Native Image um.

Installation und Ausführung

Als Erstes müssen wir das Native Image installieren. Mit dem Befehl gu list sehen wir, dass bis dato nur die GraalVM Core installiert ist. Das Native Image beziehen wir mittels

gu install native-image

Bei der Verwendung der Enterprise Edition wird man nun aufgefordert, die E-Mail-Adresse anzugeben. Es wird dann ein Link an diese Adresse verschickt, den es zu bestätigen gilt. Danach kann man die Installation über die Konsole fortsetzen. Nun folgt die eigentliche Kompilierung, bei der wir unseren Blender in Bytecode überführen und von dort das Native Image bauen.

javac Main
native-image Main

Die Ausführung starten wir dann ganz normal (auf macOS) mittels

./main

Und siehe da (Abb. 5), von den anfangs 53 Sekunden (GraalVM), sind wir auf einmal bei acht Sekunden. Dazu sagen wir natürlich nicht nein.

Abb. 5: Ausführung Native Image

Einschränkungen durch Closed-World-Ansatz

Wie oben erwähnt, ist im Closed-World-Ansatz das Nachladen etc. nicht möglich, was auch sehr sinnvoll ist. Das erzeugte Executable läuft ohne JVM und es soll nur die Elemente beinhalten, die es wirklich benötigt. Unnützen Ballast möchten wir abwerfen. Dieses Verfahren wird in der Welt der Frontend Frameworks vom Prinzip her schon länger angewandt. Nur ist es dort unter dem Namen Tree Shaking bekannt.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Können wir also unser Beispiel so umkonstruieren, dass wir eine Dynamik hineinbekommen, bei der unser natives Image nicht mehr funktioniert? Wir können Blender mittels Class.forName dynamisch laden. Wir ändern die Main.java dahingehend ab und fügen noch hinzu, dass der Klassenname über Kommandozeile als Argument übergeben wird (Listing 3).

public class Main {
  public static void main(String[] args) throws Exception {
    String className = args[0];
    Runnable blender = (Runnable)Class.forName(className).getDeclaredConstructor().newInstance();
    for (int j = 0; j < 10; j++) {
      long t = System.nanoTime();
      for (int i = 0; i < 100; i++) {
        blender.run();
      }
      long d = System.nanoTime() - t;
      System.out.println(d / 1_000_000 + " ms");
    }
  }
}

Danach kompilieren wir nochmals Main und erstellen das Native Image:

javac Main.java
native-image Main
./main Blender

Es klappt zwar, allerdings mit Warnmeldungen und plötzlich sehr langsam. GraalVM sieht, dass wir hier über Reflection eine unbekannte Klasse laden, und geht auf Nummer sicher, indem es ein „Fallback“ Image generiert. Das ist nicht das, was wir wollen. Das Fallback Image bedeutet, dass die JVM wieder im Spiel ist. Wir müssen bspw. nur das Executable in einen anderen Ordner kopieren und es von dort aus neu ausführen. Es wird nicht funktionieren. Es gibt die Möglichkeit, das Fallback Image zu deaktivieren. Das können wir bei der Erzeugung des Image mittels eines Flags definieren.

native-image Main --no-fallback

Nach Neuausführung bekommen wir danach eine altbekannte Fehlermeldung:

Exception in thread "main" java.lang.ClassNotFoundException: Blender 

Der Grund, wieso überhaupt ein Fallback Image erstellt wird, ist ein rechtlicher [3]. Oracle ist hier offenbar gezwungen, dass das Native Image auf alle Fälle eine lauffähige Anwendung ist – selbst wenn die VM inkludiert ist. Hauptsache, es läuft. Aber sowohl mit als auch ohne Fallback kommen wir zu einem unbefriedigenden Ergebnis. Unsere Anwendung läuft nicht. In diesem Fall müssen wir selbst Hand anlegen und dem Native Image mitteilen, welche Klassen wir dynamisch laden, damit es diese auch während der Kompilierung mit aufnimmt. Das erfolgt mittels eigener Konfigurationsdateien. Diese können und werden bei größeren Umgebungen durchaus ausarten. Bei unserem Beispiel ist das nicht der Fall. Wir müssen lediglich die Blender-Klasse angeben. Dazu erstellen wir eine neue Datei namens reflect-config.json mit dem Inhalt aus Listing 4.

[
  {
    "name": "Blender",
    "allDeclaredConstructors": true
  }
]

Danach starten wir die Kompilierung und Ausführung erneut. Dieses Mal geben wir jedoch die Konfigurationdatei dazu:

native-image Main -H:ReflectionConfigurationFiles=reflect-config.json
./main Blender

Jetzt klappt es ohne Schwierigkeiten. Wir müssen uns aber natürlich im Klaren sein, dass wir durch eine fehlerhafte Konfiguration Fehler in der Laufzeit erzeugen können.

Es können – auch mehrere Konfigurationsdateien eingesetzt werden, und das ist auch der Fall. Konventionsmäßig gibt man diese in den Ordner META-INF/native-image und gruppiert sie dann in einem weiteren Unterordner mittels ihrer groupId und artifactId. GraalVM versucht, so viel wie möglich von dem dynamischen Verhalten abzudecken. Sollten wir bspw. den Klassennamen als statischen String verwenden, brauchen wir weder eine Konfigurationsdatei noch wird ein Fallback Image erstellt:

Runnable blender = (Runnable)Class.forName("Blender").getDeclaredConstructor().newInstance();

Metadata bei großen Projekten

Wir haben gesehen, dass das Native Image mit seinem Closed-World-Ansatz je nach Dynamik einmal mehr und einmal weniger Hilfe benötigt. Das hier vorgeführte Beispiel ist auf das absolute Minimum reduziert. Wir haben keine packages, builden kein jar, geschweige denn setzen wir Maven oder Gradle ein. In einem professionellen Umfeld würden wir hier entsprechende Plug-ins einsetzen, bei denen das Native Image in den Build integriert ist. Auch ist bei Java die Dynamik nicht nur auf Reflection basiert. Es gibt noch Proxies, Ressourcen usw. In diesem Fall gibt die offizielle Dokumentation [4] ausführlich Auskunft.

Das Erstellen der Konfigurationsdateien kann semiautomatisch durchgeführt werden. Eine gängige Variante ist, die Anwendung normal über die JVM hochzufahren, mit ihr zu arbeiten, während im Hintergrund ein Agent läuft, der die Konfiguration automatisch erstellt. Bei unserem Beispiel machen wir das mittels des Befehls:

java -agentlib:native-image-agent=config-merge-dir=META-INF/native-image Main Blender

Bei der Ausführung werden durch die merge-Option Warnungen kommen, weil es das Verzeichnis noch nicht gibt. Das können wir ignorieren. Nachdem wir aber die Konfigurationsdateien bereits in das Standardverzeichnis erzeugt haben, werden sie automatisch beim nächsten native-image Main mitverwendet. Wir können jetzt auch unsere selbstgestrickte reflect-config.json löschen. Wenn wir jetzt nochmals main ausführen, sollte es keine Schwierigkeiten mehr geben.

Für Drittbibliotheken müssen wir das Rad nicht neu erfinden. Es gibt hier beispielsweise das sogenannte GraalVM Reachability Metadata Repository [5] auf GitHub, wo Bibliotheken ihre Konfigurationsdateien hinterlegen. Man kann dort auch sehr schön erkennen, wieso man die Konfigurationen nicht selbst schreiben sollte. Die reflect-config.json von Hibernate 6.1.1 hat zum Beispiel über 12 000 Zeilen.

 

Zusammenfassung zu Native Images

Native Image beschleunigt unsere Anwendungen signifikant. Durch das Closed-World-Prinzip müssen wir jedoch mit Einschränkungen rechnen bzw. durch Metadaten dem Compiler auf die Sprünge helfen. Als Nächstes wenden wir uns dem Einsatz von Native Image in Spring zu.

Native Image in Spring

Spring Ahead-of-Time

Wir haben bereits gesehen, dass bei dynamischen Java-Applikationen der Konfigurationsaufwand nicht zu unterschätzen ist. Wie verhält es sich jetzt mit Frameworks, wie zum Beispiel Spring, bei dem dynamisches Java das Fundament bildet?

Beispielsweise ist der Einsatz von Proxies omnipräsent und Features wie Profile über @Profile sind prinzipiell ausgezeichnet, allerdings schwer vereinbar mit dem Closed-World-Prinzip. Wenn wir uns ansehen, wie andere Frameworks damit umgehen, erkennen wir, dass zum Beispiel Micronaut [6], die komplette Dynamik bereits während des Builds durchführt. Das heißt, dass im kompilierten Bytecode, der dem Native Image vorgelegt wird, bereits alles statisch ist. Bei Spring wird dies bekanntlich während der Laufzeit gemacht.

Befinden wir uns also mit Spring in einer Sackgasse bzw. müsste man Spring von Grund auf neuprogrammieren, damit es ähnlich verfährt wie Micronaut & Co? Dadurch, dass die GraalVM nicht plötzlich und vollkommen unerwartet erschienen ist, hatte Spring natürlich dementsprechend viel Vorlaufzeit, um sich darauf vorzubereiten. Die Arbeiten begannen bereits vor 2019, jedoch wurde 2019 unter dem Namen Spring Native [7] ein GraalVM-spezifisches Projekt in den Incubator-Status erhoben. Es hatte die Aufgabe, Möglichkeiten herauszufinden und auszuprobieren, wie man Spring am besten an die Erfordernisse von GraalVM anpasst. Nach erfolgreicher Mission wurde Spring Native mehr oder weniger als deprecated erklärt und das Resultat floss direkt in Spring 3.0, wo nun die Unterstützung für GraalVM nativ dabei ist.

Das Erstellen eines Native Image in Spring setzt sich aus zwei Teilen zusammen: der Vorbereitung und dem eigentlichen Build des Native Image, das von Graal übernommen wird. Bei der Vorbereitung transformiert Spring den bestehenden Source Code dahingehend, dass er nicht mehr auf Reflection oder sonstigen dynamischen Elementen basiert. Das heißt, die ganzen Konfigurationen werden dahingehend umgeschrieben, dass die main-Methode spezielle BeanFactories aufruft, die das direkte Resultat der Transformation sind. Damit ist es für das Native Image sehr einfach herauszufinden, welche Klassen benötigt werden.

Diese „Transformation“ wird bei Spring die Ahead-of-Time Compilation genannt. Man muss sie nicht zwangsweise verwenden, um ein Native Image zu erstellen. Man kann sie auch direkt innerhalb einer VM ausführen. Das hat dann den Vorteil, dass das Auffinden der Beans nicht mehr zur Laufzeit stattfindet, wodurch man bereits einen schnelleren Start-up hätte. Wir gehen aber einen Schritt weiter und möchten ein Native Image bauen (Abb. 6).

Abb. 6: Graal Native Image mit Spring

Um Spring Boot 3 mit der GraalVM-Integration zu installieren, erstellen wir ein neues Projekt über den Initializr [8] und wählen als Abhängigkeiten GraalVM Native Support sowie Spring Web aus. Die restlichen Optionen lassen wir einfach auf Standard. In diesem Artikel wurde jedoch com.rainerhahnekamp als groupId und graalspring als artifactId verwendet.

Es ist an dieser Stelle anzumerken, dass mit Spring Boot 3 Gradle als standardmäßiges Build-Tool eingesetzt wird. Wer also nach wie vor mit Maven arbeitet, muss das explizit anwählen.

Wir verwenden 1:1 den Blender von früher, statten ihn aber mit @Service aus, sodass er als Bean erkannt wird. Des Weiteren fügen wir einen BlenderController hinzu, in dem der Blender injectet werden soll (Listings 5 und 6).

package com.rainerhahnekamp.graalspring;
 
@Service
public class Blender {
 
  public static final Colour[][][] colours = new Colour[100][100][100];
 
  // Implementierung von Blender aus vorigem Beispiel hineinkopieren
}
package com.rainerhahnekamp.graalspring;
 
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("blender")
public class BlenderController {
  private final Blender blender;
 
  BlenderController(Blender blender) {
    this.blender = blender;
  }
 
  @GetMapping("")
  public String blend() {
    blender.blend();
    return "blended";
  }
}public interface Blender {
  void blend();
}

Mit ./gradlew bootRun starten wir den Server, und wenn wir über den Browser http://localhost:8080/blender aufrufen, sollte nach einiger Zeit blended ausgegeben werden. Durch bootRun wurde aber auch schon Spring AoT aktiv. Der generierte Source Code ist im Verzeichnis build/generated/aotSources. Dort finden wir auch die für GraalVM optimierte Registrierung der Blender und BlenderController als Beans unter den Dateinamen Blender__BeandDefinitions.java sowie BlenderController__BeanDefinitions.java. Noch interessanter ist die Datei SpringGraalApplication__BeanFactoryRegistrations.java, in der alle Bean Definitions zusammenkommen. So sehen wir neben den Blendern auch die Beans, die von Spring direkt kommen.

Theoretisch könnten wir bootRun bzw. den eigentlichen Build bereits mit den AoT-Klassen starten. Für bootRun muss man dafür in der build.gradle folgenden Eintrag hinzufügen:

bootRun {
  systemProperty("spring.aot.enabled", "false")
}

Bei der erneuten Ausführung von bootRun sollte in der Konsole die Meldung Starting AOT-processed SpringGraalApplication  vorkommen. Es ist auch zu beachten, dass bereits eine Menge an Metadatenkonfiguration vorhanden ist. Diese befindet sich unter /build/aotResources/META-INF/ …

Nun lassen wir das Native Image erstellen. Mittels ./ gradlew nativeCompile wird an GraalVM das Native Image übergeben und wir sehen die bekannte Ausgabe, die wir bereits in unseren vorigen Experimenten hatten. Im Unterschied zu vorher wird allerdings die Erstellung des Native Image deutlich länger dauern. Wir müssen berücksichtigen, dass es sich hier nicht um zwei Java-Klassen handelt, sondern das Spring Framework mit Spring MVC dabei ist.

Native Image als Docker Image

Ein Problem von Native Images ist, dass sie speziell auf unseren Rechner zugeschnitten sind. Builde ich also auf macOS, werde ich nicht in der Lage sein, dies auf Windows-Rechnern laufen zu lassen. Das alte Java-Motto „Write once, run anywhere“ ist hier nicht gültig. Diese Aufgabe hat bei modernen Deployments allerdings bereits Docker übernommen. Das heißt, wenn wir unser Native Image in ein dazugehöriges Docker Image stecken, haben wir an und für sich die Plattformunabhängigkeit wieder zurückgewonnen.

Was ist also zu tun? Spring ist bereits mit allem ausgestattet. Wir brauchen lediglich einen laufenden Docker Daemon und können dann mittels ./gradlew buildBootImage unser Native Image direkt in einem Docker Image builden, das wir dann überallhin deployen können.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Closed-World in Spring

Das Closed-World-Prinzip trifft natürlich auch auf Spring zu. Wie können wir also dort mit dynamischem Java umgehen? Nun, extrahieren wir erst einmal von unserem Blender ein Interface. Das heißt, unser Blender.java teilt sich nun in zwei Klassen auf und wir verwenden eine eigene Konfigurationsklasse, die die Bean registriert (Listing 7, 8 und 9).

public interface Blender {
  void blend();
}
public class BlenderImpl implements Blender, Runnable {
 
  public static final Colour[][][] colours = new Colour[100][100][100]; 
}
@Configuration(proxyBeanMethods = false)
public class AppConfiguration {
  @Bean
  public Blender getBlender() {
    return new BlenderImpl();
  }
}

Nun geben wir jedoch eine zweite Implementierung von Blender hinzu, die bei dem Profil dev verwendet werden soll. Bei dem Profil default soll nach wie vor BlenderImpl verwendet werden. Dazu ändern wir unsere AppConfiguration.java dahingehend ab (Listing 10).

@Configuration(proxyBeanMethods = false)
public class AppConfiguration {
  @Bean
  @Profile("default")
  public Blender getBlender() {
    return new BlenderImpl();
  }
 
  @Bean
  @Profile("dev")
  public Blender getDevBlender() {
    return new Blender() {
      @Override
      public void run() {
        System.out.println("DevBlender tut nichts");
      }
    };
  }
}

Ein kurzer Check mittels SPRING_PROFILES_ACTIVE=dev ./gradlew bootRun und der Aufruf von http://localhost:8080/blender sollte in der Konsole DevBlender tut nichts erscheinen lassen. Wie erwartet, funktionieren hier die Profile.

Builden wir hingegen das native Image neu und starten es dann mit einem aktiven (Umgebungsvariable-setzen-)Profil dev, dann werden wir beim Hochfahren von Spring sehen, dass hier das dev-Profil aktiv ist. Allerdings wird die Ausgabe DevBlender nicht kommen. Es wird nämlich – egal welches Profil wir setzen – immer BlenderImpl verwendet.

Im AoT-generierten Quellcode werden wir auch keine Spur von unserem DevBlender finden. Es ist nur die Standardimplementierung vorhanden. Der Grund liegt darin, dass bei der Generierung dieser Dateien (also durch AoT) Spring im Hintergrund hochgefahren wird und – in unserem Beispiel – Code erzeugt wird, der die unterschiedlichen Konfigurationsklassen explizit aufruft. Nachdem bei den Tasks nativeCompile bzw. aotClasses kein Profil angegeben wurde, ist das default-Profil im Einsatz. Andersherum ausgedrückt: Das @Profile ist nicht mehr dynamisch, sondern wird zur Build-Zeit fixiert. Man könnte nun theoretisch beim Task nativeCompile das Profil dev setzen. Das würde dann den DevBlender als Bean registrieren. Und es wäre komplett egal, mit welchem Profil wir dann das Native Image starten. Es wird immer der DevBlender sein.

Die Verwendung von @Profile in Verbindung mit Spring AoT ist etwas verwirrend. Aus diesem Grund sollte man auf @Profile beim Einsatz von GraalVM bzw. AoT verzichten. Auch von Ableitungen, wie zum Beispiel @ConditionalOnProperty, sollte man die Finger lassen. Detaillierte Informationen sind in der Dokumentation [9] bzw. im Wiki [10] nachschlagbar.

Was macht man allerdings nun, wenn man trotzdem konditionelle Abhängigkeiten hat? Man könnte die Implementierung des Blenders über eine Property definieren. Wir würden also unsere AppConfiguration.java umgestalten, wie in Listing 11 gezeigt.

@Configuration(proxyBeanMethods = false)
public class AppConfiguration {
  @Value("${app.blender-type}")
  public String blenderType;
 
  @Bean
  public Blender getBlender() {
    if ("dev".equals(this.blenderType)) {
      System.out.println("Providing DevBlender");
      return new Blender() {
        @Override
        public void run() {
          System.out.println("DevBlender tut nichts");
        }
      };
    }
 
 
    System.out.println("Providing BlenderImpl");
    return new BlenderImpl();
  }}

Es gibt nun eine Methode, die abhängig von der gesetzten Property den DevBlender oder BlenderImpl zurückgibt. Zur Laufzeit setzen wir dann den Wert der Property über eine Umgebungsvariable und haben das gewünschte Ergebnis. Es ist jedoch hier zu beachten, dass im Native Image beide Blender-Implementierungen enthalten sind. Das wirkt sich negativ auf die Größe aus. Spring AoT könnte natürlich dasselbe machen und alle Profile in das Native Image laden. Man hat sich allerdings gegen diese Vorgehensweise entschieden.

Spring und Metadata

Gut, wie geht aber nun Spring AoT vor, wenn wir in unserem eigenen Programmcode Reflection verwenden? Dazu laden wir die BlenderImpl über Class.forName dynamisch nach. Die geänderte AppConfiguration sieht nun aus, wie in Listing 12 gezeigt.

@Configuration(proxyBeanMethods = false)
public class AppConfiguration {
  @Value("${app.blender-class}")
  public String blenderClass;
 
  @Bean
  public Blender getBlender() throws Exception {
    return (Blender)
Class.forName(blenderClass).getDeclaredConstructor().newInstance();
  }
}

Durch das Wissen, das wir mittlerweile angesammelt haben, können wir schon voraussagen, was passieren wird. Wir werden einen ClassNotFoundException bekommen. Die BlenderImpl wird über den normalen Programmcode (verfolgt man von der static void main() die imports) nicht erreichbar sein und ist aus dem Grund auch nicht beim Image dabei.

Wenn wir in /build/generated/aotResources/**/reflect-config.json nach BlenderImpl suchen, werden wir nichts finden. Wir können nun einen entsprechenden Eintrag in der reflect-config.json vornehmen oder uns wieder des Agents bedienen. Ist alles kein Problem, nur dass es von Spring eine typensichere Alternative gibt. Diese kommt in der Form des Interface RuntimeHintsRegistrar. Wir implementieren es und erhalten über die zu implementierende Methode registerHints mit RuntimeHints ein Objekt, mit dem wir typensicher den Constructor für die BlenderImpl für die Reflection registrieren können. Wir erstellen dafür eine eigene Klasse, betten diese aber sogleich als statische verschachtelte Klasse in unsere AppConfiguration ein (Listing 13).

@Configuration(proxyBeanMethods = false)
@ImportRuntimeHints(AppConfiguration.RegistryHinter.class)
public class AppConfiguration {
  @Value("${app.blender-class}")
  public String blenderClass;
 
  @Bean
  public Blender getBlender() throws Exception {
    return (Blender) Class.forName(blenderClass).getDeclaredConstructor().newInstance();
  }
 
  public static class RegistryHinter implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
      hints.reflection().registerConstructor(BlenderImpl.class.getDeclaredConstructors()[0], ExecutableMode.INVOKE);
    }
  }
}

Wir sehen, dass wir mittels @ImportRuntimeHints explizit angeben müssen, dass es RegistryHinter gibt. Diese wird auch nur dann aktiviert, wenn GraalVM die AppConfiguration als erreichbar ansieht (wofür Spring AoT sorgt). Also noch einmal das Native Image erstellen, beim Starten die Umgebungsvariable APP_BLENDER_CLASS auf den vollen Namen der BlenderImpl setzen, und dann sollte es funktionieren. Zur Sicherheit kann man sich auch in der reflect-config.json davon überzeugen, dass dieses Mal die BlenderImpl auftaucht.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Testen von Native Image mit Spring Boot

Wir möchten natürlich überprüfen können, ob das Native Image wie geplant funktioniert. In unserem Fall würde das bedeuten, dass wir einen Test benötigen, der die entsprechende Property setzt. Wir können mit Hilfe von Spring Boot einen sehr einfachen Test schreiben, der aussieht, wie in Listing 14 gezeigt.

@SpringBootTest(properties =
{"app.blenderClass=com.rainerhahnekamp.graalspring.BlenderImpl"})
class BlenderControllerTest {
  @Autowired
  BlenderController controller;
 
  @Test
  void testBlendShouldNotThrow() {
    assertDoesNotThrow(() -> controller.blend());
  }
}

Wir können den Test starten und er wird funktionieren, weil er ganz einfach in der JVM läuft und dort das Class.forName kein Problem ist. Was wir jedoch wirklich möchten, ist, dass dieser Test gegen das Native Image ausgeführt wird. Auch hier hat bereits Spring vorausschauend etwas für uns vorbereitet. Wir müssen lediglich folgenden Befehl ausführen:

./gradlew nativeTest

Hier wird erst einmal das Native Image erstellt und die Tests werden auch dagegen ausgeführt. Bitte bei diesem Beispiel darauf achten, dass es nur diese eine Testdatei gibt, in der auch der richtige Wert für die Property gesetzt wird. Normalerweise kommt der Spring Initializr bereits mit einem vorkonfigurierten Test, der hier fehlschlagen wird.

Abschluss: GraalVM in Spring

„Wo viel Licht ist, ist auch viel Schatten.“ – trifft dieses Sprichwort auch auf GraalVM zu? Man muss hier einerseits Jürgen Höller zitieren, der in seinem Vortrag über Spring 6 [11] meinte, der Einsatz von GraalVM führe zu einer Performancesteigerung. Wie viel das ist, hängt allerdings vom Einsatzfall ab und ist immer hochindividuell. Klar ist, dass der großflächige Produktiveinsatz von GraalVM erst im Entstehen ist. Es wäre illusorisch anzunehmen, dass nicht das eine oder andere Problemchen noch irgendwo auftaucht. Daneben gilt es auch noch andere Aspekte zu beachten, für die jedoch hier auf [12] verwiesen werden soll.

Als Nächstes kann der Autor nur empfehlen, GraalVM auf eine bestehende Spring-Applikation auszuführen. Aber natürlich erst, wenn diese auch auf Spring 6 bzw. Spring Boot 3 läuft. Zur weiteren Vertiefung bieten sich der Vortrag von Stéphane Nicoll und Brian Clozel von der Devoxx 2022 [13] und natürlich die offizielle Dokumentation [14] an.

Cloudarchitekturen zwingen unsere Java-Anwendungen, schneller zu starten und einen kleineren Memory Footprint (Kosten) zu haben. Ist das „alte Java“ für die modernen Zwecke nicht mehr passend? Obwohl es bezüglich des Real-World-Einsatzes noch in den Kinderschuhen steckt, haben wir eine Antwort auf diese Frage: GraalVM.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 


Links & Literatur

[1] https://blogs.oracle.com/java/post/for-building-programs-that-run-faster-anywhere-oracle-graalvm-enterprise-edition

[2] https://www.graalvm.org/

[3] https://github.com/oracle/graal/issues/2648#issuecomment-788780365

[4] https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/

[5] https://github.com/oracle/graalvm-reachability-metadata

[6] https://micronaut-projects.github.io/micronaut-aot/latest/guide/

[7] https://github.com/spring-projects-experimental/spring-native

[8] https://start.spring.io

[9] https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html#native-image.introducing-graalvm-native-images.understanding-aot-processing

[10] https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-with-GraalVM

[11] https://www.youtube.com/watch?v=mitWK_DwKGs&t=1923s

[12] https://www.infoq.com/articles/native-java-aligning/

[13] https://youtu.be/TS4DpYSmfXk

[14] https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html

[15] Podcast mit Thomas Würthinger: https://poddtoppen.se/podcast/1296655154/airhacksfm-podcast-with-adam-bien/from-maxwell-over-maxine-to-graal-vm-substratevm-and-truffle

[16] https://youtu.be/h419kfbLhUI

[17] Würthinger, Thomas: GraalVM History, Status, Vision: https://www.graalvm.org/uploads/workshop-nov-2019/2019-11-25-GraalVMCommunityWorkshop-HistoryStatusVision.pdf

[18] Interview mit Thomas Würthinger: https://www.infoq.com/news/2018/04/oracle-graalvm-v1/

[19] https://openjdk.org/projects/graal/

[20] https://www.graalvm.org/

[21] https://maxine-vm.readthedocs.io/en/latest/#

[22] https://maciejwalkowiak.com/blog/spring-boot-3-native-image-not-a-free-lunch/

The post Welche Möglichkeiten bietet GraalVM in Spring? appeared first on JAX.

]]>
Spring Native Hands-on https://jax.de/blog/spring-native-hands-on/ Mon, 05 Jul 2021 07:18:10 +0000 https://jax.de/?p=83728 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.

The post Spring Native Hands-on appeared first on JAX.

]]>
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

The post Spring Native Hands-on appeared first on JAX.

]]>
Microframeworks unter der Lupe: Javalin vs. Ktor vs. Spring Fu vs. Micronaut https://jax.de/blog/core-java-jvm-languages/microframeworks-unter-der-lupe-javalin-vs-ktor-vs-spring-fu-vs-micronaut/ Wed, 17 Apr 2019 16:11:09 +0000 https://jax.de/?p=67665 In letzter Zeit gewinnen in der Java-Welt Microframeworks wie Javalin, Ktor, Spring Fu oder Micronaut an Bedeutung. Christian Schwörer (Novatec Consulting GmbH) stellt die Frameworks in seiner kommenden JAX-Session eingesetzt vor. Wir haben im Vorfeld um eine kurze Einschätzung gebeten.

The post Microframeworks unter der Lupe: Javalin vs. Ktor vs. Spring Fu vs. Micronaut appeared first on JAX.

]]>

Was sind Microframeworks eigentlich?

JAX: In der letzten Zeit kommen Microframeworks wieder in Mode. Beispiele in der Java-Welt sind Javalin, Ktor, Spring Fu und Micronaut. Worauf zielen diese Frameworks ab?

Christian Schwörer: Unter „Microframeworks“ versteht man minimalistische Web-Frameworks zum Bau von modularen Anwendungen. Wesentlicher Bestandteil ist die Möglichkeit, einen Webserver wie zum Beispiel Netty zu konfigurieren und zu starten. Darüber werden dann für gewöhnlich REST-Endpunkte bereitgestellt oder Webinhalte ausgeliefert.

Das Besondere bei Microframeworks ist, dass sie sich auf die zentralen Konzepte bei der Anwendungsentwicklung fokussieren. Durch diese Vereinfachung steht die Developer Experience klar im Vordergrund: Es ist möglich, sehr schnell eine Web-Anwendung zu erstellen.

Ebenso zeichnen sich alle genannten Frameworks durch ihre klare Cloud-Ausrichtung und die Eignung für die leichtgewichtige Erstellung von Microservices aus.

JAX: Für die Java-Plattform haben wir mit Spring Boot und dem Eclipse MicroProfile zwei prominente Frameworks für Microservices. Wie unterscheiden sich die Microframeworks von diesen beiden?

Christian Schwörer: Wie erwähnt, konzentrieren sich Microframeworks auf die wesentlichen Bestandteile zur Erstellung von Microservices. Daher haben sie üblicherweise einen geringeren Funktionsumfang als Fullstack-Frameworks wie Spring, MicroProfile oder Grails. Allerdings gibt es auch bei Microframeworks eine große Bandbreite: von Frameworks, die sich wirklich auf das Elementare beschränken, bis hin zu welchen, die so gut wie alle von anderen Frameworks bekannten Features bieten.

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

Unabhängig vom Funktionsumfang zeichnen sich jedoch alle Microframeworks durch einen schnellen Applikationsstart und einen kleinen Memory Footprint aus. Das ist vor allem beim Einsatz in geclusterten Docker-Containern oder in Serverless-Architekturen ein entscheidender Vorteil.

Das Besondere bei Microframeworks ist, dass sie sich auf die zentralen Konzepte bei der Entwicklung fokussieren.

 

Javalin, Ktor, Spring Fu, Micronaut

JAX: Bleiben wir einmal bei den vier genannten Microframeworks und beginnen bei Javalin. Wie kann man Javalin einordnen – wo liegen die Stärken?

Christian Schwörer: Die Stärke von Javalin liegt ganz klar in seiner Einfachheit. Es ist sehr leicht zu verstehen und handzuhaben, und dementsprechend einfach ist der Code zu lesen. Dies erreicht das Framework, indem es sich auf wenige wesentliche Konzepte beschränkt, die erlernt werden müssen. Daher eignet es sich auch am ehesten für die schnelle Erstellung von überschaubar kleinen (Micro-)Services.

JAX: Ktor beschreibt sich selbst als asynchrones Web Framework für Kotlin. Für welchen Einsatzzweck ist Ktor aus deiner Sicht besonders interessant?

Christian Schwörer: Hinter Ktor steht maßgeblich JetBrains, das Unternehmen, das die meisten vermutlich von der Entwicklungsumgebung IntelliJ und der JVM-Sprache Kotlin kennen.

Es verwundert daher wenig, dass Ktor – anders als die anderen genannten Microframeworks – ausschließlich Kotlin unterstützt. Diese Einschränkung ermöglicht es allerdings, die Kotlin-Sprachfeatures ideal zu nutzen. So setzt Ktor beispielsweise intensiv auf Coroutines, der leichtgewichtigen Kotlin-Lösung für nebenläufige Programmierung. Dadurch ergibt sich ein asynchrones Framework, das sich etwa für den Bau von API-Gateways eignet.

Nebenbei erwähnt, arbeite ich persönlich seit Längerem in der Backend-Entwicklung ausschließlich mit Kotlin, so dass ich eine gewisse Affinität zu verwandten Technologien habe.

Die Stärke von Javalin liegt ganz klar in seiner Einfachheit.

 

JAX: Relativ neu ist auch das Projekt Spring Fu. Damit lassen sich Spring Boot-Anwendungen mittels einer Kotlin DSL oder einer Java DSL konfigurieren. Kannst du das einmal anhand eines Beispiels demonstrieren?

Christian Schwörer: Spring Fu bietet eine funktionale Alternative zur Annotation-basierten, deklarativen Spring Boot-Konfiguration. In folgendem Kotlin-Beispiel wird anhand der @Bean-Annotation eine Spring-Bean definiert:

@Configuration
class MyConfiguration() {
    @Bean
    fun mySpringBean() = MySpringBean()
}

Mit der Kotlin-DSL von Spring Fu sieht dies wie folgt aus:

configuration {
    beans {
        bean< MySpringBean >()
    }
}

Durch die explizite, funktionale Konfiguration wird der Overhead auf ein Minimum reduziert, der sich bei der deklarativen Nutzung von Spring Boot durch Reflection und Classpath Scanning ergibt. Dies führt zu einem schnelleren Applikationsstart und weniger Speicherverbrauch.

Allerdings gibt es Spring Fu erst als Incubator in der Version 0.0.5. Das heißt, auch wenn es sich lohnt, das Projekt im Blick zu behalten, ist es für einen Einsatz in einem produktiven Szenario meines Erachtens noch zu früh.

JAX: Und schließlich Micronaut: Was sind die Eckdaten dieses Frameworks?

Christian Schwörer: Micronaut ist sicherlich das Feature-kompletteste der erwähnten Frameworks. Neben Dependency Injection, Anbindung unterschiedlichster Datenbanken und zahlreicher Security Features bietet es Cloud-native Module wie etwa Service Discovery, Circuit Breakers und Distributed Tracing. Es positioniert sich somit am klarsten als Alternative zu Spring Boot oder Eclipse MicroProfile.

Dennoch weist es Eigenschaften von Microframeworks auf, insbesondere kurze Startzeiten und geringer Speicherverbrauch. Dies wird erreicht, da weitestgehend auf Reflection, Proxies und Classpath Scanning zur Start- und Laufzeit verzichtet wird. Ermöglicht wird dies, indem die benötigten Informationen mittels Annotation Processing und Ahead-of-Time-Compilation bereits zur Compilezeit ermittelt werden.

Ich sollte mir im Vorfeld über den groben Scope meiner Anwendung bewusst sein.

 

Microframeworks in der Praxis

JAX: Du kennst die Microframeworks ja aus der Praxis. Wie kann man am besten loslegen? Hast du einen besonderen Tipp für die Leser, der für dich persönlich gut funktioniert hat?

Christian Schwörer: Einfach selbst ausprobieren! Für alle genannten Frameworks gibt es gute Tutorials, anhand derer man sehr schnell ein Gefühl für die Entwicklung damit bekommt und einen ersten Eindruck, ob das Framework für die angedachte Aufgabe geeignet ist.

Ich sollte mir im Vorfeld aber auch über den groben Scope meiner Anwendung bewusst sein. Gehe ich davon, dass sie in Zukunft stark erweitert werden muss? Brauche ich deshalb beispielweise Features wie Dependency Injection oder explizites Transaktionsmanagement? Dann eignen sich nicht alle der angesprochenen Frameworks, da sich einige – wie bereits erwähnt – zugunsten der schnellen Erlernbarkeit und Einfachheit bewusst auf Kernfunktionen beschränken.

JAX: Vielen Dank für dieses Interview!

Die Fragen stellte Hartmut Schlosser.

 

Quarkus-Spickzettel


Quarkus – das Supersonic Subatomic Java Framework. Wollen Sie zeitgemäße Anwendungen mit Quarkus entwickeln? In unserem brandaktuellen Quarkus-Spickzettel finden Sie alles, was Sie zum Loslegen brauchen.

 

Jetzt herunterladen!

The post Microframeworks unter der Lupe: Javalin vs. Ktor vs. Spring Fu vs. Micronaut appeared first on JAX.

]]>
Spring Boot vs. Eclipse MicroProfile: Microservices-Frameworks im Vergleich https://jax.de/blog/microservices/spring-boot-vs-eclipse-microprofile-microservices-frameworks-im-vergleich/ Mon, 08 Apr 2019 10:51:54 +0000 https://jax.de/?p=67592 Microservices werden im Java-Umfeld immer öfter mit Spring Boot gebaut. Wer aus dem Java-EE- bzw. Jakarta-EE-Lager kommt, hat mit dem Eclipse MicroProfile eine Alternative zur Hand. Wo liegen die Gemeinsamkeiten, wo die Unterschiede?

The post Spring Boot vs. Eclipse MicroProfile: Microservices-Frameworks im Vergleich appeared first on JAX.

]]>

Wir haben mit Tim Zöller, Entwickler bei der ilum:e informatik ag und Sprecher auf der JAX 2019, über den alten Gegensatz „Spring versus Java EE“ gesprochen. Kommt es im Zeichen der Microservices zu einer Neuauflage unter dem Banner „MicroProfile versus Spring Boot“?

Spring Boot oder Eclipse MicroProfile: Microservices-Frameworks im Vergleich

JAX: Hallo Tim, vielen Dank, dass du dir die Zeit für dieses Interview genommen hast! Im Enterprise-Bereich war die Java-Welt ja viele Jahre lang von zwei Frameworks dominiert: Spring und Java EE. Beide hatten den möglichst kompletten Java-Application-Server als zentrale Metapher. Hat diese Metapher in Zeiten von Microservices & Serverless ausgedient?

Tim Zöller: Ich glaube nicht, dass wir die Application Server in näherer Zukunft verschwinden sehen werden. Sie vereinen eine Vielzahl erprobter APIs und Implementierungen in sich, die man aus sehr schlank gebauten WARs heraus benutzen kann. Das ermöglicht beispielsweise auf effiziente Art und Weise, Docker Images aufzubauen und zu pushen, was einigen Unternehmen wichtig ist.

JAX: Wie bewertest du die jüngsten Entwicklungen um Java EE, d.h. den Umzug zur Eclipse Foundation unter dem neuen Namen Jakarta EE? Wird der Java-Enterprise-Standard dadurch beflügelt? Oder handelt es sich eher um ein Auslaufmodell?

Tim Zöller: Ich sehe es als einen positiven Schritt, und die meisten Menschen, mit denen ich bisher darüber gesprochen habe, ebenfalls. Es ist momentan sehr spannend zu sehen, wie die Spezifikation in die Hände der Community übergeht. Die rechtlichen Probleme, die mit der Übertragung von Oracle an die Eclipse Foundation einhergehen, werfen momentan einen Schatten auf den Umzug.

Es gibt beispielsweise immer noch Unklarheiten, wenn es um Namensrechte geht. Offensichtlich wurde das durch die notwendige Umbenennung von Java EE zu Jakarta EE. Im Detail stellt es aber auch ein Problem dar, dass Klassen im Package javax.* von der Übergabe an die Eclipse Foundation betroffen sind. Diese Unsicherheiten sorgen dafür, dass sich einige Mitglieder der Community noch nicht trauen, ihre Mitarbeit zu starten.

Ich glaube nicht, dass die Application Server in näherer Zukunft verschwinden werden.

 

Microservices: Spring Boot versus Eclipse MicroProfile

JAX: Wer neue Java-Anwendungen im Cloud- und Microservices-Kontext baut, greift heute immer mehr zu Spring Boot. Weshalb eigentlich? Wo liegen die Stärken von Spring Boot im Vergleich zum Ansatz des klassischen Spring Frameworks und Java EE?

Tim Zöller: Mit dem Konzept der Spring Boot Starter und dem einfachen Aufsetzen einer neuen Anwendung wurde zunächst einmal ein gewaltiger Konfigurations-Overhead abgeschafft. Die Möglichkeit, ein einzelnes, ausführbares JAR zu erzeugen und die Applikation extern zu konfigurieren, erhöht die Flexibilität in der Entwicklung und im Deployment. Eingebaute Health-Checks und Metriken machen den Betrieb leichter.

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

JAX: Mit dem Eclipse MicroProfile hat sich ein eher wieder an Java EE bzw. Jakarta EE angelehnter Konkurrent zu Spring Boot herausgebildet. Was haben die beiden Projekte gemeinsam?

Tim Zöller: Enorm viel. Zum einen versucht MicroProfile, genau die Konzepte abzudecken, die Spring Boot erfolgreich machen: geringe Konfiguration, Health Checks, Metriken, Resilience-Maßnahmen – viele Features, die den Betrieb von mehreren, vernetzten Services in der Cloud vereinfachen. Wenn man die gleichen Dinge mit beiden Frameworks umsetzt, fällt sehr schnell auf, dass auch die Konzepte ähnlich umgesetzt sind: REST Clients in MicroProfile sehen beispielsweise OpenFeign REST Clients aus Spring Boot sehr ähnlich, Spring REST Controller werden fast gleich aufgebaut wie REST Services mit JAX-RS. Das setzt sich in den meisten Konzepten fort.

Eclipse MicroProfile und Spring Boot haben enorm viel gemeinsam.

 

JAX: Und wo liegen die Unterschiede?

Tim Zöller: Technisch liegen die Unterschiede darin, dass MicroProfile lediglich das API definiert, welches von den verschiedenen Herstellern umgesetzt wird, etwa von OpenLiberty, Thorntail oder Payara Micro. Das API versucht, hierbei sämtliche Themen zu erfassen: REST Clients, REST Services, Fault Tolerance, Configuration, Health, Metrics, usw. Bei Spring Boot existieren diese Konzepte auch, aber als Bibliotheken im Spring-Boot- oder Spring-Cloud-Umfeld, die Entwickler bei Bedarf in die Anwendung einbinden können. Ein weiterer Unterschied ist, dass MicroProfile komplett im Besitz einer offenen Community ist, welche den Standard vorantreibt.

Spring oder Java EE – der alte Kampf?

JAX: Zurück zum alten Gegensatz Spring versus Java EE: Läuft es aus deiner Sicht nun auf eine Neuauflage unter dem Banner Spring Boot versus Eclipse MicroProfile hinaus?

Tim Zöller: Ich glaube nicht, dass diese Diskussion wieder so grundsätzlich aufkommt. Anwendungslandschaften sind heute homogener als damals. Wenn man 25 Services in Produktion laufen hat, spricht nichts dagegen, die einen in Spring Boot und die anderen mit MicroProfile umzusetzen. Die Kommunikation erfolgt ohnehin über definierte Protokolle.

Spring Boot hat bereits ein gigantisches Ökosystem, welches fertige Lösungen für viele Anwendungsfälle, gerade in der Cloud, bereitstellt. Diese haben sich oft schon in ganz großen Maßstäben bewiesen, z.B. im Einsatz bei Netflix. Wenn Service Discovery, clientseitiges Loadbalancing und eine Einbindung in spezielle Monitoring-Systeme gefragt sind, ist Spring Boot nahezu konkurrenzlos.

Allerdings verlagern sich ebendiese Funktionen zunehmend in die Infrastruktur und werden, z.B. bei einem Einsatz auf Kubernetes, seltener in die Applikation eingebaut. Die Stärke von MicroProfile liegt darin, dass es nur einen Standard beschreibt, keine Implementierung. Neben den „üblichen Verdächtigen“ wie OpenLiberty, Thorntail oder Payara Micro ist es auch möglich, eine Applikation, die gegen diesen Standard entwickelt wurde, auf Implementierungen ausführen zu lassen, die speziell für kleine und schnelle Programme entwickelt wurden. Beispiele sind hier KumuluzEE oder Quarkus.

Sowohl Spring Boot als auch Eclipse MicroProfile stellen großartige Tools bereit.

 

JAX: In deiner JAX-Session Eclipse MicroProfile vs. Spring Boot: getting back in the Ring! nimmst du beide Frameworks genauer unter die Lupe. Was ist die zentrale Botschaft, die du den Teilnehmern mit auf den Weg geben möchtest?

Tim Zöller: Sowohl Spring Boot als auch Eclipse MicroProfile stellen großartige Tools bereit, um die Entwicklung von schlanken, leicht konfigurier- und wartbaren Anwendungen zu ermöglichen. Es ist eher eine Geschmacksfrage als ein „richtig“ oder „falsch“.

JAX: Vielen Dank für dieses Interview!

Die Fragen stellte Hartmut Schlosser

Java-Dossier für Software-Architekten 2019


Mit diesem Dossier sind Sie auf alle Neuerungen in der Java-Community vorbereitet. Die Artikel liefern Ihnen Wissenswertes zu Java Microservices, Req4Arcs, Geschichten des DevOps, Angular-Abenteuer und die neuen Valuetypen in Java 12.

Java-Wissen sichern!

The post Spring Boot vs. Eclipse MicroProfile: Microservices-Frameworks im Vergleich appeared first on JAX.

]]>