micronaut - JAX https://jax.de/tag/micronaut/ Java, Architecture & Software Innovation Fri, 18 Oct 2024 13:04:11 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Java Module und JPMS: Endlich bereit für den Einsatz? https://jax.de/blog/java-module-jpms-einfuehrung-vorteile-herausforderungen-best-practices/ Mon, 08 Apr 2024 07:41:42 +0000 https://jax.de/?p=89614 Wer im Web nach Java-Modulen oder dem Java Platform Module System (JPMS) sucht, stößt vor allem auf Kritik. Ist das JPMS also eher Flop als top? Auch wenn Module nur sehr langsam im Java Ecosystem angenommen werden, gibt es Projekte, die sie mit großem Erfolg einsetzen. Wir schauen die Vor- und Nachteile an, erfahren, mit welchen Best Practices man ein großes Projekt mit JPMS zum Erfolg führen kann, betrachten, welche Probleme es gibt, wann der Einsatz von JPMS sinnvoll ist, und zeigen, was Entwickler von (Open-Source-)Bibliotheken auch dann beachten sollten, wenn sie kein Interesse an Java-Modulen haben.

The post Java Module und JPMS: Endlich bereit für den Einsatz? appeared first on JAX.

]]>
Jeder Java-Entwickler kennt das: Die Sichtbarkeiten public, protected, default und private sind nicht immer hilfreich. Wer hat sich nicht schon gewünscht, dass etwas nur für das eigene Package sowie für Sub-Packages sichtbar wäre? Noch schlimmer ist das Problem der Namespace Pollution: In einem großen Projekt sammeln sich unzählige Bibliotheken im Classpath, und die Auto Completion für generische Begriffe wie List, Entity, Metadata oder StringUtil liefert alle möglichen und unmöglichen Treffer und im Worst Case den richtigen erst ganz am Ende der Liste (Abb. 1).

Abb. 1: Namespace Pollution

Lösungen für diese Probleme sowie eine Alternative für den mit Java 17 abgekündigten Security Manager [1] bieten Java-Module. Der Einsatz von Modulen ist eine grundlegende Entscheidung, da das Java-Modul-System nur ganz oder gar nicht verwendet werden kann. Wenn man es nutzt, kommen neben den Vorteilen auch grundsätzliche Veränderungen im Verhalten dazu. Im Open-Source-Projekt m-m-m [2] nutze ich das JPMS extensiv und mit großer Begeisterung. Ein weiteres großes Java-Open-Source-Projekt, das JPMS konsequent nutzt, ist Helidon [3]. Insgesamt wurde JPMS aber bisher vom Java Ecosystem nur sehr zögerlich angenommen.

Stay tuned

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

 

Dieser Artikel versteht sich als eine logische Fortsetzung von [4] und will Mut machen, sich mit dem JPMS auseinanderzusetzen. Er geht auf Details und Best Practices ein, die über die typischen Hello-World-Tutorials zu Java-Modulen im Web hinausgehen.

Java-Module

Begriffe wie Modul oder auch Komponente werden in der IT nicht präzise definiert und daher in verschiedenen Kontexten unterschiedlich verwendet, was zu großer Verwirrung führen kann. Relativ gut kann man sich darauf einigen, dass es sich dabei um geschlossene Funktionseinheiten mit definiertem API handelt und dadurch eine bessere Kapselung erreicht wird. In Java ist ein Modul eine Einheit, wie man es von einer JAR-Datei oder einem Maven-Modul kennt, die über einen spezifischen Moduldeskriptor verfügt (oder für die als Automatic Module ein Moduldeskriptor dynamisch erzeugt wird).

Der Moduldeskriptor

Dieser Deskriptor wird im Source Code in der Datei module-info.java im Default-Package definiert. Es handelt sich hierbei um ein völlig anderes Konstrukt als Class, Interface, Enum, Annotation oder Record. Beim Build wird aus dem Modul eine JAR-Datei, die im Wurzelverzeichnis eine module-info.class-Datei enthält. Der Modul-Deskriptor definiert eine ganze Reihe wichtiger Eigenschaften des Moduls:

  • Name des Moduls
  • Abhängigkeiten von anderen Modulen
  • Liste der exportieren Packages
  • Angebotene sowie verwendete Services
  • Reflection-Zugriffe

Modulname und Packages

Der Name des Moduls dient als globaler, weltweit eindeutiger Identifier, um das Modul zu referenzieren. Er folgt den gleichen Syntaxregeln wie ein Package, ist davon aber prinzipiell völlig unabhängig. Nutzer von Maven oder Gradle können es sich als Kombination von Group-ID und Artifact-ID vorstellen. Es hat sich als Best Practice herausgestellt, Package- und Modulnamen sowie Artifact-ID in Einklang zu bringen. Das bedeutet, dass sich der Modulname als Namensraum bzw. Präfix in den Packages, die im Modul enthalten sind, wiederfindet. Diese Konvention hat auch den Vorteil, dass Entwickler, die ein Modul verwenden möchten, einfach zwischen Modulnamen und Packages hin und her mappen können, ohne in einer externen Dokumentation nachschauen zu müssen. Eine Klasse aus einem bekannten Package zu verwenden, heißt zwar noch nicht, dass der Modulname mit dem Package-Namen übereinstimmt, aber wenn der Modulname ein Präfix davon ist, kann er in modernen IDEs mit Autovervollständigung gezielt gefunden werden.

Zudem sind alle Java-Entwickler bereits mit den Best Practices für Packages samt den Namensräumen als „umgedrehten Domainnamen“ vertraut. Zur Veranschaulichung stellen wir uns als konkretes Beispiel vor, dass wir für awesome-restaurant.com arbeiten und gerade das Backend für Onlinereservierungen und -bestellungen bauen. Der Java-Code verwendet für Bestellungen bereits Packages wie com.awesome_restaurant.online.order.domain oder com.awesome_restaurant.online.order.repository. Da ein Minuszeichen in Package-Namen nicht zulässig ist, wurde es beim Mapping der Domain auf Java-Packages durch einen Unterstrich ersetzt (man kann es natürlich auch weglassen oder durch einen Punkt ersetzen).

Möchte ich nun das Backend als Java-Modul bereitstellen, könnte meine module-info.java-Datei so aussehen:

module com.awesome_restaurant.online.order {
  requires jakarta.persistence;
  // ...
  exports com.awesome_restaurant.online.order.domain;
  // ...
}

Abhängigkeiten von anderen Modulen

Abhängigkeiten werden in der module-info.java-Datei durch das Schlüsselwort requires eingeleitet. Am Ende folgt der Name des angeforderten Moduls. Im obigen Beispiel ist das jakarta.persistence, der neue Modulname der JPA aus Jakarta EE.

Optional kann requires noch einer der beiden folgenden Modifier folgen:

  • transitiv: Damit wird die Abhängigkeit transitiv, d. h. Module, die mein Modul anfordern, erhalten diese Abhängigkeit automatisch ebenfalls. Andernfalls ist das nicht so und der Code der Abhängigkeit ist für sie zunächst unsichtbar. Anders als bei Maven oder Gradle betrifft diese Eigenschaft nur die Sichtbarkeit und ändert nichts daran, dass die Abhängigkeit zur Laufzeit gebraucht wird. Somit wird beim JPMS sehr genau zwischen transitiven und nichttransitiven Abhängigkeiten unterschieden: Ich deklariere Abhängigkeiten von einem anderen Modul nur dann als transitiv, wenn es für die Verwendung meines Moduls (via API) auch für den Aufrufer sichtbar sein muss oder etwas völlig Querschnittliches ist, wie z. B. das SLF4J API zum Logging. Tauchen z. B. Typen aus diesem Modul in meinem eigenen API auf, dann deklariere ich die Abhängigkeit als transitiv. Andernfalls erhalte ich von meiner IDE eine Warnung. Abhängigkeiten, die ich nur intern für meine Implementierungen benötige, deklariere ich nicht transitiv und vermeide damit das Namespace-Pollution-Problem.
  • static: Damit wird eine Abhängigkeit optional, d. h., das angeforderte Modul ist zur Laufzeit nicht erforderlich und das JPMS wirft keinen Fehler, wenn es fehlt. Natürlich muss ich meinen Code so schreiben, dass er damit umgehen kann. Am Schlüsselwort static sieht man mal wieder, dass der Java-Compiler auf reservierte Schlüsselwörter fokussiert ist. Hier wurde also offensichtlich ein bereits in Java reserviertes Schlüsselwort missbraucht, was für diesen Kontext nicht gerade selbsterklärend ist. Es wäre wesentlich intuitiver gewesen, hier einfach optional statt static zu schreiben.

Damit kommen wir zur ersten starken Einschränkung bei der Verwendung des JPMS: In meinem Modul sehe ich fremden Code nur dann, wenn er aus einem Modul kommt, das als Abhängigkeit per requires importiert wird (inklusive transitiver Abhängigkeiten). Das betrifft sogar Klassen aus dem JDK – bis auf solche, die aus dem Modul java.base stammen. Das ist super, weil es das eingangs erwähnte Problem der Namespace Pollution löst.

 

Gleichzeitig bedeutet es aber auch, dass ich nur Code importieren und auf ihn zugreifen kann, wenn er selbst wiederum als Java-Modul bereitsteht. Und damit beginnt schon ein großes Dilemma: Die coole Open-Source-Bibliothek, die ich gerne nutzen möchte, ist kein Modul? Dann kann ich sie in meinem Modul erst mal nicht verwenden.

An dieser Stelle möchten wir die verschiedenen Arten von Java-Modulen differenzieren:

  • Systemmodule werden vom JRE bzw. JDK bereitgestellt. Mit dem Projekt Jigsaw (auf Deutsch Stichsäge) hat Oracle die Mammutaufgabe gemeistert, das JDK als historisch gewachsenen Codehaufen sauber in Java-Module mit definierten Abhängigkeiten zu zerschneiden. Das beschleunigt den Start der JVM. Module wie java.desktop (Swing) oder java.rmi mit all ihren Klassen müssen nicht geladen werden, wenn man sie nicht braucht.
  • Custom- oder Application-Module sind Module des Java Ecosystem, die explizit über einen Moduldeskriptor (module-info.class) verfügen, aber nicht zum JDK gehören. Auf ihnen liegt der Fokus dieses Artikels.
  • Automatic Module sind ein Behelfsmittel für das angesprochene Problem, eine Bibliothek aus einem Modul zu nutzen, die selbst keinen Moduldeskriptor hat. Entwickler von (Open-Source-)Bibliotheken für Java, die keine Lust haben, Moduldeskriptoren für ihre JARs zu pflegen, sollten unbedingt in ihrer Manifest-Datei einen Modulnamen über die Property Automatic-Module-Name festlegen. Damit kann das JAR über diesen Modulnamen importiert werden und alle Packages sind automatisch exportiert. Ist auch das nicht der Fall, bleibt als letzter Notanker nur die Möglichkeit, die Bibliothek nach bestimmten Regeln über den Kernbestandteil des Dateinamens der JAR-Datei anzusprechen. Das mag zum Ausprobieren einen Versuch wert sein – ich rate jedoch davon ab, es als finale Lösung für ein Release zu verwenden. Stattdessen sollte man die Autoren der Bibliothek per Feature-Request-Ticket überzeugen, einen Automatic-Module-Name festzulegen oder man verzichtet einfach auf den Einsatz der Bibliothek und sucht eine Alternative.
  • Unnamed Module: In Java gibt es genau ein Unnamed Module, in dem automatisch alles landet, was kein Modul ist. Andere Module können aber nicht auf Code daraus zugreifen.

Neue Spielregeln für Packages und Reflection

Die Spielregeln beim JPMS sind vom Prinzip her das Gegenteil von dem, was man als Java-Entwickler mit den normalen Classpaths gewohnt ist: Im normalen Java ist grundsätzlich alles sichtbar und erlaubt, außer, wir schränken es mit Schlüsselwörtern, wie z. B. private ein. Packages selbst sind immer public. Beim JPMS ist mein eigener Code zunächst nur innerhalb meines Moduls sichtbar. Will ich das ändern, muss ich Packages explizit exportieren. Hierzu verwenden wir in der module-info.java-Datei das Schlüsselwort exports, gefolgt von dem Package, das wir veröffentlichen möchten. Das geht nur für Packages, die im Modul auch existieren und nicht leer sind. Ich muss also jedes einzelne Package, das ich exportieren möchte, explizit auflisten.

Eine Syntax, die alle Packages automatisch exportiert, ist nicht vorgesehen. Das ist auch sinnvoll, denn so wird verhindert, dass ein nachträglich hinzugefügtes Package aus Versehen exportiert und damit öffentlich wird. Es geht hierbei um das Geheimnisprinzip, das der erfahrene Java-Entwickler aus der Softwarearchitektur kennt. Nur beim Automatic Module werden automatisch alle Packages exportiert und damit das gesamte Verhalten von JARs im Classpath für Module simuliert.

Die Restriktionen gehen aber noch weiter: Ich kann keinen Code in ein Package platzieren, in das bereits ein anderes Modul Code platziert – auch wenn ich als Autor dieses anderen Moduls alles unter meiner Kontrolle habe und sogar dann nicht, wenn das Package nicht exportiert ist. Dieses sogenannte Split-Package-Konstrukt wird in konventionellem Java manchmal eingesetzt, um Sichtbarkeiten bewusst zu hintergehen: Ich lege meine eigene Klasse ins Package org.hibernate, um an eine Methode von Hibernate zu kommen, die protected oder default als Sichtbarkeit hat. Und wenn dieser Hack nicht reicht, hole ich den Holzhammer raus und nutze Reflection und mache mit der Methode setAccessible(true) alles zugänglich. Um die Katze vollständig aus dem Sack zu lassen: Reflection geht in JPMS per Default auch nicht mehr, außer, sie wird explizit erlaubt.

Als ich mich zum ersten Mal mit dem JPMS auseinandergesetzt habe, war spätestens das der Moment, wo ich geschluckt und das Thema erstmal beiseite gewischt habe. Das ist vermutlich auch der Grund, warum viele Artikel im Web so negativ über das JPMS berichten und vom Einsatz abraten. Das ist aber sehr schade, denn meines Erachtens gründet diese Reaktion ausschließlich in der Macht der Gewohnheit.

IT ist ständig im Wandel und Patterns, die vor zehn oder zwanzig Jahren unser Denken bestimmt haben, sind heute überholt und wurden durch neue ersetzt. Aus Sicht von IT-Security und Zero Trust ist das Design des JPMS genial für einen Neustart. Sind nicht gerade Reflection und Serialisierung die Wurzel aller Übel der gravierenden CVEs in der Java-Welt? Und ist nicht die Verwendung von (Deep) Reflection zur Laufzeit spätestens seit dem Erscheinen von GraalVM, Quarkus und Co. ein Konstrukt des vergangenen Jahrtausends? Mit JEP 411 wurde der Security Manager beerdigt und man sollte sich die Frage stellen, ob man nicht auf einem sinkenden Schiff segelt, wenn man sich nicht mit dem JPMS auseinandersetzt.

Wer sich in diesem Sinne auf das JPMS einlassen kann, der wird sich nach einer gewissen Eingewöhnungszeit fragen, warum Java nicht von Anfang an so konzipiert wurde. Mit dem konkreten Ausprobieren kommt auch die Erfahrung, um die Vor- und Nachteile abwägen zu können und zu wissen, in welchem Vorhaben der Einsatz sinnvoll ist und wo eher nicht. Der Vollständigkeit halber sei noch erwähnt, dass das Schlüsselwort open vor module im Moduldeskriptor das ganze Modul per Reflection freigibt oder das Schlüsselwort opens dies nur für ein explizites Package erlaubt (analog zu exports). Im Allgemeinen rate ich von diesem Konstrukt jedoch ab.

Stay tuned

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

 

In meiner Zeit vor dem JPMS verwendete ich in meinen Packages Segmente wie api oder impl, um das auszudrücken, was ich ohne JPMS nicht ausdrücken konnte. Mit externen Tools wie SonarQube konnte ich dann ungewollte Zugriffe aus einem api-Package auf ein impl-Package aufdecken. Inzwischen kann ich auf künstliche Segmente wie api verzichten und über meinen Moduldeskriptor festlegen, welches Package zum API gehört und daher exportiert wird. Die Implementierungen können immer noch in impl-Sub-Packages liegen, wenn man es explizit machen möchte. Da diese für andere Module unsichtbar sind, können die Packages und sämtliche enthaltenen Klassen nach Belieben umbenannt werden, ohne Gefahr zu laufen, dass ein Nutzer des Moduls beim nächsten Release Compilerfehler erhält. Das Beste ist, dass alle diese internen Klassen und ihre Methoden nach Belieben public sein dürfen und der Spagat zwischen flexibler interner Nutzung und Verhinderung ungewollter externer Nutzung beendet ist.

Was aber, wenn mein Projekt aus vielen Modulen besteht und ich bestimmte Zugriffe für eigene Module erlauben und für fremde Module verbieten will? Kein Problem! Im Moduldeskriptor kann ich dazu beim exports-Statement nach dem Package und dem Schlüsselwort to einen oder mehrere Modulnamen angeben, auf die der Zugriff eingeschränkt wird. In unserem Beispiel könnte das so aussehen:

exports com.awesome_restaurant.online.order.repository to
com.awesome_restaurant.online.order.app,
com.awesome_restaurant.online.reservation.app;

Wichtig zum Verständnis ist, dass nach exports ein Package kommt und nach to Modulnamen. Wildcards, um gleich einen ganzen Namensraum zu erlauben (z. B. com.awesome_restaurant.*), werden dabei leider nicht unterstützt.

Services 2.0

In Java gibt es seit der Version 1.6 den ServiceLoader, um Services dynamisch anzufordern, die sich über textuelle Dateien unter META-INF/services konfigurieren lassen. Das Konstrukt hat keine übermäßige Verbreitung gefunden und ist vielen Java-Entwicklern unbekannt. In Frameworks wie Spring oder Quarkus hat man mit Dependency Injection bereits mächtigere Werkzeuge für dieses Problem.

Mit dem JPMS erfährt der ServiceLoader aber ein Facelifting und damit eine Wiedergeburt: Im Moduldeskriptor können angebotene und genutzte Services Refactoring-stabil angegeben werden. Damit können Module Implementierungen einer Schnittstelle (oder abstrakten Klasse) anfordern, und beliebige andere Module können dazu Implementierungen bereitstellen. Dieses Konstrukt ermöglicht es, mit purem Java, unabhängig von irgendwelchen Frameworks (und Reflection Magic), dynamische Services umzusetzen. Egal ob mit ServiceLoader oder Frameworks wie Spring haben sich für mich zwei Muster herauskristallisiert:

  • Singleton: Ich möchte zu einem Interface genau eine Instanz haben. Woher die Implementierung kommt und wie diese zusammengebaut wird, ist für die Nutzung abstrahiert und wird vom Framework oder eben von Java geregelt. Ein Beispiel wäre ein Interface ConfigurationService oder der berüchtigte EntityManager.
  • Plug-ins: Ich möchte etwas flexibel erweiterbar gestalten und biete selbst ein Interface an, zu dem es auch zur Laufzeit viele unterschiedliche Implementierungen parallel geben darf, die wir Plug-ins nennen. Sie erweitern den Funktionsumfang und dazu können unterschiedliche Parteien in Harmonie beitragen. Als Beispiel stellen wir uns vor, dass wir eine Suchmaschine programmieren, und bieten als Plug-in-Interface TextExtractor an, das zu einem bestimmten Dateiformat den reinen Text extrahieren kann. Ein Modul stellt eine Implementierung PdfTextExtractor bereit, ein anderes XpsTextExtractor usw. Keins dieser Module exportiert irgendetwas, und die nutzende Anwendung importiert diese Module, ohne direkten Zugriff auf deren Klassen zu erhalten.

 

In beiden Fällen füge ich im Modul, das auf den Service zugreifen möchte, ein uses-Statement in den Modul-Deskriptor ein und gebe dabei das Interface des Service an:

module org.search.engine {
  uses org.search.engine.extractor.TextExtractor;
  exports org.search.engine;
  exports org.search.engine.extractor;
  // ...
}

Um im Code an Implementierungen des Service TextExtractor zu gelangen, erzeuge ich einen passenden ServiceLoader und iteriere über die verfügbaren Implementierungen:

ServiceLoader<TextExtractor> serviceLoader = ServiceLoader.load(TextExtractor.class);
for (TextExtractor extractor : serviceLoader) {
  register(extractor);
}

Dabei ist es wichtig, dass der Aufruf von ServiceLoader.load im Code des Moduls stattfindet, in dem das uses-Statement definiert ist. Wer eine generische Hilfsklasse in ein anderes Modul auslagert, die den ServiceLoader anhand eines übergebenen Class-Objekts lädt, wird das schmerzlich bemerken. Die Hilfsklasse habe ich in meinem Projekt trotzdem gebaut, muss aber den ServiceLoader außerhalb im verantwortlichen Modul erzeugen und ihn dann an die Hilfsklasse übergeben. Nutzen entsteht aber erst dann, wenn für den Service auch mindestens eine Implementierung bereitgestellt wird.

In einem anderen Modul implementieren wir dazu besagten PdfTextExtractor und stellen diese Implementierung über ein provides-Statement im Moduldeskriptor zur Verfügung:

module com.company.search.extractor.pdf {
  provides org.search.engine.extractor.TextExtractor
  with com.company.search.extractor.pdf.PdfTextExtractor;
}

Eine Anwendung, die das Ganze im Zusammenspiel verwenden möchte, importiert die Suchmaschine samt der gewünschten Plug-in-Module:

module com.awesome_restaurant.online.app {
  requires org.search.engine;
  requires com.company.search.extractor.pdf;
  requires com.enterprise.search.extractor.xps;
  // ...
}

Brauche ich den XPS-Support nicht mehr, entferne ich einfach das entsprechende requires-Statement. Will ich ein anderes Format unterstützen, lade ich das passende Plug-in über eine weitere requires-Anweisung (wobei das requires-Statement optional ist, solange sich das Modul mit dem Plug-in im Modulpfad befindet).

Hier noch einige Tipps aus dem praktischen Umgang damit:

  • Findet der ServiceLoader zur Laufzeit für den angeforderten Service gar keine oder „zu viele“ Implementierung(en), so muss ich mich selbst um die Fehlerbehandlung kümmern.
  • Für einen Singleton Service möchte ich eine Exception, wenn keine Implementierung gefunden wurde. Gegebenenfalls möchte ich aber auch eine Default- bzw. Fallback-Implementierung mitliefern, die dann verwendet wird.
  • Finde ich für einen Singleton Service mehrere Implementierungen, will ich typischerweise auch eine Exception. Doch der ServiceLoader und das Java-Modul bieten nicht viel Flexibilität (wie z. B. DI-Frameworks aus Spring oder Quarkus): Eine Möglichkeit, einen Service durch ein Modul wieder zu entfernen oder zu ersetzen, gibt es nicht. Mit extrem feingranularen Modulschnitten kann man das alles irgendwie lösen, aber das überfordert schnell die Nutzer meiner Module. Daher habe ich mir angewöhnt, Implementierungen aus meinem eigenen Package Namespace bei Singleton Services zu ignorieren, falls es auch eine Implementierung aus einem externen Package gibt. Damit erlaube ich externen Nutzern meiner Bibliotheken, meine Implementierung durch ihre eigene zu ersetzen, und erhöhe die Flexibilität.

Internationalisierung

Java bietet mit ResourceBundle und MessageFormat die Grundlagen für die Internationalisierung einer Anwendung, also für die Unterstützung mehrerer Sprachen z. B. bei Texten für den Endnutzer. Das Laden eines solchen ResourceBundle aus properties-Dateien (z. B. UiMessages_de.properties) lädt jedoch eine Ressource per ClassLoader, was wie ein Reflection-Zugriff gewertet wird. Daher gibt es Probleme, wenn man diese properties-Dateien in einem Modul verpackt und dann generischen Code aus einem anderen Modul diese als ResourceBundle laden will. Solche Aspekte sind in Java nicht klar dokumentiert und es hat mich initial etwas Zeit gekostet, das vollständig zu verstehen. Um nicht das komplette Modul mit dem Schlüsselwort open für Reflection freizugeben, ist die pragmatischste Lösung, Bundles zur Internationalisierung in separaten JAR-Dateien ohne Moduldeskriptor auszuliefern. Sind sie im Class-/Modulpfad, so landen sie im Unnamed Module und können ohne Probleme geladen werden.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

IDE-Unterstützung

Als ich die ersten Versuche mit Java-Modulen gemacht habe, war die Unterstützung in gängigen Entwicklungsumgebungen katastrophal bis nicht vorhanden. Inzwischen hat sich das deutlich stabilisiert und auch Code Completion und Refactoring werden unterstützt. Ich kann sowohl in IntelliJ als auch in Eclipse komplexe Projekte mit vielen Java-Modulen zuverlässig entwickeln. In Eclipse gibt es noch diverse Bugs bei module-info.java-Dateien mit dem Code-Formatter und insbesondere mit Import-Statements. Ich verzichte daher einfach auf Imports in diesen Dateien und verwende voll qualifizierte Typen. Auch bei IntelliJ gibt es Kinderkrankheiten, wie z. B. IllegalAccessError, weil irgendein Teil von JUnit im Unnamed Module gelandet ist.

Tests

Im einfachen Fall schreibt man seine JUnit-Tests ganz normal wie in Projekten ohne Java-Module. Will man aber im Test ein zusätzliches Testmodul haben, einen Service zum Testen hinzufügen oder Ähnliches, gibt es Probleme. Eigentlich würde man gern unter src/test/java eine zusätzliche module-info.java hinterlegen können, die dann für den Test verwendet wird. Genau das wird auch in der Maven-Dokumentation als Lösung propagiert – wird aber von IDEs gar nicht unterstützt [5]. Als Workaround erstelle ich dann zusätzliche Module, die nur zum Testen dienen, und schließe diese vom Deployment aus, sodass sie nicht im Release veröffentlicht werden.

Entscheidung für oder gegen JPMS

Im nächsten Spring-Boot- oder Quarkus-Projekt würde ich persönlich JPMS trotzdem noch nicht nutzen. Diese Frameworks basieren maßgeblich auf Reflection, und mit Java-Modulen schaffe ich mir hier viele Probleme, ohne großen Nutzen zu stiften. Inzwischen ist es immerhin technisch möglich, was schon mal als großer Fortschritt zu werten ist. Wenn ich ein Backend mit JPMS ausprobieren will, kann ich das mit Helidon tun, das besser für JPMS geeignet ist, jedoch im Vergleich zu Spring Boot und Quarkus eher ein Schattendasein fristet. Micronaut kann mit dem JPMS aktuell gar nicht verwendet werden [6].

Schreibe ich hingegen eine Java-App ohne große Frameworks, sollte ich das JPMS ausprobieren. Entwickle ich eine Bibliothek, die möglichst viele Nutzer erreichen soll, ist die Unterstützung des JPMS Pflicht. Andernfalls schließe ich diverse potenzielle Nutzer von vornherein aus.

Fazit und Ausblick

Im Jahr 2018 konnte man das JPMS zwar schon vollständig nutzen, kämpfte aber gegen Windmühlen. In der heutigen Zeit hat sich das JPMS so weit etabliert, dass die Unterstützung sämtlicher Tools ausgereift und die der wichtigen Java-Bibliotheken gegeben ist. Es ist also an der Zeit, sich intensiver mit dem Thema auseinanderzusetzen und eigene Experimente und Erfahrungen zu machen. Für Entwickler von Bibliotheken ist es heutzutage Pflicht, Moduldeskriptoren (mindestens als Automatic-Module-Name) mitzuliefern.

Wenn sich Java weiter weg von Deep Reflection zur Laufzeit hin zu AoT und CWA entwickelt und auch die Frameworks sich entsprechend weiterentwickeln, könnte die Verwendung des JPMS in der Zukunft zumindest für Green-Field-Projekte zum Standard werden.


Links & Literatur

[1] https://openjdk.org/jeps/411

[2] https://m-m-m.github.io

[3] https://github.com/helidon-io/helidon

[4] Langer, Angelika; Kreft, Klaus: „Eine Anwendung in vielen Teilen. Project Jigsaw aka Java Module System“; in: Java Magazin 10.2017, https://entwickler.de/reader/reading/java-magazin/10.2017/1976daae3f43cab4336cd7ad

[5] https://github.com/eclipse-jdt/eclipse.jdt.core/issues/1465

[6] https://github.com/micronaut-projects/micronaut-core/issues/6395

The post Java Module und JPMS: Endlich bereit für den Einsatz? appeared first on JAX.

]]>
Ein Liebesbrief an die Java-Community https://jax.de/blog/ein-liebesbrief-an-die-java-community/ Fri, 01 Sep 2023 06:14:04 +0000 https://jax.de/?p=88940 Ich möchte diesen Artikel nutzen, um vielen Menschen und Unternehmen zu danken. Ein solches Projekt mit meinem Sohn ist nur möglich, weil sie fantastische Bibliotheken und Werkzeuge geschaffen haben, mit denen wir Anwendungen schnell entwickeln können.

The post Ein Liebesbrief an die Java-Community appeared first on JAX.

]]>
In meinem vorherigen Artikel [1] habe ich gezeigt, wie ich Java Enums verwende, um die Kategorien für Videos auf der Website https://4drums.media [2] zu definieren. Diese Website ist ein Lieblingsprojekt, das ich zusammen mit meinem dreizehnjährigen Sohn entwickelt habe.

Ich werde hier nicht das gesamte Projekt mit Ihnen teilen und Ihnen den kompletten Code zeigen. Nein, ich möchte die Gelegenheit vielmehr nutzen, um allen zu danken, die es uns ermöglicht haben, diese Website zu erstellen!

Abb. 1: Die verfügbaren Video- und Tutorialkategorien auf der Website 4drums.media

Stay tuned

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

 

Über 4drums.media

Wie viele Jugendliche heutzutage möchte auch mein Sohn „im Internet berühmt“ werden – er spielt Schlagzeug, seitdem er ein kleiner Junge ist. Um seine Ambitionen zu unterstützen, haben wir gemeinsam eine Website erstellt, auf der er Schlagzeugvideos veröffentlichen und eine Community aufbauen kann, die sich für das Schlagzeugspielen interessiert. Da er kein Budget hat, ist das Hosten und Bearbeiten von Videos keine Option. Glücklicherweise bieten YouTube und Vimeo APIs an, um mit ihren Diensten zu interagieren. In Kombination mit meinen Erfahrungen mit Spring Boot, Vaadin usw. konnten wir schnell loslegen und hatten bald die erste Version, die jetzt verfügbar ist. Werfen wir einen Blick auf die von uns verwendeten Tools, die Sie für Ihr nächstes persönliches oder berufliches Projekt inspirieren könnten.

IntelliJ IDEA

Visual Studio Code, Eclipse, NetBeans – alles großartige und kostenlose IDEs zur Erstellung von Java-Code. Aber die am häufigsten verwendete IDE ist seit vielen Jahren IntelliJ IDEA [3]. Ich benutze die (kostenpflichtige) Ultimate Edition, aber die kostenlose Community Edition ist genauso gut. Und ja, mein Sohn benutzt diese Version auf seinem Schul-PC! Am Code selbst hat er nicht mitgewirkt, aber ich konnte ihn einfach die neueste Version ziehen lassen, ein paar Farben und Symbole hinzufügen, lokal testen und sie dann wieder in das Repository einspeisen. Und dank des automatisierten Builds (siehe GitHub weiter unten) kann er die Änderungen innerhalb weniger Minuten live sehen. Ist es nicht großartig, dass jeder, vom Anfänger bis zum erfahrenen Benutzer, ein solch fantastisches Tool frei nutzen kann? Vielen Dank, JetBrains!

Spring Boot und Bibliotheken

Ich liebe Quarkus, Micronaut und andere, weil sie viel Evolution in die Java-Welt gebracht haben. Da ich jedoch bereits viel Erfahrung mit Spring Boot [4] hatte, habe ich diesen Weg weiterverfolgt. Aber es ist nicht nur Spring selbst, das dieses System zu einem fantastischen Framework macht. Es ist die Kombination mit Flyway zur Erweiterung der Datenbankstruktur, H2 für lokale Tests, Jsoup zur Validierung von Texteingaben, JUnit für Tests usw. Ich bin also nicht nur Spring dankbar, sondern auch den vielen Unternehmen und Entwicklern, die großartige Java-Bibliotheken und -Tools entwickeln!

YouTube und Vimeo

Einen Videodienst selbst zu hosten, würde den Rahmen eines Vater-Sohn-Projekts bei Weitem sprengen. Aber zum Glück müssen wir das nicht tun, denn viele Videodienste erlauben eine vollständige Integration. So können sie auf einfache Weise mehr Zuschauer anziehen, da ihre Videos in andere Websites integriert werden. Der Player unterliegt jedoch vollständig der Kontrolle des Anbieters, sodass jede Ansicht eines Videos (und der Werbung …) eine zusätzliche Ansicht für seine jeweilige Plattform darstellt.

Mit dem WebClient von Spring ist es mühelos möglich, die Beschreibung, den Kanal, das Vorschaubild und weitere Informationen z. B. von jedem YouTube-Video abzurufen. In Kombination mit Datensätzen, die der YouTube-API-Antwort-JSON [5] entsprechen, und der FasterXML-Bibliothek von Jackson ist nur minimaler Code erforderlich, um Daten anzufordern und das Ergebnis zu parsen (Listing 1).

public Optional<YouTubeApiResponse> getYouTubeVideoApi(String id) {
  String url = "https://www.googleapis.com/youtube/v3/videos"
  + "?id=" + id + 
  + "&part=snippet%2CcontentDetails%2Cstatistics"
  + "&key=" + apiKeyYouTube;
  YouTubeApiResponse video = webClient.get()
    .uri(URI.create(url))
    .retrieve()
    .bodyToMono(YouTubeApiResponse.class)
    .block();
  if (video == null) {
    logger.error("Could not get info from YouTube for {}", id);
    return Optional.empty();
  }
  logger.info("Video info from YouTube: {}", video.title());
  return Optional.of(video);
}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record YouTubeApiResponse(
  String kind,
  String etag,
  List<Item> items) {}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record Item(
  String kind,
  String etag,
  String id,
  Snippet snippet,
  ContentDetails contentDetails,
  Statistics statistics
) {
}

Vimeo bietet die gleiche Art von API [6], daher haben wir beide Dienste integriert. Mit dem Aufstieg des Fediverse werden Alternativen wie PeerTube [7] an Bedeutung gewinnen, und wir werden definitiv versuchen, die unterstützten Videoplattformen später zu erweitern.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Vaadin

Ich habe JavaFX schon oft für grafische Benutzeroberflächen auf dem Desktop verwendet. Mit Vaadin habe ich ein ähnlich leistungsfähiges und Java-basiertes Framework für die Websiteentwicklung gefunden. Mit reinem Java-Code können Sie sowohl das Backend als auch das Frontend innerhalb eines Maven-Projekts erstellen, das einfach zu paketieren und zu verteilen ist. Auf start.vaadin.com [8] können Sie die verschiedenen Seiten konfigurieren, die Sie benötigen (Abb. 2), das Theme gestalten (Abb. 3) und die Java- und Vaadin-Version auswählen, die Sie verwenden möchten. Wenn Sie fertig sind, können Sie ein vollständig vorbereitetes Maven-Projekt auf Basis von Spring Boot mit einem Mausklick herunterladen, entpacken und in der IDE öffnen und ausführen, um es weiter zu bearbeiten.

Abb. 2: Sie können die verschiedenen Seiten konfigurieren …

Abb. 3: … und das Theme gestalten

Jede Komponente ist auf der Vaadin-Website [9] sehr gut dokumentiert und mit viel Beispielcode versehen. Das Hinzufügen einer solchen Komponente zu den HTML-Seiten und das Verknüpfen mit einer Backend-Methode ist sehr einfach, wie Sie im Beispielcode für den Like-Button in Listing 2 sehen.

var like = new Button();
like.setIcon(VaadinIcon.THUMBS_UP_O.create());
like.setText(String.valueOf(numberOfLikes));
like.addClickListener(e -> {
  videoService.addLike(video, user);
});
add(like);

Abb. 4: Der Output von Listing 2

Vaadin bietet eine lizenzierte Version mit zusätzlichen erweiterten Komponenten (Charts, GridPro …) und Support. Dennoch bietet die freie und quelloffene Version selbst für viele Geschäftsfälle alle Tools, um eine vollständige Webanwendung zu erstellen. Ich habe sie in meinen früheren Jobs für Back-Office-Anwendungen verwendet, und die Entwicklungsgeschwindigkeit, die sich aus einer einzigen Codebasis für APIs und UI ergibt, ist enorm!

 

PostgreSQL

Gibt es heute noch Websites, die keine Datenbank verwenden? Mein Weg zur Datenbank begann vor vielen Jahren mit Microsoft Access. Obwohl es nicht für die Verwendung im Web gedacht war, konnte ich viele dynamische Websites mit einer Access-Datenbank als Kern erstellen. Später habe ich SQL Server, MySQL, NoSQL-Datenbanken und andere verwendet. Aber ich denke, wir können festhalten, dass PostgreSQL [10] den größten Teil der Welt der Onlinedatenbanken erobert hat. Ich liebe Tools, die einfach da sind, um die man sich nicht kümmern muss und die das tun, was sie tun sollen. Genau das hat PostgreSQL für mich seit vielen Jahren getan. Und auch hier ist die Kombination mit Spring Boot erstaunlich einfach und unkompliziert.

GitHub-Repository und Aktionen

Da ich nun schon seit einiger Zeit in dieser Branche tätig bin, habe ich viele Entwicklungen in der Art und Weise gesehen, wie Quelldateien verwaltet und gemeinsam genutzt werden. Von einem normalen Dateiserver mit ständigen Konflikten, über CVS, SVN bis hin zu Git. Im Foojay-Podcast „The Future of Source Control and CI/CD“ [11] sind wir zu dem Schluss gekommen, dass das aktuelle Git wahrscheinlich kein Endpunkt ist und wir weitere Entwicklungen sehen werden. Ist es nicht seltsam, dass nur die Softwareindustrie einen solchen Workflow angenommen hat? Warum verwendet z. B. der Gesetzgeber nicht denselben Ansatz, während die Erweiterung und Verbesserung von Gesetzen eigentlich sehr ähnlich ist und auf einem System von „Patches“ basiert? Könnte es sein, dass Trunks, Merge-Konflikte und zu viele mögliche Befehle Git zu kompliziert machen? Und dass nur Softwareentwickler in der Lage zu sein scheinen, mit diesen Herausforderungen fertigzuwerden?

Die Beantwortung dieser Fragen würde den Rahmen dieses Artikels sprengen, aber ich bin auf jeden Fall sehr dankbar für das, was GitLab, GitHub und Co. als kostenlosen Service für Open-Source- und Pet-Projekte anbieten. Ein zuverlässiges Versionskontrollsystem zu haben, ohne sich um Serverkosten, Back-ups, Upgrades usw. kümmern zu müssen, ist die erste Hürde, die diese Anbieter nehmen. Aber für mich ist der wichtigste Grund, sie zu lieben, die CI/CD, die sie bieten. Dank GitHub Actions führt jeder Commit in das Repository dazu, dass eine neue Websiteversion innerhalb von Minuten bereitgestellt wird, und zwar vollständig automatisiert, dank einer einzigen .yml-Datei. Ja, wir sind uns alle einig, dass YAML ätzend ist und das Erreichen einer funktionierenden Aktionsdatei nervt, aber wenn man eine funktionierende Lösung hat und sich nicht mehr um den Build- und Deployment-Prozess kümmern muss, ist das ein echter Meilenstein in jedem Projekt!

Stay tuned

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

 

Uberspace

Bis jetzt habe ich die meisten der Tools aufgelistet, die ich für die 4drums.media-Website verwendet habe und die alle kostenlos erhältlich sind! Jetzt bleibt nur noch ein letzter Schritt für jede Website: ein Onlinezuhause finden. Die Möglichkeiten sind endlos, von Shared Hosting über VPS bis hin zu kompletten Netzwerkumgebungen bei AWS, Azure oder jedem anderen Anbieter. Von nicht so günstig bis sehr teuer …

Da unser Budget für dieses Projekt sehr begrenzt ist (eigentlich nichtexistent), habe ich in Deutschland eine schöne Lösung bei uberspace.de [12] gefunden. Mit einem Pay-as-you-want-Modell ab 5 Euro/Monat bekommt man einen VPS, den man nach Bedarf konfigurieren kann. In unserem Fall bedeutete das, die richtige Java-Version und PostgreSQL zu installieren. Mit einer sehr detaillierten Dokumentation [13] war jeder Schritt in diesem Prozess eine schnelle und angenehme Erfahrung. Und wenn man nicht weiterkommt, antwortet der Support schnell und hilft einem oder verweist auf die richtige Dokumentation.

Das Hosting der Website (und des Domänennamens) ist also der einzige Teil dieses Projekts, bei dem „Herr Visa“ uns helfen musste, aber der Betrag ist trotzdem minimal.

Fazit

Java ist nicht nur eine Sprache und eine Laufzeitumgebung. Es ist auch eine große Gemeinschaft von Menschen und Unternehmen, die erstaunliche Dinge entwickelt, die in den meisten Fällen kostenlos genutzt werden können. Teil dieser Gemeinschaft zu sein, ist eine tägliche Freude, und ich bin wirklich dankbar, dass all das verfügbar ist! Deshalb sage ich allen, die einen Beitrag leisten und all dies möglich machen: Danke! Aus tiefstem Herzen!


Links & Literatur

[1] Delporte, Frank: „Verborgene Schönheiten“; in Java Magazin 9.2023

[2] https://4drums.media

[3] https://www.jetbrains.com/idea/

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

[5] https://developers.google.com/youtube/v3/

[6] https://developer.vimeo.com

[7] https://joinpeertube.org

[8] https://start.vaadin.com

[9] https://vaadin.com/docs/latest/components

[10] https://www.postgresql.org

[11] https://foojay.io/today/foojay-podcast-26/

[12] https://uberspace.de

[13] https://manual.uberspace.de

[14] https://webtechie.be/books

The post Ein Liebesbrief an die Java-Community 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.

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

]]>