graalvm - JAX https://jax.de/tag/graalvm/ Java, Architecture & Software Innovation Fri, 18 Oct 2024 12:53:13 +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.

]]>
Keynote: Revolutionizing Java-Based Cloud Deployments with GraalVM https://jax.de/blog/keynote-revolutionizing-java-based-cloud-deployments-with-graalvm/ Mon, 16 May 2022 13:39:53 +0000 https://jax.de/?p=86676 In this JAX 2022 keynote, Thomas Wuerthinger, Senior Research Director at Oracle Labs and the GraalVM founder and project lead, introduces you to GraalVM.

The post Keynote: Revolutionizing Java-Based Cloud Deployments with GraalVM appeared first on JAX.

]]>
GraalVM offers native compilations of Java-based applications to make them leaner and cheaper in the cloud: They start instantly and use less memory. Additionally, there is improved performance predictability, simplified packaging, and better scalability. This talk will cover how to take advantage of this new revolutionary way to run Java-based applications including trade-offs and limitations.

 

Stay tuned

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

 

The post Keynote: Revolutionizing Java-Based Cloud Deployments with GraalVM 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.

]]>
Native Java-Programme auf der GraalVM https://jax.de/blog/native-java-programme-auf-der-graalvm/ Mon, 11 Jan 2021 10:12:28 +0000 https://jax.de/?p=82514 GraalVM und SubstrateVM, native Java-Programme: Schlagworte, die im Moment in ganz vielen Artikeln die Runde machen. Worum geht es da?

The post Native Java-Programme auf der GraalVM appeared first on JAX.

]]>
GraalVM

Die GraalVM ist eine hochperformante Runtime, die laut Webseite des Herstellers Oracle signifikante Performanceverbesserungen für Anwendungen und erheblich effizientere Microservices verspricht: „GraalVM is a high-performance runtime that provides significant improvements in application performance and efficiency which is ideal for microservices. It is designed for applications written in Java, JavaScript, LLVM-based languages.“

Sowohl ein neuer und verbesserter Just-in-Time-(JIT)-Compiler als auch ein Ahead-of-Time-(AOT-)Compiler werden erwähnt. Beim neuen JIT-Compiler wird bereits versprochen, dass er verschiedene Szenarien im Vergleich zum Standard-JDK deutlich beschleunigt.

In obiger Quelle heißt es dann weiter: „For existing Java applications, GraalVM can provide benefits by […] creating ahead-of-time compiled native images.“ Während das erstmal sehr geschwollen klingt, heißt es nichts anderes, als dass die GraalVM aus existierenden Java-Programmen EXE-Executables bzw. ELF-Programme erzeugen kann. Das GraalVM-Modul hinter diesem Feature ist die SubstrateVM.

SubstrateVM

Die SubstrateVM ist das Subsystem der GraalVM, das letzten Endes als Teil eines nativen Images selbiges ausführt und die Laufzeitumgebung repräsentiert. Aus dem entsprechenden Readme:
„(A native image) does not run on the Java VM, but includes necessary components like memory management and thread scheduling from a different virtual machine, called „Substrate VM“. Substrate VM is the name for the runtime components (like the deoptimizer, garbage collector, thread scheduling etc.). The resulting program has faster startup time and lower runtime memory overhead compared to a Java VM.“

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

Auf der Seite des GraalVM-Teams finden sich einige Benchmarks, die die Vorteile zeigen, die sich ergeben, wenn zum Beispiel Java Microservices als native Programme ausgeführt werden. Dazu gehört der geringere Speicherbedarf, aber insbesondere die deutlich schnelleren Startzeiten. Sollen Microservices elastisch skaliert werden, ist das eine kritische Metrik. Die hier gezeigten Statistiken sind beeindruckend und werden zweifelsohne für viele Anwendungen erhebliche Vorteile bringen.

Mein Hintergrund: Library-Autor

Ich arbeite beim Hersteller der gleichnamigen Graphdatenbank Neo4j Inc. Dort bin ich Teil des Treiber- beziehungsweise Spring-Data-Neo4j-Teams. Unsere Aufgabe ist die Bereitstellung von Datenbankkonnektoren für unterschiedliche Sprachen, inklusive Java. Zusammen mit Gerrit Meier (@meistermeier) entwickle und pflege ich Spring-Data-Neo4j.

Die Aufgabe des Java-Treibers ist erst einmal simpel: Herstellung einer Netzwerkverbindung zur Datenbank. Diese kann natürlich SSL-verschlüsselt betrieben werden. Darüber hinaus hat der Treiber einige Funktionen, die für die Arbeit mit einem Cluster aus Neo4j relevant sind.

Spring-Data-Neo4j und unser Object Mapping Framework setzen auf dem Treiber auf. Ihre Aufgabe ist es, quasi beliebige Domain-Modelle, die unsere Kunden und Nutzer in Form von annotierten Java-Klassen auf diese Frameworks werfen, in Cypher-Abfragen abzubilden beziehungsweise aus den Ergebnissen von Cypher-Abfragen zu materialisieren. Die Object Mapping Frameworks setzen dazu in der Regel Java Reflection ein. Mit Java Reflection werden benutzte Annotationen, Namen von Attributen etc. gelesen. Das ist natürlich etwas, das auch zur Kompilierungszeit möglich ist, aber die Entscheidung fiel an diesen Stellen ganz klar zugunsten der dynamischen Variante.

Im Zusammenhand mit den beiden Schlagworten SSL und Java Reflection bin ich seit gut eineinhalb Jahren mit der GraalVM beschäftigt. Zum einen möchten wir Anwendungen, die unseren Java-Treiber verwenden, ermöglichen, nativ kompiliert zu werden, ohne auf SSL zu verzichten. Zum anderen möchten wir natürlich mit Version 6 von Spring-Data-Neo4j, einem kompletten Rewrite des Moduls, Teil des Spring Native Projects werden.

Auf diesem Standpunkt kann ich nicht nur großen Nutzen aus dem bestehenden Tooling um SubstrateVM ziehen, sondern auf der anderen Seite auch dazu beitragen, dass Menschen, die unseren Treiber nutzen oder Spring-Data-Neo4j nativ betreiben möchten, nicht selbst den Aufwand betreiben müssen, den wir und unsere Kollegen bei VMWare hinter sich haben, um Spring Data native zu kompilieren.

Frameworks

Neuere Microservices Frameworks wie Quarkus und Micronaut sind teilweise direkt unter dem Aspekt „Kompatiblität mit GraalVM Native“ entwickelt worden und bieten bereits seit 2019 dezidierte Hooks an, um die Erstellung von nativen Anwendungen zu vereinfachen.

In Hinblick auf Spring und Spring Boot existiert das aktuell als experimentell markierte Spring GraalVM Native Projekt. Es bringt sowohl Annotationen und andere Hilfsmittel, die von Entwicklerinnen und Entwicklern für ihre Anwendungen genutzt werden können, als auch bereits vorgefertigte Pakete von Hinweisen, um Spring-Projekte selbst native lauffähig zu machen. Während ich Spring GraalVM Native im Java Magazin 9.20 nur andeutete, schrieb mein Freund Jonas Hecht einen exzellenten Artikel zum Thema Spring GraalVM Native.

Ich will heute aber gar nicht auf eins der konkreten Frameworks eingehen oder diskutieren, welcher Ansatz besser ist. Mir geht es im Folgenden darum, zu zeigen, was alles gemacht werden kann – nicht unbedingt und in jedem Fall muss –, um eine normale Java-Anwendung so zu ertüchtigen, dass sie mit GraalVM Native funktioniert.

Für die meisten Anwendungsentwicklerinnen und -entwickler wird sich die Frage „native oder nicht“ in einigen Jahren sicherlich nicht mehr mit der Wahl des Frameworks entscheiden, sondern wird unter fachlichen Gesichtspunkten beantwortet werden können. Damit das möglich ist, betreiben wir Library-Autor*innen und Framework-Entwickler*innen teils erheblichen Aufwand. Aufwand, der im besten Fall wohlwollend zur Kenntnis genommen wird, im schlechtesten Fall als „Magie“ verschrien ist. Letzteres möchte ich vermeiden. Es ist keine Magie, es ist teilweise Detektivarbeit, und notwendige Schritte können auch „zu Fuß“ nachvollzogen werden. Ich für meinen Teil schätze aber die kollaborative Arbeit, die in diesem Raum von den beteiligten Menschen geleistet wird.

Native Image

Seit meiner ersten Begegnung mit GraalVM während der JCrete 2017 ist ein ganzer Zoo an Werkzeugen entstanden. Eines dieser Werkzeuge heißt Native Image (Kasten: „Installation der GraalVM und des Native-Image-Tools“).

Installation der GraalVM und des Native-Image-Tools

Oracle stellt unter Downloads sowohl die kostenfrei nutzbare Community-Edition als auch die lizenzpflichtige Enterprise-Version für mehrere Betriebssysteme zur Verfügung. Die Installation unterscheidet sich je nach Betriebssystem. Allen Varianten ist gemein, dass das Native-Image-Tool mit Hilfe des GraalVM Component Updater, kurz gu, nachinstalliert werden muss. Der Aufruf lautet – wenn GraalVM korrekt installiert wurde und das entsprechende bin-Verzeichnis im Pfad liegt – gu install native-image.

Unter Linux und macOS kann SDKMan! genutzt werden, um GraalVM inklusive aller Tools zu installieren. Die ersten Schritte werden ausführlich unter Install GraalVM beschrieben.

 

Die vollständigen Quellen der folgenden Codeschnipsel stehen mit passender Verzeichnisstruktur auf meinem GitHub-Account zur Verfügung.
Gegeben sei das triviale Java-Programm in Listing 1.

 
package ac.simons.native_story.trivial;

public class Application {

  public static void main(String... args) {

    System.out.println("Hello, " + (args.length == 0 ? "User" : args[0]));
  }
}

Als Single-Source-File kann es mit jedem neuen JDK ohne Aufruf von javac gestartet werden. Ein java trivial/src/main/java/ac/simons/native_story/trivial/Application.java Michael produziert erwartungskonform Hello, Michael.

Um es hingegen als Input für Native Image zu verwenden, muss es kompiliert werden. Anschließend kann native-image wie folgt aufgerufen werden:

javac trivial/src/main/java/ac/simons/native_story/trivial/Application.java
native-image -cp trivial/src/main/java 
ac.simons.native_story.trivial.Application app

native-image wird Output wie in Listing 2 erzeugen.

 
Build on Server(pid: 21148, port: 50583)
[ac.simons.native_story.trivial.application:21148]    classlist:      71.34 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]        (cap):   1,663.79 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]        setup:   1,850.67 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]     (clinit):     107.06 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]   (typeflow):   2,620.63 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]    (objects):   3,051.08 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]   (features):      83.23 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]     analysis:   5,962.31 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]     universe:     112.18 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]      (parse):     218.57 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]     (inline):     494.42 ms,  4.55 GB
[ac.simons.native_story.trivial.application:21148]    (compile):     912.43 ms,  4.43 GB
[ac.simons.native_story.trivial.application:21148]      compile:   1,828.57 ms,  4.43 GB
[ac.simons.native_story.trivial.application:21148]        image:     465.08 ms,  4.43 GB
[ac.simons.native_story.trivial.application:21148]        write:     135.90 ms,  4.43 GB
[ac.simons.native_story.trivial.application:21148]      [total]:  10,465.92 ms,  4.43 GB

Nach einiger Zeit steht das native Binary app zur Verfügung und der Aufruf ./app Michael produziert dieselbe Ausgabe wie zuvor – nur schneller. Zum Aufruf von native-image stehen entsprechende Maven- und Gradle-Plug-ins zur Verfügung, die in den nachfolgenden Beispielen genutzt werden.

Viel komplizierter ist es eigentlich nicht, aus dem JIT-Bytecode ein AOT Image zu bauen, wären da nicht Frameworks, die Reflection nutzen, oder augenscheinlich triviale Dinge wie Ressourcen.

Ein fiktives Framework

Für die folgenden Erklärungen werde ich aus dem trivialen Hello-World-Beispiel ein unnütz kompliziertes Java-Programm machen, das hoffentlich einige der Dinge zeigt, die zumindest in einigen Frameworks und vermutlich auch Anwendungen „so passieren“.
Die Grußworte kommen natürlich aus einem Service (Listing 3).

 
public interface Service {

  String sayHelloTo(String name);

  String getGreetingFromResource();
}

Services fallen nicht einfach so vom Himmel, wir nutzen eine Factory wie in Listing 4. Diese könnte zum Beispiel notwendige Abhängigkeiten besorgen und in den Service injizieren. Im Beispiel selbst instanziiert sie dynamisch eine Implementierung des Service. Dynamisch, da keine Compile-Zeit-Konstante mit dem Namen der Implementierung genutzt wird, sondern der Name dynamisch bestimmt wird.

 
public class ServiceFactory {

  public Service getService() {
    Class<Service> aClass;
    try {
      aClass = (Class<Service>) Class.forName(ServiceImpl.class.getName());
      return aClass.getConstructor().newInstance();
    } catch (Exception e) {
      throw new RuntimeException("¯\\_(ツ)_/¯", e);
    }
  }
}

Unsere Beispielimplementierung, gezeigt in Listing 5, nutzt darüber hinaus einen Zeit-Service sowie Textressourcen.

 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.util.stream.Collectors;

public class ServiceImpl implements Service {

  private final TimeService timeService = new TimeService();

  @Override
  public String sayHelloTo(String name) {
    return "Hello " + name + " from ServiceImpl at " + timeService.getStartupTime();
  }

  @Override
  public String getGreetingFromResource() {
    try (BufferedReader reader = new BufferedReader(
    new InputStreamReader(this.getClass().getResourceAsStream("/content/greeting.txt")))) {

      return reader.lines()
      .collect(Collectors.joining(System.lineSeparator()));
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }
}

Der Zeitservice ist natürlich ebenfalls vollkommen naiv implementiert und verlässt sich auf die Konstante STARTED_AT auf Klassenebene, um den Startzeitpunkt der JVM zu speichern. Das ist aus mehreren Gründen naiv, soll hier aber nicht thematisiert werden.

Zu guter Letzt wird aus dem einfachen Main-Programm das in Listing 6 dargestellte Biest.

 
import java.lang.reflect.Method;

public class Application {

  public static void main(String... a) {

    Service service = new ServiceFactory().getService();
    System.out.println(service.sayHelloTo("GraalVM"));

    System.out.println(invokeGreetingFromResource(service, "getGreetingFromResource"));
  }

  static String invokeGreetingFromResource(Service service, String theName) {

    try {
      Method method = Service.class.getMethod(theName);
      return (String) method.invoke(service);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}

Dieses konstruierte Beispiel zielt darauf, viele der unter Limitations of GraalVM Native Image dargestellten Punkte aufzuzeigen. Adressiert werden dabei:

  • Reflektive Erzeugung von Instanzen mit Hilfe dynamischer Werte: ServiceImpl.class.getName() kann nicht als konstanter Klassenname erkannt und entsprechend behandelt werden
  • Dynamische Methodenaufrufe (was analog auch für den Zugriff auf Felder gilt)
  • Zugriff auf Resource
  • Statische Initialisierung von Feldern während der Initialisierung von Klassen mit Werten, die vom Zeitpunkt der Initialisierung abhängen

Wird diese Anwendung nun als JAR-Datei paketiert und mit einem entsprechendem Manifest und Main-Eintrag versehen, kann sie wie folgt aufgerufen werden:

 
java -jar only-on-jvm/target/only-on-jvm-1.0-SNAPSHOT.jar
Hello GraalVM from ServiceImpl at 2020-09-15T09:37:37.832141Z
Hello, from a resource.

Das Native-Image-Tool kann nicht nur mit Bytecodedateien umgehen, sondern auch mit JAR-Archiven, sofern diese ein entsprechendes Manifest mit Zeiger auf die main-Klasse haben. Listing 7 zeigt, was passiert, wenn versucht wird, das paketierte Framework-Programm nativ zu kompilieren.

 
native-image -jar only-on-jvm/target/only-on-jvm-1.0-SNAPSHOT.jar
...
Warning: Reflection method java.lang.Class.forName invoked at ac.simons.native_story.ServiceFactory.getService(ServiceFactory.java:8)
Warning: Reflection method java.lang.Class.getMethod invoked at ac.simons.native_story.Application.invokeGreetingFromResource(Application.java:18)
Warning: Reflection method java.lang.Class.getConstructor invoked at ac.simons.native_story.ServiceFactory.getService(ServiceFactory.java:9)
Warning: Aborting stand-alone image build due to reflection use without configuration.
Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
Build on Server(pid: 26437, port: 61293)
...
Warning: Image 'only-on-jvm-1.0-SNAPSHOT' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).

Wir sehen etliche Warnungen, und alle künstlich herbeigeführten Probleme werden zuverlässig erkannt. Das ist schon mal gut. Zu guter Letzt wird dennoch ein Binary erzeugt. Dieses Binary ist aber ein sogenanntes fallback image. Es benötigt ein lokal installiertes JDK zur Laufzeit. Falls dieses nicht im Pfad ist, führt der Start zu einem Fehler:

 
./only-on-jvm-1.0-SNAPSHOT
Error: No bin/java and no environment variable JAVA_HOME

Zu ergänzen bleibt hier: Das Binary ist nicht wirklich kleiner als ein pures SubstrateVM Binary, obwohl es auf das externe JDK angewiesen ist, und es ist nichtsdestotrotz plattformspezifisch. Ein auf macOS erzeugtes Binary wird nicht auf Linux starten. native-image kann mit der Option –no-fallback aufgerufen werden und bricht mit einem Fehler ab, anstatt ein Fallback Image zu erzeugen.

Welche Werkzeuge gibt es nun, um diese Probleme zu beheben? Zuerst adressieren wir die offensichtlichen: das dynamische Laden von Klassen und den reflektiven Zugriff auf Methoden und Felder. Dazu gibt es zwei Möglichkeiten: Wir können die benötigten Klassen aufzählen und explizit im Image inkludieren – oder sie ersetzen.

Aufzählung von Klassen, Methoden und Feldern die verfügbar sein müssen

Die Codeanalyse durch die GraalVM erkennt Aufrufe wie Class.forName(), die die Anwesenheit von Klassen erforderlich machen. Ist die GraalVM in der Lage, die Parameter solcher Aufrufe auf Compile-Zeit-Konstanten zurückzuführen, werden die entsprechenden Klassen und Methoden automatisch mit ins Image eingeschlossen. Unser Beispiel jedoch ist so formuliert, dass das nicht geht. An dieser Stelle kommt explizite Konfiguration ins Spiel. native-image wird über den Parameter -H:ReflectionConfigurationFiles der Ort einer JSON-Datei mitgeteilt, die wie in Listing 8 aussehen kann.

 
[
  {
    "name" : "ac.simons.native_story.ServiceImpl",
    "allPublicConstructors" : true
  },
  {
    "name" : "ac.simons.native_story.Service",
    "allPublicMethods" : true
  }
]

Die Konfiguration in Listing 8 bedeutet, dass wir den reflektiven Zugriff auf alle öffentlichen Konstrukte der Klasse ServiceImpl sowie auf alle öffentlichen Methoden des Service Interface erlauben. SubstrateVM wird diese Klassen mit in das binäre Image nehmen. Weitere Optionen dieser Konfiguration werden im entsprechenden Handbuch beschrieben.

Es besteht die Möglichkeit, eine zentrale Properties-Datei für das Tool zu erstellen, die es erspart, ein oder mehrere –H:ReflectionConfigurationFiles=/path/to/reflectconfig-Optionen anzugeben: Wird in den Resources einer JAR-Datei unter META-INF/native-image (sinnigerweise in einem Unterpfad wie META-INF/native-image/GROUP_ID/ARTIFACT_ID, um Konflikte mit anderen Libraries zu vermeiden) eine Datei namens native-image.properties angelegt, so wird diese zur Steuerung des Tools verwendet. Über die Eigenschaft Args werden die entsprechenden Kommandozeilenparameter gesetzt. Eine erste Variante sieht so aus:

 
Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json

Das Programm wird nun ohne Warnhinweise in ein natives Binary kompiliert. Allerdings wird es mit einer NullpointerException abstürzen, da es /content/greeting.txt nicht mit ins Image geschafft hat. Die entsprechende Aufzählung wird in Listing 9 gezeigt. Das Listing repräsentiert die resources-config.json.

 
{
  "resources": [
    {
      "pattern": ".*greeting.txt$"
    }
  ]
}

Diese Datei muss native-image explizit mitgeteilt werden. Der Parameter (Kasten: „Zwei Varianten“) lautet –H:ResourceConfigurationResource. die entsprechende zweite Variante der Steuerungsdatei sieht so aus:

 
Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json \
       -H:ResourceConfigurationResources=${.}/resources-config.json

Zwei Varianten

Die Parameter zur Steuerung der Konfiguration durch JSON-Dateien kommen in zwei Varianten. Einmal als XXXConfigurationResources und einmal als XXXConfigurationFiles. Die Resource-Form wird für alle Resources innerhalb eines Artefakts genutzt, die Files-Variante für externe Dateien. Die Wildcard ${.} verhält sich entsprechend. Die Hilfe gibt Auskunft über die möglichen Parameter: native-image –expert-options | grep Configuration.

 

Das Binary läuft ohne Ausführungsfehler (Listing 10), aber läuft es auch fehlerfrei? Nicht ganz. Nachdem ich das Binary erzeugt habe, habe ich den Text fortgesetzt. Es ist Zeit vergangen, aber das Programm zeigt auch bei wiederholtem Aufruf immer dieselbe Zeit an. Der Grund ist im TimeService zu suchen. Dieser Service hält eine Konstante vom Typ Instant, die beim ersten Zugriff auf die TimeService-Klasse laut Java Language Specification (JLS) zu initialisieren ist. Der erste Zugriff ist aber der Zugriff zur Kompilierungszeit des Images.

 
./reflection-config-1.0-SNAPSHOT
Hello GraalVM from ServiceImpl at 2020-09-15T15:02:47.572800Z
Hello, from a resource.

Wird eine Klasse schon zur Kompilierungszeit initialisiert, spart das natürlich Laufzeit, kann aber – wie in diesem Fall – zu Fehlern führen. Hier wird beschrieben, welche Klassen als Safe betrachtet werden und welche immer zur Laufzeitinitialisierung führen.

Für die Version von GraalVM, die ich zur Erstellung des Beispiels und des Artikels genutzt habe, wundert es mich, warum GraalVM diesen Zugriff als sicher betrachtet, da die java.time.* definitiv auf native Calls zurückgreifen. Für das Beispiel müssen wir daher sicherstellen, dass der kritische TimeService zur Laufzeit initialisiert wird. Das wird in einer dritten Variante der native-image.properties über den –initialize-at-run-time-Parameter gemacht:

 
Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json \
       -H:ResourceConfigurationResources=${.}/resources-config.json \
       --initialize-at-run-time=ac.simons.native_story.TimeService

Damit entsteht ein korrekt lauffähiges, schnelles Binary.

Substitutions (Ersetzungen)

In meinem Team habe ich sicherstellen müssen, dass der Neo4j-Java-Treiber nativ läuft. Das war erheblich mehr Aufwand und nicht durch reine Konfiguration zu erledigen. Wir setzen Netty für SSL-Verbindungen ein. Damit entsprechende Infrastruktur überhaupt den Weg in das native Binary findet, müssen folgende Parameter gesetzt sein: –H:EnableURLProtocols=http,https –enable-all-security-services -H:+JNI. Quarkus und andere Frameworks bieten dazu entsprechende Hooks an. Die Parameter können wie in den anderen Beispielen in native-image.properties ergänzt werden.

Andere Dinge brauchten einen aktiven Ansatz, sogenannte Ersetzungen. Hier kommt das GraalVM-SVM-Projekt ins Spiel. SVM ist eine provided Dependency, die auf der JVM nicht bemerkt, aber von der SubstrateVM entsprechend ausgewertet wird. Listing 11 zeigt die entsprechenden Koordinaten.

 
<dependency>
  <groupId>org.graalvm.nativeimage</groupId>
  <artifactId>svm</artifactId>
  <version>${native-image-maven-plugin.version}</version>
  <!-- Provided scope as it is only needed for compiling the SVM substitution classes -->
  <scope>provided</scope>
</dependency>

Damit können nun package-private-Klassen erstellt werden, die innerhalb der SubstrateVM ganze Zielklassen oder Methoden austauschen oder löschen. In Listing 12 wird die reflektive Erstellung des Service durch einen konkreten Aufruf ersetzt.

 
import ac.simons.native_story.Service;
import ac.simons.native_story.ServiceImpl;

import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

@TargetClass(className = "ac.simons.native_story.ServiceFactory")
final class Target_ac_simons_native_story_ServiceFactory {

  @Substitute
  private Service getService() {
    return new ServiceImpl();
  }
}

@TargetClass(className = "ac.simons.native_story.Application")
final class Target_ac_simons_native_story_Application {

  @Substitute
  private static String invokeGreetingFromResource(Service service, String theName) {

    return "#" + theName + " on " + service + " should have been called.";
  }
}

class CustomSubstitutions {
}

Die Namen der Ersetzungen spielen keine Rolle, die @TargetClass-Attribute müssen hingegen exakt sein. In unserem fiktiven Beispiel kann damit reflection-config.json entfallen. Die Ersetzungen sind sehr mächtig. Die entsprechenden Ersetzungen zeigen zum Beispiel im Neo4j-Java-Driver, was möglich ist.

Der Tracing-Agent

Da wir nun den Mechanismus hinter Native Image und seiner Konfiguration kennengelernt haben, können wir uns das Leben einfacher machen. Die GraalVM stellt den Reflection-Tracing-Agenten zur Verfügung. Dieser JVM-Agent kann etliche der hier gezeigten Dinge automatisch erkennen, vorausgesetzt, die entsprechenden Codepfade werden durchlaufen.

Wird das Beispiel mit aktiviertem Agenten als JVM-Programm ausgeführt, erkennt er in diesem Fall alle Probleme. Der Agent steht im GraalVM OpenJDK zur Verfügung. Er generiert Dateien wie in Listing 13.

 
java --version
openjdk 11.0.7 2020-04-14
OpenJDK Runtime Environment GraalVM CE 20.1.0 (build 11.0.7+10-jvmci-20.1-b02)
OpenJDK 64-Bit Server VM GraalVM CE 20.1.0 (build 11.0.7+10-jvmci-20.1-b02, mixed mode, sharing)

java  -agentlib:native-image-agent=config-output-dir=only-on-jvm/target/generated-config -jar only-on-jvm/target/only-on-jvm-1.0-SNAPSHOT.jar
Hello GraalVM from ServiceImpl at 2020-09-16T07:12:27.194185Z
Hello, from a resource.

dir only-on-jvm/target/generated-config
total 32
14417465 0 drwxr-xr-x  6 msimons  staff  192 16 Sep 09:12 .
14396074 0 drwxr-xr-x  8 msimons  staff  256 16 Sep 09:12 ..
14417471 8 -rw-r--r--  1 msimons  staff  278 16 Sep 09:12 jni-config.json
14417468 8 -rw-r--r--  1 msimons  staff    4 16 Sep 09:12 proxy-config.json
14417470 8 -rw-r--r--  1 msimons  staff  226 16 Sep 09:12 reflect-config.json
14417469 8 -rw-r--r--  1 msimons  staff   77 16 Sep 09:12 resource-config.json

Der Agent generiert eine Konfiguration, die schärfer ist als unsere eigene und somit zu einem kleineren Binary führt, wie ein Blick in die reflect-config.json in Listing 14 zeigt.

 
[
  {
    "name":"ac.simons.native_story.Service",
    "methods":[{"name":"getGreetingFromResource","parameterTypes":[] }]
  },
  {
    "name":"ac.simons.native_story.ServiceImpl",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  }
]

Der Agent ist ein fantastisches Tool, um eine Ausgangsbasis für die eigene Konfiguration des nativen Image zu haben.

Fazit

Es benötigt nicht viel Ehrgeiz, Java und seine Features so einzusetzen, dass Anwendungen oder Frameworks entstehen, die nicht mit GraalVMs Native Image harmonieren. Das obige Beispiel ist natürlich erzwungen konstruiert, aber die Erfahrung zeigt, dass genau solche Dinge nicht nur in alten Anwendungen und Frameworks existieren, sondern auch neu noch so geschrieben werden.

Reflektion wird in etlichen Frameworks wie Spring Core, Hibernate ORM, Neo4j OGM und Spring Data genutzt, um dynamisch Attribute von Klassen zu ermitteln und Abfragen zu generieren oder Instanzen zu hydrieren. Dependency Injection Frameworks nutzen Reflection, um Injektoren zu erstellen und Abhängigkeiten zu verbinden. Object Mapper haben natürlich im Vorfeld keine Vorstellung davon, wie ein Domain Model aussehen mag.

Viele der erwähnten und gezeigten Dinge könnten bereits elegant zur Kompilierungszeit gelöst werden, zum Beispiel durch Prozessoren, die zur Kompilierungszeit Annotationen lesen und Indizes schreiben oder Bytecode generieren. Das ist zum Beispiel der von Micronaut verfolgte Ansatz. Hibernate in Quarkus generiert Indizes für das Domain Model.

Ältere Frameworks wie Spring, die über etliche andere Libraries integrieren, haben diesen Luxus nicht. Dennoch: Auch dort wird das Tooling verbessert. Im Rahmen von Spring-GraalVM-Native durfte ich unsere aktuelle Spring-Data-Neo4j-Version ertüchtigen.

Die Mittel, die Frameworks wie Quarkus, Spring-Native und Micronaut zur Verfügung stehen – sei es in Form von programmatischen Hooks, deklarativen Annotationen oder anderem – basieren schlussendlich alle auf Konfiguration und Substitutions wie oben gezeigt. Zumindest von Spring-Native weiß ich darüber hinaus, dass SubstrateVM-Plug-ins erstellt worden sind, die der GraalVM Wissen über das Framework mitgeben.

Die Werkzeuge rund um GraalVM sind großartig und adressieren obige Probleme sowie Dinge wie JNI und Proxies. Sie sind gut dokumentiert. Es reicht allerdings nicht aus, einfach nur Klassen auf das Native-Image-Tool zu werfen und zu glauben, die entstehenden Binaries verhalten sich in jedem Fall korrekt.

Im nächsten Jahr wird sich die Welt weiterbewegt haben und es wird zum guten Ton gehören, dass Frameworks native Kompilierung unterstützen. Dabei sollte nicht vergessen werden, dass dort keine Magie, sondern sorgfältige, jedoch teilweise ermüdende Arbeit geschieht. Ermüdend deshalb, weil es erstaunlich ist, was alles verfügbar sein muss, damit eine Standardanwendung im Kontext eines Frameworks funktioniert. Die Vorteile einer nativen Framework-basierten Anwendung kommen nicht umsonst.

Hinweise

Die originale englischsprachige Version erschien im September 2020 auf meinem Blog. Mein Dank geht an meine Freunde und Kollegen Gerrit, Michael und Gunnar sowie an Oleg von Oracle, die den Artikel gegengelesen und die gröbsten Fehler und Schnitzer korrigiert haben.

Die Beispiele wurden mit GraalVM 20.1.0 CE Edition, sowohl in der JDK-8- als auch in der JDK-11-Version getestet. Es ist nicht auszuschließen, dass neuere Varianten bessere Heuristiken in der automatischen Erkennung obiger Szenarien haben.

 

Quarkus-Spickzettel


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

 

Jetzt herunterladen!

The post Native Java-Programme auf der GraalVM appeared first on JAX.

]]>