JAX https://jax.de/ Java, Architecture & Software Innovation Wed, 10 Apr 2024 08:40:31 +0000 de-DE hourly 1 https://wordpress.org/?v=6.4.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.

]]>
Resiliente Kafka Consumer bauen https://jax.de/blog/kafka-fehlerbehandlung-guide/ Mon, 04 Mar 2024 08:55:02 +0000 https://jax.de/?p=89539 Asynchrone Kommunikation ist der Königsweg für den Datenaustausch zwischen lose gekoppelten Systemen. Laut einer Erhebung von 6sense [1] ist der Marktführer für den Austausch asynchroner Nachrichten Apache Kafka. Wir beschreiben in diesem Artikel unseren Weg als agiles Team zu einer einfachen Catch-all-Fehlerbehandlung für Nachrichten, die wir via Kafka empfangen. Und so viel sei vorab verraten: Am Ende unserer Reise finden wir sogar die richtige Strategie zur Selbstheilung.

The post Resiliente Kafka Consumer bauen appeared first on JAX.

]]>
Wir berichten hier über die Erfahrungen, die wir in einem gemeinsamen Projekt im öffentlichen Sektor gesammelt haben. Dort haben wir verschiedene Strategien für die Fehlerbehandlung bei der Verarbeitung asynchroner Nachrichten in unseren Consumern erarbeitet und verglichen. Neben der Minimierung unserer Betriebsaufwände mussten wir dabei wegen notorisch enger Zeitpläne auch unseren Entwicklungsaufwand minimieren. Deswegen haben wir zunächst nach einer Strategie gesucht, die in einem ersten Schritt als einzige „Catch all“-Strategie für alle Fehlerszenarien anwendbar ist: eine „Minimum Viable“-Fehlerstrategie.

Bei der Bewertung der Strategien haben wir die möglichen Fehlerszenarien nach zwei Dimensionen unterschieden, der Fehlerquelle und der Fehlerhäufigkeit.

Fehlerquelle:

  • Der Producer erzeugt Nachrichten, die nicht dem Schnittstellenvertrag entsprechen, entweder syntaktisch oder semantisch.
  • Der Consumer ist nicht in der Lage, alle Nachrichten, die dem Schnittstellenvertrag entsprechen, zu verarbeiten, z. B., wirft er unerwartete Ausnahmen.
  • Die Infrastruktur arbeitet nicht erwartungsgemäß, z. B. fällt die Verbindung des Consumers zu seiner Datenbank aus.

Fehlerhäufigkeit:

  • Der Fehler tritt nur selten und vereinzelt auf, z. B. als sporadischer Edge Case.
  • Der Fehler ist ein generelles und länger andauerndes Problem, das viele Nachrichten betrifft.

Bei der schlussendlichen Bewertung der Strategien haben sich die Vor- und Nachteile dann oft als unabhängig von Fehlerquelle oder Fehlerhäufigkeit herausgestellt.

Stay tuned

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

 

Kafka-Grundlagen

Um die verschiedenen Strategien der Fehlerbehandlung zu verstehen, ist es notwendig, die grundlegenden Konzepte von Kafka zu kennen. Diese erläutern wir hier kurz. Für Details verweisen wir z. B. auf den Artikel „Kafka 101: Massive Datenströme mit Apache Kafka“ [2].

Broker

Kafka bezeichnet sich als Event-Streaming-Plattform, die als Ersatz für traditionelle Message-Broker benutzt werden kann. Der Message-Broker dient dazu, asynchrone Nachrichten in Topics zu veröffentlichen. Nach einer konfigurierbaren Zeit, der Retention Time, löscht der Broker die Nachrichten.

Producer

Der Producer veröffentlicht Nachrichten mit einem Schlüssel in einem Kafka Topic. Der Broker teilt ein Topic in mehrere Partitionen auf und speichert eine Nachricht in genau einer Partition des Topics. Dabei berücksichtigt der Broker den Schlüssel der Nachricht und schreibt alle Nachrichten mit demselben Schlüssel auch in dieselbe Partition. Je Partition garantiert der Broker, dass die Reihenfolge der Nachrichten erhalten bleibt. Der Producer muss die Schlüssel der Nachrichten also so wählen, dass alle Nachrichten, deren Reihenfolge untereinander erhalten werden muss, denselben Schlüssel haben.

Consumer

Die an den Nachrichten interessierten Consumer registrieren sich für das Topic und können so die Nachrichten vom Broker lesen und verarbeiten. Dabei geben die Consumer die Consumer Group an, zu der sie gehören. Der Broker stellt dann sicher, dass jede Partition genau einem Consumer einer Consumer Group zugewiesen ist.

Problembeschreibung

In unserem Projekt nutzen wir Kafka (Kasten: „Kafka-Grundlagen“), um Änderungen an den Daten, die ein System verantwortet, als Events dieses Systems zu veröffentlichen. Im Domain-Driven Design spricht man hier von den Zustandsänderungen eines Aggregats [3]. Wir erzeugen und verarbeiten die Events im Publish-Subscribe-Modus. Da die Retention Time mit fünf Tagen als endlich vorgegeben ist, enthalten die Events nicht nur die geänderten Daten, sondern immer den kompletten neuen Zustand des Aggregats.

Bezüglich der Performance haben wir recht entspannte Anforderungen: Aus fachlicher Sicht ist es ausreichend, wenn die Events vor Ablauf der Retention Time verarbeitet werden. Dabei ist es jedoch wichtig, dass die Reihenfolge der Events mit demselben Schlüssel erhalten bleibt und kein Event übersprungen oder anderweitig nicht vollständig verarbeitet wird. Events mit demselben Schlüssel nennen wir abhängig.

Nach Mathias Verraes [4] gibt es zwei schwierige Probleme in verteilten Systemen. Die Einhaltung der Reihenfolge ist das erste. Dieses Problem haben wir gelöst, indem die Nachrichten, deren Reihenfolge untereinander erhalten werden muss, denselben Schlüssel bekommen. Das zweite schwere Problem ist die Exactly-once Delivery. Wir können es umgehen, indem wir unsere Consumer idempotent implementieren.

Codebeispiele

Im Folgenden illustrieren wir jede betrachtete Strategie mit einem Codebeispiel. Diese nutzen die MicroProfiles Reactive Messaging und Fault Tolerance. Als Implementierung für beide MicroProfiles benutzen wir die für Quarkus üblichen Bibliotheken von SmallRye. Die Codebeispiele sind auch auf GitHub [5] zu finden.

Bei der Nutzung der Codebeispiele sind zwei Aspekte zu berücksichtigen: die Commit Strategy [6] und die Failure Strategy [7]. Als Commit Strategy haben wir latest gewählt, da beim Default throttled der Timeout throttled.unprocessed-record-max-age.ms [8] während länger dauernder Retries einer fehlerhaften Nachricht erreicht werden kann. Als Failure Strategy haben wir eine eigene Strategie implementiert, die davon ausgeht, dass der Code, der die Nachricht empfängt, die Fehler direkt behandelt und daher keine separate Failure Strategy mehr notwendig ist (Listing 1).

public class CustomKafkaFailureHandler implements KafkaFailureHandler {

  @ApplicationScoped
  @Identifier("custom")
  public static class Factory implements KafkaFailureHandler.Factory {

    @Override
    public KafkaFailureHandler create(
      KafkaConnectorIncomingConfiguration config,
      Vertx vertx,
      KafkaConsumer<?, ?> consumer,
      BiConsumer<Throwable, Boolean> reportFailure) {
        return new CustomKafkaFailureHandler();
    }
  }

  @Override
  public <K, V> Uni<Void> handle(IncomingKafkaRecord<K, V> record,
    Throwable reason, Metadata metadata) {
      return Uni.createFrom()
                .<Void>failure(reason)
                .emitOn(record::runOnMessageContext);
  }

Consumer herunterfahren

Der erste Halt auf unserer Reise führt uns zu einer äußerst einfach umzusetzenden Strategie: Consumer herunterfahren. Hierbei stoppt der Consumer die Verarbeitung und fährt den Prozess, in dem er läuft, herunter. Das macht er in dem Codebeispiel (Listing 2), indem er System.exit() aufruft. Alternativ könnte er z. B. in einer Kubernetes-Umgebung die Liveness-Probe [9] fehlschlagen lassen und so den Containermanager dazu veranlassen, den Prozess herunterzufahren. Diese Strategie, eingeschränkt auf den Kafka-Client des betroffenen Topics, bietet auch SmallRye Reactive Messaging an und nennt sie fail [10], [11].

@Incoming("aggregate-in")
public CompletionStage<Void> consume(Message<String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    controller.process(record.getPayload());
    return record.ack();
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
    System.exit(1);
    return null; // never reached
  }
}

Um diese Strategie universell einsetzen zu können, ist es hilfreich, einen Containermanager zu haben, der den heruntergefahrenen Prozess neu startet. So bekommen wir den Retry in Form eines Restart Loops [7] geschenkt.

Der Weg zur Korrektur ist je nach Fehlerursache unterschiedlich: Bei einem Fehler im Producer sendet dieser die korrigierte Nachricht sowie alle abhängigen Nachrichten erneut. Der Consumer muss dann die Offsets der fehlerhaften Nachricht und aller ursprünglichen abhängigen Nachrichten überspringen.

Bei einem Fehler im Consumer ist lediglich der Bugfix zu deployen, dann läuft der Consumer einfach weiter. Bei einem Fehler in der Infrastruktur ist nichts zu tun, der Consumer läuft einfach weiter, sobald der Fehler behoben wurde.

Vorteile:

  • Die Strategie erhält die Reihenfolge.
  • Der manuelle Aufwand für die Fehlerbehebung ist beim Team des Consumers unabhängig von der Anzahl fehlerhafter Nachrichten.

Nachteile:

  • Der Microservice blockiert auch fehlerfreie Nachrichten.
  • Die Strategie führt in überwachten Umgebungen wie Kubernetes zu einer Restart-Schleife. Die wiederum führt je nach Timeout, Restart Backoff, Restart Cutoff und Start-up-Zeit dazu, dass der Broker die Partitionen auf die restlichen Consumer umverteilt, sodass am Ende jeder Consumer versuchen wird, die fehlerhafte Nachricht zu verarbeiten, und dann herunterfährt. Das wiederum verursacht vermutlich einen erhöhten Ressourcenverbrauch. Durch das Herunterfahren weiterer Consumer werden auch immer mehr Nachrichten in anderen Partitionen blockiert.
  • Die Strategie erfordert in nicht überwachten Umgebungen einen Mechanismus, um den Consumer nach der Fehlerbehebung wieder zu starten.
  • Die Strategie erfordert einen zusätzlichen Mechanismus zum Überspringen von fehlerhaften und ggf. auch abhängigen Nachrichten.
  • Bei Fehlern in der Nachricht müssen alle abhängigen Nachrichten vom Producer nochmals geschickt werden.

Dead Letter Queue

Die nächste Station unserer Reise ist ein Klassiker der Fehlerbehandlung von asynchronen Nachrichten: die Dead Letter Queue (DLQ). Im Zusammenhang mit Kafka wird diese Strategie oft auch als Dead Letter Topic bezeichnet, bei der Umsetzung mittels Datenbank auch als Dead Letter Table. Tritt ein Fehler beim Verarbeiten einer Nachricht im Consumer auf, sortiert der Consumer die betroffene Nachricht in die namensgebende Warteschlage für „tote“ Nachrichten aus. Anschließend macht er mit der Verarbeitung der nächsten Nachricht weiter (Listing 3). Als Alternative zu einer eigens implementierten Fehlerbehandlung bietet SmallRye die Dead Letter Queue auch als Failure Strategy an [10], [12].

@Incoming("aggregate-in")
public CompletionStage<Void> consume(Message<String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    controller.process(record.getPayload());
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
    deadLetterEmitter.send(record);
  }
  return record.ack();
}

Der Weg zur Korrektur ist wieder von der Fehlerursache abhängig. Bei einer semantisch oder syntaktisch fehlerhaften Nachricht schickt der Producer eine korrigierte Nachricht. Die fehlerhafte Nachricht in der DLQ muss daraufhin übersprungen werden. Bei einem Fehler des Consumers muss der Bugfix deployt werden, anschließend müssen die Nachrichten der DLQ wieder eingelesen werden. Auch nach Behebung eines Infrastrukturproblems muss die DLQ wieder eingelesen werden.

Vorteile:

  • Blockiert nur Nachrichten, bei denen ein Fehler auftritt, d. h. keine Verzögerung nicht betroffener Nachrichten und maximaler Durchsatz.
  • Gute Skalierung auch bei vielen fehlerhaften Nachrichten.
  • Falls der Fehler nicht in der Nachricht lag, kann die DLQ nach dem Bugfix erneut eingelesen werden.
  • Der manuelle Aufwand für die Fehlerbehebung ist beim Team des Consumers unabhängig von der Anzahl fehlerhafter Nachrichten.
  • Bei einer Implementierung der DLQ mittels Datenbank kann man die Retention Time von Kafka umgehen, wodurch man mehr Zeit für die Fehlerbehebung hat.

Nachteile:

  • Erfordert einen separaten (u. U. manuellen) Mechanismus zum Einlesen der DLQ nach dem Bugfix.
  • Wirft die Reihenfolge der Nachrichten durcheinander.
  • DLQ muss eine Kapazität wie das ursprüngliche Topic haben, falls der Fehler einen Großteil der Nachrichten betrifft.
  • Die Strategie erfordert einen zusätzlichen Mechanismus zum Überspringen von fehlerhaften und ggf. auch abhängigen Nachrichten.
  • Bei Fehlern in der Nachricht müssen alle abhängigen Nachrichten vom Producer nochmals geschickt werden.

DLQ Advanced

Mit Blick auf unsere Problemstellung hat die einfache Dead Letter Queue ein entscheidendes Problem: Sie kann die Reihenfolge von Nachrichten nicht erhalten. Unsere Reise führt deswegen tiefer in die Welt der DLQs. Die Idee: Eine erweiterte Dead Letter Queue nimmt nicht nur Nachrichten auf, die bei der Verarbeitung Probleme machen, sondern auch alle von ihr abhängigen Nachrichten. Dazu muss lediglich der Schlüssel der fehlerhaften Nachricht gespeichert werden. Anschließend können alle abhängigen Nachrichten durch den gleichen Schlüssel identifiziert und ebenfalls in die DLQ geleitet werden (Listing 4).

@Incoming("aggregate-in")
public CompletionStage<Void> consume(KafkaRecord<String, String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    if(controller.shouldSkip(record.getKey())) {
      LOGGER.warn("Record skipped: " + record);
      deadLetterEmitter.send(record);
    } else {
      controller.process(record.getPayload());
    }
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
    controller.addKeyToSkip(record.getKey());
    deadLetterEmitter.send(record);
  }
  return record.ack();
}

Der Weg zur Korrektur ist zunächst identisch mit dem der einfachen DLQ. Im Detail wird es jedoch kompliziert. Um die Reihenfolge beim Wiedereinlesen der DLQ weiterhin zu erhalten, muss ihr Einlesen mit dem Einlesen des regulären Topics koordiniert werden. Benutzt man ein Kafka Topic als DLQ, stellen sich weitere Probleme, wie die Verteilung von Schlüsseln in zwei Topics auf unterschiedliche Consumer und die Vermischung von korrigierten und nicht korrigierten Nachrichten in der DLQ. Die Realisierung als Dead Letter Table in der Datenbank löst manche dieser Probleme, verursacht aber auch neue. Wegen dieser steigenden Komplexität haben wir zu diesem Zeitpunkt ein Zwischenfazit gezogen, um nach weiteren Alternativen zu suchen.

Vorteile:

  • Die Strategie erhält die Reihenfolge.
  • Blockiert nur fehlerhafte und davon abhängige Nachrichten, d. h. keine Verzögerung nicht betroffener Nachrichten und maximaler Durchsatz.
  • Bei einer Implementierung der DLQ mittels Datenbank kann man die Retention Time von Kafka umgehen, womit man mehr Zeit für die Fehlerbehebung hat.

Nachteile:

  • Erfordert einen zusätzlichen Mechanismus zum Überspringen von fehlerhaften und ggf. auch abhängigen Nachrichten.
  • Erfordert komplexen (u. U. manuellen) Mechanismus zum Einlesen und Synchronisieren der DLQ nach dem Bugfix mit dem ursprünglichen Topic.
  • Die DLQ muss eine Kapazität wie das ursprüngliche Topic haben, falls der Fehler einen Großteil der Nachrichten betrifft.
  • Der manuelle Aufwand für die Fehlerbehebung ist beim Team des Consumers unabhängig von der Anzahl fehlerhafter Nachrichten.

Pausieren und Wiederholen

Die Dead Letter Queue ist eine naheliegende Lösung, wenn man von Fehlern ausgeht, die einzelne Nachrichten betreffen. Je mehr Nachrichten betroffen sind – egal, ob durch die Reihenfolge oder die gemeinsame Fehlerursache –, desto komplexer und aufwendiger wird die Lösung. Man kann sich dem Problem einer allgemeinen Fehlerbehandlung aber auch von einem anderen Szenario her nähern: dem Ausfall der Infrastruktur. Dabei spielt es keine Rolle, ob dieser Fehler den Ausfall einer notwendigen Datenbank, des Kafka-Clusters oder der Netzwerkverbindung betrifft. Entscheidend ist: Der Fehler betrifft vermutlich alle Nachrichten. Er lässt sich also nicht durch eine Quarantäne einzelner Nachrichten begrenzen. Die klassische Lösung für diesen Fall kennt jeder von automatischen Telefonansagen: „Bitte versuchen Sie es später noch einmal!“

Der erste Teil dieser Taktik besteht im Pausieren der Arbeit. Beim kompletten Herunterfahren des Consumers provozieren wir jedoch ein kostspieliges Rebalancing durch den Broker (siehe oben). Man kann aber auch das Konsumieren pausieren, solange man nicht den gesamten Consumer anhält. Das verhindert, dass der Broker die Pause als Ausfall des Consumers wertet und ein Rebalancing einleitet.

Der zweite Teil der Taktik ist das Wiederholen (engl. Retry) der Aktion, bei der ein Fehler aufgetreten ist. In unserem Fall bedeutet das auf oberster Abstraktionsebene immer das Konsumieren einer Nachricht. Wenn man außerdem die fehlerhafte Nachricht nicht committet, wird sie der Consumer nach der Pause einfach erneut einlesen und versuchen, sie zu verarbeiten.

Listing 5 zeigt, wie man beide Schritte mit der Retry-Annotation des MicroProfile Fault Tolerance umsetzt. Hiermit pausiert der Consumer für alle Partitionen, die ihm der Broker zugewiesen hat. Alternativ kann man dem Broker auch das Pausieren [13] einer einzelnen Partition direkt mitteilen und später die Verarbeitung mittels resume [14] wieder aufnehmen.

@Incoming("aggregate-in")
@Retry(delay = 2000L, jitter = 400L, maxRetries = -1, maxDuration = 0)
@ExponentialBackoff(maxDelay = 2, maxDelayUnit = ChronoUnit.HOURS)
public CompletionStage<Void> consume(Message<String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    controller.process(record.getPayload());
    return record.ack();
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
    return record.nack€;
  }
}

Der Weg zur Korrektur ist wie immer abhängig von der tatsächlichen Fehlerursache. Da Infrastrukturprobleme meist externe Probleme sind, muss das Entwicklerteam bei der Lösung der Fehlerursache oft gar nicht aktiv werden. Tritt ein Infrastrukturproblem auf, können alle Consumer in einer Consumer Group unabhängig voneinander die Arbeit einstellen und in den Wiederholungsmodus wechseln. Sobald das Problem gelöst ist, sorgt das automatische Wiederholen für den Wiederanlauf aller Consumer.

Falls der Fehler beim Producer liegt, muss dieser wie bei allen anderen Strategien auch eine korrigierte Nachricht sowie eventuell weitere abhängige Nachrichten erneut schicken. Der Consumer muss alle vom Fehler betroffenen Nachrichten konsumieren und ignorieren. Bis dahin werden je nach gewählter Umsetzungsvariante entweder alle Nachrichten blockiert, die in der Partition mit der fehlerhaften Nachricht landen oder die in den Partitionen landen, die dem Consumer zugewiesen sind. Für abhängige Nachrichten wird die Partition quasi zu einer erweiterten Dead Letter Queue, die beim Wiedereinlesen allerdings keine Synchronisation benötigt. Ob und wie viele unabhängige Nachrichten vom Pausieren betroffen sind, hängt vom Verhältnis der Anzahl der Kafka-Schlüssel zur Anzahl der Partitionen und ggf. auch von der Anzahl der Consumer ab.

Falls der Fehler im Consumer liegt, blockieren die Partitionen, die von dem Fehler aktuell betroffen sind. Wie stark das Pausieren den Durchsatz bremst, hängt von der Schwere des Fehlers, möglichen Zusammenhängen mit den Kafka-Schlüsseln sowie vom Zufall ab. Sobald ein Bugfix deployt wurde, laufen die pausierten Partitionen durch das automatische Wiederholen ohne operativen Eingriff selbstständig wieder an.

Vorteile:

  • Pausiert nur betroffene Partitionen eines Consumers bzw. nur einen Consumer. Die Granularität hängt von der Anzahl der Consumer und der Anzahl der Partitionen ab.
  • Die Umsetzungsvariante mit der Retry-Annotation ist einfacher als das Pausieren einzelner Partitionen.
  • Verhindert Ressourcenverschwendung durch unnötiges Rebalancing.
  • Bei notwendigem Rebalancing (z. B. Consumer stürzt ab, neue Consumer kommen zwecks Skalierung hinzu) wird der neue Consumer automatisch die fehlerhafte Partition wieder pausieren.
  • Das System macht automatisch weiter, nachdem das Problem behoben ist, z. B. durch Deployment eines Bugfixes. Auch bei sporadischen Problemen, die sich selbst nach kurzer Zeit lösen, macht das System automatisch weiter. Das Team profitiert dabei vom Prinzip der Selbstheilung.
  • Der manuelle Aufwand für die Fehlerbehebung ist beim Team des Consumers unabhängig von der Anzahl fehlerhafter Nachrichten.

Nachteile:

  • Falls es mehr Kafka-Schlüssel als Partitionen gibt (was der Normalfall sein dürfte), werden auch Schlüssel angehalten, die sich zufällig in derselben Partition befinden, aber nicht betroffen sind.
  • Bei der Umsetzungsvariante mit der Retry-Annotation werden auch Nachrichten in Partitionen angehalten, die nicht die fehlerhafte Nachricht enthalten, falls es weniger Consumer als Partitionen gibt.
  • Erfordert zusätzlichen Mechanismus zum Überspringen von fehlerhaften und ggf. auch abhängigen Nachrichten.
  • Bei Fehlern in der Nachricht müssen alle abhängigen Nachrichten vom Producer nochmals geschickt werden.

Einfach loggen

Zunächst dachten wir nach dem produktiven Einsatz unseres Systems einige Wochen lang, wir könnten es uns auf der Insel „Pausieren und Wiederholen“ gemütlich machen. Dann mussten wir jedoch einen Abstecher zu einer weiteren Strategie unternehmen, die wir ursprünglich als absurd angesehen und daher direkt aussortiert hatten. Mittlerweile haben wir sie jedoch für spezielle Fälle zu schätzen gelernt. Diese Fälle zeichnen sich dadurch aus, dass ein Sachbearbeiter im ursprünglichen System Daten manuell korrigieren muss. Das verringerte den Durchsatz bei der Strategie „Pausieren und Wiederholen“ so deutlich, dass wir die Nachricht nun mittels einfachem Loggen behandeln. Dabei haben wir sichergestellt, dass nur einzelne Nachrichten so behandelt werden, und setzen diese Strategie somit nicht als Catch-all-Strategie ein.

Diese Strategie loggt die fehlerhafte Nachricht und bzw. oder ihre Metadaten, zählt gegebenenfalls eine Metrik hoch und verarbeitet dann die nächste Nachricht (Listing 6). Auch diese Strategie bietet SmallRye an und nennt sie ignore [10], [15].

@Incoming("aggregate-in")
public CompletionStage<Void> consume(Message<String> record) {
  LOGGER.trace("Aggregate received: {}", record.getPayload());
  try {
    controller.process(record.getPayload());
  } catch (Exception e) {
    LOGGER.error("Oops, something went terribly wrong with "
      + record.getPayload(), e);
  }
  return record.ack();
}

Der Weg zur Korrektur ist hier unabhängig von der Fehlerursache manuell: Die Logmeldung oder die erhöhte Metrik stößt einen manuellen Prozess an, der die Fehlerursache beheben und dann die geloggten und damit nicht verarbeiteten Nachrichten reproduzieren muss. Aufgrund des hohen manuellen Aufwands sollte diese Strategie nur für Fehlerursachen angewandt werden, die einzelne Nachrichten betreffen. Insbesondere scheidet sie damit für Infrastrukturprobleme aus.

In unserem Projekt setzen wir diese Strategie für Nachrichten ein, die der Producer fehlerhaft erstellt hat. Nachdem der Fehler im Producer behoben wurde, lässt der manuelle Prozess die Nachricht und alle davon abhängigen Nachrichten erneut erzeugen.

Vorteil:

  • Überspringt nur Nachrichten, bei denen ein Fehler auftritt, und vermeidet damit eine Verzögerung nicht betroffener Nachrichten. Die Strategie maximiert also den Durchsatz.

Nachteile:

  • Wirft die Reihenfolge der Nachrichten durcheinander.
  • Verliert Nachrichten, falls die Fehlerursache im Consumer oder in der Infrastruktur liegt.

Fazit

Unsere Reise durch die Untiefen der asynchronen Fehlerbehandlung zeigt, dass je nach konkreter Problemstellung verschiedene Optionen infrage kommen, die auf den ersten Blick absurd erscheinen. Ganz prinzipiell kann man die von uns gefundenen Optionen nach der Granularität des Eingriffs und der Komplexität ihrer Umsetzung sortieren (Abb. 1).

Abb. 1: Strategien im Überblick

Einfach loggen bedeutet de facto keinen Eingriff und ermöglicht so einen hohen Durchsatz. Zudem ist es bestechend simpel. Der Preis ist das Risiko des Verlusts der korrekten Reihenfolge und kompletter Nachrichten. Die klassische Dead Letter Queue verhindert den Datenverlust durch einen minimalen Eingriff, kann aber die Reihenfolge auch nicht sicherstellen. Sie ist dafür auch etwas komplexer, als nur zu loggen. Die erweiterte Dead Letter Queue greift exakt so viel ein wie notwendig, um Datenverlust zu verhindern und die Reihenfolge zu erhalten. Dafür muss man sich aber um eine Reihe von Detailproblemen kümmern. Diese Option ist deshalb die komplexeste. Bei Pausieren und Wiederholen werden u. U. auch unabhängige Nachrichten angehalten, allerdings selten das gesamte Topic. Datenverlust und Reihenfolge sind kein Thema. Die Komplexität ist moderat und beschränkt sich größtenteils auf das Wissen über die Konfiguration von Kafka selbst. Die einfachste Option, die Datenverlust vermeidet und die Reihenfolge einhält, ist das komplette Herunterfahren des Consumers. Das bedeutet aber in den meisten Fällen auch den niedrigsten Durchsatz während eines Fehlers.

Selbstverständlich kann man all diese Lösungen durch weitere kleinere Lösungen ergänzen. Auch wenn wir auf der Suche nach einer Catch-all-Strategie waren: Oft ist es besser, Probleme dort zu lösen, wo sie auftreten. Das heißt, dass z. B. eine Datenbankausnahme auch einfach das Wiederholen des Datenbankaufrufs auslösen kann, statt die Verarbeitung der Nachricht sofort abzubrechen. Für bestimmte fachliche Spezialfälle mögen sogar einfaches Loggen und eine manuelle Lösung durch den Fachbereich akzeptabel sein.

Für unser Projekt haben wir uns am Ende für Pausieren und Wiederholen entschieden. Die erweiterte Dead Letter Queue wurde uns zu komplex und wir waren uns nicht sicher, ob wir trotz dieser Komplexität nicht immer noch manuell in die Produktion würden eingreifen müssen. Pausieren und Wiederholen waren dagegen vergleichsweise simpel zu verstehen und dabei nicht zu restriktiv. Vor allem aber faszinierte uns, dass wir damit effektiv manuelle Eingriffe in der Produktion vermeiden konnten. Das automatische Wiederholen bedeutete letztlich den Beginn einer geradezu magischen neuen Eigenschaft unseres Systems: der Selbstheilung.


Links & Literatur

[1] https://6sense.com/tech/queueing-messaging-and-background-processing/apache-kafka-market-share

[2] https://entwickler.de/software-architektur/kafka-101/

[3] https://www.dddcommunity.org/library/vernon_2011/

[4] https://twitter.com/mathiasverraes/status/632260618599403520

[5] https://github.com/HilmarTuneke/kafka-fehlerbehandlung

[6] https://smallrye.io/smallrye-reactive-messaging/4.16.0/kafka/receiving-kafka-records/#acknowledgement

[7] https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy

[8] https://smallrye.io/smallrye-reactive-messaging/4.16.0/kafka/receiving-kafka-records/#configuration-reference

[9] https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/

[10] https://smallrye.io/smallrye-reactive-messaging/4.16.0/kafka/receiving-kafka-records/#failure-management

[11] https://quarkus.io/blog/kafka-failure-strategy/#the-fail-fast-strategy

[12] https://quarkus.io/blog/kafka-failure-strategy/#the-dead-letter-topic-strategy

[13] https://kafka.apache.org/37/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html#pause(java.util.Collection)

[14] https://kafka.apache.org/37/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html#resume(java.util.Collection)

[15] https://quarkus.io/blog/kafka-failure-strategy/#the-ignore-strategy

The post Resiliente Kafka Consumer bauen appeared first on JAX.

]]>
Showdown der funktionalen Programmierung auf der JVM https://jax.de/blog/kotlin-vs-scala-funktionale-programmierung-jvm/ Mon, 19 Feb 2024 10:08:20 +0000 https://jax.de/?p=89461 Kotlin ist eine Weiterentwicklung von Java mit zusätzlichen Features und Vereinfachungen. Die Ideen dafür kamen überwiegend aus der funktionalen Programmierung. Kein Wunder, bietet diese doch gegenüber OOP viele Vorteile, wie architektonische Entkopplung durch unveränderliche Daten und bessere Domänenmodelle durch mächtigere Abstraktionen. Aber reichen Kotlins Innovationen aus, um es zu einer waschechten funktionalen Programmiersprache zu machen?

The post Showdown der funktionalen Programmierung auf der JVM appeared first on JAX.

]]>
Der Begriff „funktionale Programmiersprache“ gründete sich ursprünglich darauf, dass die Sprache Funktionen „erstklassig“, also als normale Objekte behandelt und erlaubt, sie zur Laufzeit zu erzeugen. Bei statisch typisierten Sprachen (und dazu gehört auch Kotlin) bedeutet der Begriff außerdem, dass es das Typsystem erlaubt, Funktionen zu beschreiben, und die erstklassige Manipulation von Funktionen zumindest nicht behindert.

Ein wichtiger pragmatischer Aspekt funktionaler Programmierung (FP) ist die Verwendung unveränderlicher (immutable) Daten. Diese kann eine Programmiersprache mehr oder weniger gut unterstützen. Dazu gehört auch die Möglichkeit, Funktionen ohne die Verwendung von Zuweisungen zu schreiben. Schließlich zählt die Unterstützung von höherstehenden Abstraktionen wie Monaden zur Standardausrüstung funktionaler Sprachen.

Unveränderliche Daten

Das mit den unveränderlichen Daten klingt eigentlich ganz einfach: Wir verzichten auf Zuweisungen, fertig. Damit das praktikabel wird, gehört allerdings doch mehr dazu. Wenn eine Klasse unveränderliche Objekte beschreibt, ist es meist sinnvoll, einen Konstruktor zu definieren, der für jedes Attribut der Klasse ein Argument akzeptiert, außerdem equals– und hashCode()-Methoden, die ausschließlich über die Gleichheit beziehungsweise die Hashfunktion der Attribute definiert sind, Ähnliches gilt für die toString()-Methode. In Java hat die IDE, die Konstruktor und Methoden automatisch generieren kann, diesen Job traditionell erledigt. Danach liegt aber eine Menge Code herum, der nur das Offensichtliche tut und Übersicht sowie Wartung behindert.

Stay tuned

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

 

Glücklicherweise hat Kotlin sogenannte Data Classes, in denen dieser Code automatisch vom Compiler generiert wird. Das ist so eine Klasse:

data class Rectangle(val length: Double, val width: Double)

Das data vor class sorgt für die automatische Generierung von Konstruktor und Methoden, val sorgt dafür, dass die entsprechenden Attribute nicht verändert werden können. (Weil das so praktisch ist, hat Java längst mit Records nachgezogen.)

Bei funktionalen Sprachen sind Pendants zur Java Collections Library dabei, die allerdings ebenfalls ohne Veränderungen auskommen. Bei Kotlin sind scheinbar unveränderliche Collections dabei. So hängen wir zum Beispiel eine Liste an eine andere Liste:

val list1 = listOf(1,2,3)
val list2 = listOf(4,5,6)
val list3 = list1 + list2

Hier ist list3 eine neue Liste mit den gleichen Elementen wie list1 (die unverändert bleibt) und den Elementen von list2 zusätzlich dahinter. Hinter den Kulissen tut aber die gute alte Java ArrayList ihren Dienst: Das bedeutet, dass die Operation + list2 ein neues Array für list3 anlegt und alle Elemente sowohl von list1 als auch von list2 nach list3 kopiert – und das ist teuer. Aus diesem Grund verwenden die Collection Libraries funktionaler Sprachen sogenannte funktionale Datenstrukturen, die nicht bei jedem Update alles kopieren müssen. Insbesondere ist eine Liste in der funktionalen Programmierung eine konkrete Datenstruktur, die bei allen (anderen) funktionalen Sprachen fest eingebaut ist. Wir können sie aber in Kotlin nachbauen:

sealed interface List<out A>
object Empty : List<Nothing>
data class Cons<A>(val first: A, val rest: List<A>) : List<A>

Eine Liste ist also entweder Empty (die leere Liste) oder eine sogenannte Cons-Liste, bestehend aus erstem Element und rest-Liste (es handelt sich also um eine unveränderliche, einfach verkettete Liste). FP-Pendants zu den ersten beiden Listen von oben können wir so konstruieren:

val list1 = Cons(1, Cons(2, Cons(3, Empty)))
val list2 = Cons(4, Cons(5, Cons(6, Empty)))

Das ist etwas umständlich, verglichen mit listOf, dieses Manko könnten wir aber mit einer einfachen varargs-Funktion beheben. Die plus-Methode auf den eingebauten Kotlin-Listen können wir für die FP-Listen ebenfalls implementieren:

fun <A> List<A>.append(other: List<A>): List<A> =
  when (this) {
    is Empty -> other
    is Cons ->
       Cons(this.first, this.rest.append(other))
  }

Das Beispiel von oben funktioniert mit FP-Listen so:

val list3 = list1.append(list2)

Die append-Methode wirft ein paar Fragen auf: Warum ist sie eine „Extension Method“? Könnte das bei der Rekursion Probleme machen? Dazu später mehr, hier geht es ja erst einmal um unveränderliche Daten: Und tatsächlich verändert append gar nichts, kopiert die Elemente aus list1, hängt aber list2 an, ohne sie zu kopieren. Hinterher teilen sich also list3 und list2 dieselben Cons-Objekte. Weil Listen unveränderlich sind, ist das unproblematisch: Das Programm kann so tun, als wären list2 und list3 völlig unabhängig voneinander. Die append-Methode verbraucht nur so viel Laufzeit und Speicher, wie list1 lang ist – die Länge von list2 ist völlig irrelevant.

Listen sind für viele Situationen geeignet, aber natürlich nicht für alle – wenn list1 lang ist, braucht append auch mehr Zeit und viel Speicherplatz. Entsprechend haben die Collection Libraries funktionaler Sprachen in der Regel auch noch ausgefeiltere Datenstrukturen auf der Basis von Bäumen beziehungsweise Trees im Gepäck, bei denen die meisten Operationen quasi konstant laufen.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Solche funktionalen Datenstrukturen gibt es auch für Java und Kotlin in Form der Vavr-Library [1], die neben FP-Listen auch Vektoren mitbringt. Für Vavr gibt es auch das Add-on vavr-kotlin [2], das die Vavr Collections in Kotlin noch etwas idiomatischer zugänglich macht. Zwischenfazit: Funktionale Datenstrukturen sind bei Kotlin nicht standardmäßig dabei, können aber in Form von Vavr nachgeladen werden. Tolle Sache.

Erstklassige Funktionen

Kommen wir zur Unterstützung für Funktionen als Objekte: Da macht Kotlin gegenüber Java einen riesigen Sprung durch zwei Neuerungen:

  • Das Kotlin-Typsystem kennt Funktionstypen, die man sich in Java umständlich als Single-Method-Interface selbst definieren muss.
  • Kotlin macht effektiv keinen Unterschied zwischen Value Types und Reference Types.

Was das bringt, zeigt die Implementierung des FP-Klassikers map, den eine Funktion (die als Objekt übergeben wird) auf alle Elemente einer Liste anwendet und dann eine Liste der Ergebnisse zurückliefert:

fun <A, B> List<A>.map(f: (A) -> B): List<B> =
  when (this) {
    is Empty -> Empty
    is Cons ->
     Cons(f(this.first),
       this.rest.map(f))
  }

Super-Sache:

val list4 = list3.map { x -> x + 1 }

Das geht natürlich auch in Java, das Pendant sieht fast identisch aus:

var list4 = list3.map(x -> x + 1);

Allerdings funktioniert das in Java nur so richtig gut, wenn der Lambdaausdruck unmittelbar im Methodenaufruf steht. In dem Moment, in dem wir ihn herausziehen, sieht die Sache anders aus. In Kotlin ist das kein Problem:

val inc = { x: Int -> x + 1 }
val list4 = list3.map(inc)

Wir müssen bei inc den Typ von x angeben, weil noch andere Typen die Operation + 1 (Double, Long) unterstützen. Den Typ der Funktion inferiert Kotlin nun als (Int) -> Int. In Java müssen wir uns allerdings beim Typ von inc für ein Interface entscheiden – am Naheliegendsten ist IntUnaryOperator:

IntUnaryOperator inc = x -> x + 1;

Allerdings können wir den nicht an map übergeben – es kommt zu einer Fehlermeldung: „‘map(Function<Integer,B>)’ cannot be applied to ‘(IntUnaryOperator)’“. Das liegt daran, dass map ein Argument des Typs Function<A, B> akzeptiert – wir mussten uns ja für ein spezifisches Interface entscheiden. Leider passen Function und IntUnaryOperator nicht zueinander, weil Function auf Referenztypen besteht, während IntUnaryOperator auf den Value Type int festgelegt ist. Die Konsequenz ist, dass zum Beispiel das Stream-Interface in Java neben map auch mapToInt, mapToDouble und mapToLong anbieten muss, die jeweils ein IntStream, DoubleStream und LongStream produzieren. Das ist umständlich und macht fortgeschrittene Anwendungen von Funktionen höherer Ordnung in Java unpraktikabel – in Kotlin kein Problem, ein weiterer Pluspunkt.

Schleifen und Endrekursion

Zu erstklassiger Unterstützung von Funktionen gehört auch, dass wir in der Lage sein sollten, Funktionen ohne Verwendung von Zuweisungen zu schreiben. Dafür sind append und map gute Beispiele – rein funktional programmiert. Sie haben aber beide denselben Haken: Für lange Listen funktionieren sie nicht, weil sie einen Stack-Overflow verursachen. Das liegt an einigen Eigenheiten der JVM: Sie muss sich merken, wie es nach dem rekursiven Aufruf weitergeht (mit Cons) und verwendet dafür einen speziellen Speicherbereich, den Stack. Dieser Stack ist in seiner Größe beschränkt und viel kleiner als der Hauptspeicher, der eigentlich zur Verfügung stünde. Außerdem ist die Größe beim Start der JVM festgelegt und später nicht mehr zu ändern. Funktionale Sprachen mit eigener Runtime haben dieses Problem meist nicht, weil sie flexiblere Datenstrukturen anstatt eines Stacks mit fester Größe verwenden.

fun <A> List<A>.append(other: List<A>): List<A> {
  var list = this.reverse()
  var acc = other
  while (list is Cons) {
    acc = Cons(list.first, acc)
    list = list.rest
  }
  return acc
}

In Java wäre der einzige Weg, das Problem zu vermeiden, eine Schleife – und Schleifen benötigen Zuweisungen, um von einem Durchlauf zum nächsten zu kommunizieren. In Kotlin sieht das so aus wie in Listing 1. Die neue append-Funktion benutzt zwei Schleifenvariablen: die Eingabeliste und das Zwischenergebnis, den Akkumulator. Die Eingabeliste wird am Anfang umgedreht, weil Listen von hinten nach vorn konstruiert werden.

Das neue append funktioniert, ist aber aus funktionaler Sicht schlecht – insbesondere, weil while-Schleifen fehleranfällig sind. Werden zum Beispiel die beiden Zuweisungen an acc und list vertauscht, funktioniert die Methode nicht mehr. Diese Methode können wir auch ohne Zuweisungen mit einer Hilfsfunktion schreiben, die den Akkumulator als Parameter akzeptiert (Listing 2).

fun <A> List<A>.append(other: List<A>): List<A> =
  appendHelper(this.reverse(), other)

fun <A> appendHelper(list: List<A>, acc: List<A>): List<A> =
  when (list) {
    is Empty -> acc
    is Cons ->
      appendHelper(list.rest, Cons(list.first, acc))
  }

Diese Version kommt ohne Zuweisungen aus und benutzt einen rekursiven Aufruf statt der while-Schleife. (Die Hilfsfunktion könnten wir auch lokal innerhalb von append definieren – ein weiterer Punkt für FP-Support in Kotlin.) Anders als der rekursive Aufruf der ursprünglichen append-Version ist dieser hier ein sogenannter Tail Call – also ein Aufruf, der am Ende der Funktion steht.

 

Solche Tail Calls brauchen eigentlich keinen Platz auf dem Stack – entsprechend bestünde bei appendHelper auch keine Gefahr, dass der Stack überläuft. „Eigentlich“ deshalb, weil die JVM leider auch für Tail Calls völlig unnötig Stackplatz verbraucht. (Das ist ein seit mindestens 1996 bekannter Bug, und FP-Fans fiebern mit jedem neuen JVM-Release darauf, dass endlich ein Fix dabei ist.) Wir können aber den Kotlin-Compiler überreden, den rekursiven Tail Call intern in eine Schleife zu kompilieren, indem wir vor die Funktionsdefinition das Wort tailrec schreiben. Das geht leider nur bei statischen Selbstaufrufen (insbesondere also nicht bei Aufrufen von Methoden, die spät gebunden werden), aber immerhin, mehr ist auf der JVM auch nicht drin. Zwischenfazit: Funktionen sind in Kotlin tatsächlich so erstklassig, wie es auf der JVM geht. Die Richtung stimmt also auch hier.

Typen

Kommen wir nochmal zu den Typen: Wir haben ja schon gesehen, dass Kotlin mit Funktionstypen eine wesentliche Anforderung funktionaler Programmierung erfüllt. Leider hat das Typsystem aber auch einige Tücken. Um die zu verstehen, werfen wir einen Blick darauf, wie die Definition von Listen in der funktionalen Programmiersprache Haskell aussehen würde (wenn Listen da nicht schon eingebaut wären):

data List a = Empty | Cons a (List a)

Die Struktur ist ähnlich wie die Kotlin-Definition, aber zwei Unterschiede fallen auf: In Kotlin steht beim Typparameter A noch ein out davor. Außerdem steht beim Empty-Objekt Nothing als Typparameter von List.

Das kommt daher, dass Kotlin versucht, sowohl OO als auch FP zu unterstützen: OO braucht Klassenhierarchien, was auf der Typebene zu Subtyping führt. Deshalb ist es dort notwendig, dass bei Empty ein Typparameter für List angegeben wird – und da der Typparameter A für den Typ der Elemente der Liste steht, die leere Liste aber keine Elemente hat, müssen wir einen Typ angeben, der zu allen anderen passt. In Kotlin ist das eben Nothing, ein „magischer“ Typ ohne Elemente, der als Subtyp aller anderen Typen fungiert (das Gegenteil von Any sozusagen.) Außerdem ist out notwendig, damit eine Liste aus Elementen bestehen kann, die unterschiedliche Typen haben – solange sie einen Supertyp gemeinsam haben. Falls das kompliziert klingt: ist es. Wir können uns der Sache annähern, indem wir out entfernen. Sofort kommt eine Fehlermeldung, zum Beispiel bei:

val list1 = Cons(1, Cons(2, Cons(3, Empty)))

Abgekürzt lautet diese so: „Type mismatch: inferred type is Int but Nothing was expected“. Das liegt daran, dass Empty den Typ List<Empty> hat, wir aber für die Gesamtliste den Typ List<Int> brauchen. Das out sorgt jetzt dafür, dass List<Empty> ein Subtyp von List<Int> ist und damit alles wieder passt – ein Fall von sogenannter Kovarianz. (Wenn wir in geschrieben hätten, wäre Kontravarianz angesagt und es würde andere Fehlermeldungen geben.)

Das Wort out steht dafür, dass die Methoden von List andere Listen nur als „Output“ produzieren dürfen. Das bedeutet insbesondere, dass wir append nicht als „normale“ Methode innerhalb von List implementieren können, weil sie die zweite Liste als Eingabe akzeptiert. Diese Einschränkung gilt nicht für Extension Methods, weshalb wir append als eine solche definiert haben.

Die Situation ist aber immerhin besser als in Java, das erfordert, die Varianzannotationen an die Methoden zu schreiben. Die Java-Stream-Version von append, genannt concat (ebenfalls als statische Methode realisiert), hat zum Beispiel diese Signatur:

static <T> Stream<T> concat(Stream<? extends T> a,
                            Stream<? extends T> b)

Die Kombination von OO und FP führt also zu einigen Komplikationen, auch wenn Kotlin die Situation besser meistert als Java.

Higher-kinded Types

Kotlin hat sich sichtlich von Scala inspirieren lassen, was die FP-Unterstützung betrifft, aber auch auf einige Aspekte von Scala verzichtet – in der Hoffnung, die aus Scala bekannte Komplexität einzudämmen. Dem sind leider auch nützliche Features des Scala-Typsystems zum Opfer gefallen.

Eine wesentliche Einschränkung gegenüber Scala betrifft sogenannte Higher-kinded Types, die es in Scala gibt, in Kotlin leider nicht. Zur Erläuterung dieser Typen schauen wir uns eine Kotlin-Version des Optional-Typs aus Java an. Er ist nützlich, wenn eine Funktion manchmal ein Ergebnis liefert, manchmal aber auch nicht. Entsprechend gibt es zwei Fälle Some und None, ähnlich wie bei Listen:

sealed interface Option<out A>
object None : Option<Nothing>
data class Some<A>(val value: A) : Option<A>

Wir können für Option eine praktische map-Methode definieren:

fun <A, B> Option<A>.map(f: (A) -> B): Option<B> =
  when (this) {
    is None -> None
    is Some -> Some(f(this.value))
  }

Diese Verwandtschaft mit der map-Methode von List können wir sehen, wenn wir beide Signaturen untereinanderschreiben:

fun <A, B> List  <A>.map(f: (A) -> B): List  <B>
fun <A, B> Option<A>.map(f: (A) -> B): Option<B>

Typen wie List und Option, die eine map-Operation unterstützen, heißen in der funktionalen Programmierung Funktor und spielen dort eine wichtige Rolle. Deshalb liegt es nahe, ein Interface Functor zu definieren, in dem die map-Methode steht. Das könnte so aussehen:

interface Functor<F> {
  fun <A, B> map(f: (A) -> B): F<B>
}

Mit dieser Definition wäre Functor ein Higher-kinded Type: Ein Typkonstruktor (also ein Typ mit Generics), der einen anderen Typkonstruktor als Parameter akzeptiert. Das ist dann aber leider doch ein Schritt zu viel für Kotlin, der Compiler quittiert: „Type arguments are not allowed for type parameters“. Es gibt zwar einen Trick, um Higher-kinded Types zu simulieren, nachzulesen zum Beispiel in dem wunderbaren Buch „Functional Programming in Kotlin“ [3]. Er ist aber so umständlich, dass er in der Praxis kaum anwendbar ist.

Stay tuned

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

 

Beim Typsystem ist in Kotlin also noch Luft nach oben. Dass es geht, zeigt Scala, das Higher-kinded Types unterstützt und für das es entsprechend Libraries mit höherstehenden Konzepten wie Functor gibt, die in Kotlin einfach nicht praktikabel sind. Fairerweise muss man aber zugegeben, dass Higher-kinded Types auch von manchen „richtigen“ funktionalen Sprachen wie F# nicht unterstützt werden.

Monaden

Wo funktionale Programmierung ist, sind Monaden oft nicht weit: Sie sind dort fürs Grobe zuständig und erlauben die Verknüpfung von Funktionen in sequenziellen Abläufen mit Effekten. Effekte sind Dinge, die neben der reinen Berechnung stattfinden – typischerweise Interaktionen mit der Außenwelt. Effekte können zum Beispiel benutzt werden, um in hexagonaler Architektur zwischen Ports und Adaptern zu vermitteln. Für eine komplette Einführung in Monaden fehlt uns hier der Platz, darum skizzieren wir nur, wie sie in Kotlin funktionieren.

Die Idee ist ganz einfach: Ein sequenzieller Prozess wird durch ein Objekt repräsentiert. Für die Objekte erstellen wir einen speziellen Typ (das ist die Monade), in dem genau die Operationen enthalten sind, die in dem Prozess zulässig sind. Zum Beispiel enthält die kleine Demo-Library kotlin-free-monads [4] ein Interface TtyM, um Prozesse abzubilden, die eine textuelle Ausgabe generiereren:

sealed interface TtyM<out A> {
  fun <B> bind(next: (A) -> TtyM<B>): TtyM<B>
}

Die bind-Operation (manchmal auch flatMap genannt) macht zusammen mit einer sogenannten pure-Operation TtyM zu einer Monade. Der Typparameter A ist für das Ergebnis des Prozesses zuständig. Wenn wir die TtyM-Monade benutzen, verwenden wir also nicht mehr println, sondern konstruieren stattdessen ein Write-Objekt, dessen Klasse TtyM implementiert.

Diese Vorgehensweise hat den Vorteil, dass wir das TtyM-Objekt in unterschiedlichen Kontexten ausführen können. Im normalen Betrieb macht das folgende Funktion, die für jedes Write einfach println ausführt:

fun <A> run(tty: TtyM<A>): A

Für automatisierte Tests wollen wir aber wissen, welcher Text ausgegeben wurde, und dafür gibt es eine Funktion, die die Zeilen des Outputs in eine Liste schreibt:

fun <A> output(tty: TtyM<A>, output: MutableList<String>): A

Das ist praktisch, wenn wir schon ein TtyM-Objekt haben. Aber dessen Konstruktion mit Konstruktor- und Methodenaufrufen ist normalerweise ziemlich umständlich, mit vielen geschachtelten Lambdaausdrücken und Klammern. Aus diesem Grund haben die meisten funktionalen Sprachen eine spezielle, deutlich kompaktere Syntax für monadische Programme. Kotlin bringt eine solche Syntax zwar nicht direkt mit, wir können sie aber mit Hilfe von suspend-Funktionen selbst bauen, sodass ein TtyM-Programm zum Beispiel so gebaut werden kann:

TtyDsl.effect {
  write("Mike")
  write("Sperber")
  pure(42)
}

Das Lambdaargument von effect ist eine suspend-Funktion. Das suspend sorgt dafür, dass der Compiler die Funktion in eine Coroutine transformiert. Die wiederum erlaubt der effect-Funktion, eine monadische Form der Funktion zu extrahieren. Wer sich für die Details interessiert, sei an das Kotlin-free-monads-Projekt [4] verwiesen.

Auch hier also ein Pluspunkt für Kotlin, zumindest gegenüber Java. Die Verwendung von suspend-Funktionen für monadische Syntax ist aber gegenüber funktionalen Sprachen deutlich aufwendiger und auch mit einigen Einschränkungen verbunden, sodass immer noch Luft nach oben bleibt.

Fazit

Mit zwei Fragen sind wir in diesen Artikel gestartet:

  1. Ist Kotlin funktional genug, dass es sich in lohnt, darin funktional zu programmieren?
  2. Ist Kotlin die richtige Sprache, wenn man auf der JVM funktional programmieren möchte?

Die Antwort auf die erste Frage ist ein entschiedenes Ja: Kotlin unterstützt die einfache Definition unveränderlicher Daten, kennt Typen für Funktionen, eliminiert die lästige Unterscheidung zwischen Referenztypen und Value Types, kompiliert rekursive Tail Calls korrekt, vereinfacht gegenüber Java den Umgang mit Generics und erlaubt die Definition monadischer Syntax. Das ist mehr als genug, um funktionale Programmierung gewinnbringend zu nutzen, mächtige Abstraktionen zu definieren und architektonisch von der Entkopplung durch Unveränderlichkeit zu profitieren. Wer mehr darüber wissen möchte, dem sei das oben erwähnte Buch [3] ans Herz gelegt. Eine grundsätzliche Einführung in funktionale Programmierung gibt es in meinem eigenen Buch „Schreibe Dein Programm!“ [5].

Die zweite Frage ist schwieriger zu beantworten: Auf der JVM gibt es zwei (weitere) funktionale Sprachen, nämlich Clojure und Scala. Beide sind großartig – während aber Clojure in vielerlei Hinsicht fundamental anders tickt als Kotlin und Java (kein Typsystem, Lisp-Syntax), ist Scala die Verlängerung der Linie von Java zu Kotlin.

 

Scala unterstützt funktionale Programmierung besser als Kotlin: Es gibt Higher-kinded Types, die Monaden funktionieren in Scala deutlich einfacher und außerdem erleichtern sogenannte Implicits es, automatisch die richtigen Implementierungen für Interfaces zu finden. Des Weiteren existiert um Scala herum ein umfangreiches Ökosystem funktionaler Libraries und Frameworks.

Die gegenüber Scala verringerte Komplexität von Kotlin fällt in der Praxis kaum ins Gewicht. Viele der typischen Schwierigkeiten in der täglichen Arbeit gibt es in Kotlin auch. Die IDE-Unterstützung ist für beide ähnlich gut. Wer also funktionale Programmierung mag und die Wahl hat, wird wahrscheinlich mit Scala glücklicher.


Links & Literatur

[1] Vavr, funktionale Library für Java: https://github.com/vavr-io/vavr

[2] Kotlin-Wrapper für Vavr: https://github.com/vavr-io/vavr-kotlin

[3] Vermeulen, Marco; Bjarnason, Rúnar; Chiusano, Paul: „Functional Programming in Kotlin“; Manning, 2021

[4] Monaden-Library für Kotlin: https://github.com/active-group/kotlin-free-monad

[5] Sperber, Michael; Klaeren, Herbert: „Schreibe Dein Programm!“; Tübingen University Press, 2023

The post Showdown der funktionalen Programmierung auf der JVM appeared first on JAX.

]]>
Sichere Web-Apps für alle https://jax.de/blog/sichere-web-apps-content-security-policy-csp-gegen-cross-site-scripting-xss-tipps-tricks/ Fri, 05 Jan 2024 11:53:43 +0000 https://jax.de/?p=89358 Das Einschleusen von Schadcode über eine fremde Domäne lässt sich mit einer Content Security Policy (CSP) erheblich einschränken. Erfahren Sie, wie Sie Ihre Webapplikation gegen Cross-site-Scripting-(XSS-)Attacken härten können.

The post Sichere Web-Apps für alle appeared first on JAX.

]]>
Formulare in Webapplikationen sind für so manche Hackerattacke ein geeignetes Einfallstor. Ganz gleich, ob es ein Angriff auf die Datenbank mit einer SQL Injection ist, oder ob schädliches JavaScript per XSS [1] nachgeladen und zur Ausführung gebracht wird.

Gefahr durch Eingaben

Sicherlich ist der erste und wichtigste Schritt, sämtliche Variablen, die durch Formularfelder befüllt werden, auf eine gültige Eingabe hin zu prüfen. Ein gängiges Mittel ist die Validierung über reguläre Ausdrücke, die sicherstellen, dass nur gültige Zeichen und ein korrektes Format zugelassen werden. Möchte man aber beispielsweise formatierte und umfangreiche Artikel ermöglichen, wie es bei einem Content-Management-System der Fall ist, haben die gängigen Schutzmaßnahmen durchaus ihre Schwächen. Denn das Prinzip, möglichst die Eingabe zu beschränken und gefährliche Zeichen herauszufiltern, hat bei formatiertem Text seine Grenzen.

Wollen wir beispielsweise ein Zitat in Anführungszeichen setzen, können wir die beiden Zeichen ‘ und “ nicht einfach aus dem Text entfernen, da diese ja Bestandteil des Inhalts sind. In der Vergangenheit nutzte man bei dieser Problematik einen zusätzlichen Mechanismus, um Schutz vor Cyberangriffen sicherzustellen. Hierfür werden sämtliche kritische Zeichen wie spitze Klammern, Anführungszeichen und Backslashes über ihre HTML Escapes ersetzt. Damit gelten diese als problematisch klassifizierten Sonderzeichen nicht mehr als Quellcode, da aus > ein &gr; wird. Daher sind die soeben beschriebenen Maßnahmen besonders wirkmächtig und bieten bereits einen effektiven Schutz.

Die Schutzwirkung lässt sich allerdings über den Mechanismus CSP erheblich verbessern, da dieser weit über das Prüfen von Nutzereingaben hinausgeht. Sämtliche in einer Webapplikation geladene Ressourcen wie Bilder, CSS und JavaScript lassen sich dank dieser Technologie steuern.

Die Content Security Policy ist ein Projekt des W3C und greift als Bestandteil des Browsers – es sind keine zusätzlichen Plug-ins oder Ähnliches zu installieren. Für aktuelle Browser gilt CSP Level 3. Auf der entsprechenden Webseite des W3C [2] finden Sie eine vollständige Übersicht aller Versionen einschließlich der unterstützenden Webbrowser.

In Umgebungen, die erhöhte Sicherheitsanforderungen erfordern, beispielsweise Onlinebanking, kann die Browserversion des Clients abgefragt werden und bei erheblich veralteten Varianten des Browsers somit der Zugriff verweigert werden.

String userAgent = request.getHeader("user-agent");
String browserName = "";
String  browserVer = "";
if(userAgent.contains("Chrome")){
  String substring=userAgent.substring(userAgent.indexOf("Chrome")).split(" ")[0];
  browserName=substring.split("/")[0];
  browserVer=substring.split("/")[1];
} else if(userAgent.contains("Firefox")) {
  String substring=userAgent.substring(userAgent.indexOf("Firefox")).split(" ")[0];
  browserName=substring.split("/")[0];
  browserVer=substring.split("/")[1];
}

Stay tuned

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

 

Listing 1 zeigt die Möglichkeit, über den request die Identifikation des Browsers auszulesen. Anschließend demonstrieren die beiden Beispiele, wie die Versionsnummer aus dem Text für Chrome und Firefox extrahiert wird. Bei dieser Strategie sollte man sich aber bewusst sein, dass es für Schwindler recht einfache Möglichkeiten gibt, die Nutzung eines anderen Browsers vorzugaukeln.

Damit wird uns noch einmal vor Augen geführt, dass Web Application Security als ganzheitliches Konzept zu verstehen ist, in dem mehrere Maßnahmen als Verbund wirken – denn einzelne Techniken lassen sich durchaus umgehen. Schauen wir uns daher nun den Wirkmechanismus von CSP im Detail an.

Beginnen wir mit einer reinen HTML-Ausgabe, die CSS, Google Fonts, CDN JavaScript, ein YouTube iFrame und Grafiken enthält. Listing 2 dient uns als Ausgangspunkt, ohne den Einsatz von CSP.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    
    <title>Content Security Policy Demo</title>
    
    <link rel="stylesheet" href="style.css" />
    
    <link
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
      rel="stylesheet" />
    
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
      integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2"
      crossorigin="anonymous" />
  </head>

  <body>
    <main>
      <div class="box">
        <div id="vue"></div>
      </div>

      <div class="box">
        <div class="embed">
          <iframe width="100%" height="500px" frameborder="0"
            src="https://www.youtube.com/embed/3nK6rcAbuzo"
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
            allowfullscreen></iframe>
        </div>
      </div>

      <div class="box">
        <div class="grid">
          <img src="https://images.unsplash.com/photo-1535089780340-34cc939f9996?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80" />
          <img src="https://images.unsplash.com/photo-1587081917197-95be3a2d2103?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80" />
          <img src="https://images.unsplash.com/photo-1502248103506-76afc15f5c45?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80" />
        </div>
      </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"></script>
    <script nonce="rAnd0m">
      new Vue({
        el: '#vue',
        render(createElement) {
          return createElement('h1', 'Hello World!');
        },
      });
    </script>
  </body>
</html>

Abb. 1: HTML ohne CSP

Wie wir in Abbildung 1 sehen können, handelt es sich hierbei um eine einfache HTML-Seite, die verschiedenen Content lädt. Dazu gehört zum Beispiel auch, das JavaScript-Framework Bootstrap extern über das Content Delivery Network (CDN) jsDelivr zu laden. Wer mit dem Thema CDN noch nicht vertraut ist, findet dazu einen sehr informativen Artikel auf Wikipedia [3]. Stellen wir uns nun einmal ein realistisches Szenario aus der Praxis vor.

Es könnte nun sein, dass die von uns verwendete Bootstrap-Bibliothek eine bekannte Sicherheitslücke für XSS-Angriffe enthält. Der Content des CDN von jsDelivr ist durchaus vertrauenswürdig. Ein Angreifer könnte nun aber versuchen, von einem Webspace eine eigens präparierte JavaScript-Datei über die Bootstrap-Sicherheitslücke einzuschleusen. Ich möchte an dieser Stelle natürlich kein Hackertutorial an die Hand geben und erklären, wie ein solcher Angriff ausgeführt werden kann; ebensowenig möchte ich Ideen verbreiten, welcher Unfug sich über XSS anstellen lässt. Mir geht es darum, eine zuverlässige Methode vorzustellen, die den Angriff wirkungsvoll verhindern kann. Also richten wir unser Augenmerk nun auf die Prävention von XSS.

Schutz durch CSP

Mittels CSP werden nun Meta-Header-Attribute gesetzt, mit denen bestimmt werden kann, was zuverlässige Quellen sind. Das erlaubt es jedem Contenttyp (Ressource, also Bild, CSS und so weiter), explizite, vertrauenswürdige Quellen (Domains) zuzuweisen. Damit lassen sich JavaScript-Dateien so einschränken, dass sie nur vom eigenen Server geladen werden und beispielsweise von CDN jsDelivr. Findet ein Angreifer nun einen Weg, eine XSS-Attacke auszuführen, bei der er versucht, von einer anderen Domain als von den erlaubten Domains Schadcode einzuschleusen, blockiert CSP das Skript von der nicht freigegebenen Domain.

Dazu ein einfaches Beispiel: Wenn wir im <head> in Listing 2 gleich nach dem meta-Tag für die Seitencodierung die Zeile <meta http-equiv=”Content-Security-Policy” content=”default-src ‘self'” /> einfügen und die Datei ausführen, bekommen wir eine Ausgabe wie in Abbildung 2.

Abb. 2: Blocked Content

Der Teil content=”default-src ‘self'” sorgt dafür, dass sämtliche Inhalte, die nicht von der eigenen Domäne stammen, über CSP im Browser blockiert werden. Wollen wir zusätzlich für JavaScript das jsDelivr CDN zulassen, müssen wir den meta-Tag wie in folgt formulieren:

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self';
  script-src 'nonce-rAnd0m' https://cdn.jsdelivr.net; />

Auf der offiziellen Webseite für Content Security Policy [4] finden Sie eine vollständige Auflistung sämtlicher Attribute und eine umfangreiche Liste an Beispielen. So bewirkt der Ausdruck nonce-rAnd0m dass Inline-Skripte geladen werden dürfen. Dazu muss das zugehörige JavaScript mit dem Attribut [script nonce=”rAnd0m”/] gekennzeichnet werden. Damit unser Beispiel aus Listing 2 vollständig funktioniert, benötigen wir für den CSP-meta-Tag folgenden Eintrag (Listing 3).

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self';
  script-src 'nonce-rAnd0m' https://cdn.jsdelivr.net;
  img-src https://images.unsplash.com;
  style-src 'self' https://cdn.jsdelivr.net;
  frame-src https://www.youtube.com;
  font-src https://fonts.googleapis.com;" />

Für Java-Webapplikationen können die Headerinformationen zu den Beschränkungen der CSP über den response hinzugefügt werden. Die Zeile response.addHeader (“Content-Security-Policy”, “default-src ‘self'”); bewirkt wie zu Beginn, dass sämtliche Inhalte, die nicht von der eigene Domäne stammen, blockiert werden.

MEHR PERFORMANCE GEFÄLLIG?

Performance & Security-Track entdecken

 

Zentrale Regelung über Servlet-Filter

Nun ist das applikationsweite manuelle Hinzufügen von CSP-Regeln für jede eigene Seite recht mühselig und zudem noch sehr fehleranfällig. Eine zentrale Lösung für den Einsatz von Java-Servern ist die Verwendung von Servlet-Filtern (Listing 4).

public class CSPFilter implements Filter {

  public static final String POLICY = "default-src 'self'";

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  throws IOException, ServletException {
    if (response instanceof HttpServletResponse) {
      ((HttpServletResponse)response).setHeader("Content-Security-Policy", CSPFilter.POLICY);
    }
  }

  @Override
  public void init(FilterConfig filterConfig) throws ServletException { }

  @Override
  public void destroy() { }
}

Um den Filter dann zum Beispiel im Apache Tomcat zu aktivieren, wird noch ein kleiner Eintrag in der web.xml benötigt (Listing 5).

<filter>
  <filter-name>CSPFilter</filter-name>
  <filter-class>com.content-security-policy.filters.CSPFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>CSPFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

Sicher könnte man für sämtliche Applikationen in einem vorgeschalteten Proxyserver wie dem Apache-2-HTTP-Server oder NGNIX alle CSP-Regeln zentral verwalten. Davon ist allerdings aus zwei Gründen abzuraten: Zum einen verlagert sich so die Verantwortlichkeit von der Entwicklung hin zum Betrieb, was zu erhöhtem Kommunikationsaufwand und einem Flaschenhals in der Entwicklung führt. Zum anderen werden die so entstehenden Regeln sehr komplex und entsprechend schwieriger zu lesen beziehungsweise zu warten. Der hier vorgeschlagene Servlet-Filter ist daher eine gute Option, um die Sicherheitsregeln applikationsweit zentral für exakt die entwickelte Anwendung zu verwalten. Zudem passt das auch hervorragend in ein agiles DevOps-Konzept, denn die notwendige Konfiguration ist Bestandteil des Source Codes und über die Versionsverwaltung unter Konfigurationsmanagement zugänglich.

 

Fazit

Wie ich in diesem Artikel zeigen konnte, ist das Thema Web-Application-Security nicht immer gleich ein Fall für Spezialisten. Persönlich empfinde ich es als eine große Errungenschaft der letzten Jahre, wie das Härten von Webanwendungen kontinuierlich einfacher wird. CSP halte ich auf diesem Weg für einen Schritt in die richtige Richtung. Natürlich gilt es weiterhin, stets ein Auge auf aktuelle Entwicklungen zu haben, denn die bösen Buben und Mädels ruhen sich nicht auf ihren Lorbeeren aus und lassen sich ständig neue Gemeinheiten einfallen. Der gerade populär gewordene breite Einsatz von künstlichen neuronalen Netzen wie ChatGPT lässt im Moment nur grob erahnen, was uns künftig noch an Cyberangriffen aus den Tiefen des weltweiten Netzes erwarten wird.


Links & Literatur

[1] https://owasp.org/www-community/attacks/xss/

[2] https://www.w3.org/TR/CSP/

[3] https://de.wikipedia.org/wiki/Content_Delivery_Network

[4] https://content-security-policy.com

The post Sichere Web-Apps für alle appeared first on JAX.

]]>
Quarkus: Next Level Jakarta EE https://jax.de/blog/quarkus-next-level-jakarta-ee/ Mon, 11 Dec 2023 12:13:34 +0000 https://jax.de/?p=89304 Jakarta EE (aka Java EE) ist eine weit verbreitete Basis für Java-Enterprise-Anwendungen. Für viele ist die Plattform fest mit dem Begriff „Application Server“ verbunden, einem vermeintlich schwergewichtigen Ding, auf dem große Anwendungen laufen. Das scheint so gar nicht in die aktuelle Zeit zu passen, in der statt weniger Großanwendungen nun viele kleinere betrieben werden und separate Serverinstallationen eher unerwünscht sind. Das bedeutet aber mitnichten das Ende von Jakarta EE.

The post Quarkus: Next Level Jakarta EE appeared first on JAX.

]]>
Vielfach hört man die Meinung, dass moderne Anwendungen oder Microservices mit Jakarta EE nicht machbar seien und man daher seine Codebasis auf Spring Boot migrieren müsse. Wir werden im Folgenden sehen, dass das so nicht stimmt. Um Missverständnissen vorzubeugen: Es geht nicht darum, ob Spring Boot oder Jakarta EE das beste Enterprise-Framework ist – so etwas gibt es ohnehin nicht. Es geht vielmehr darum zu zeigen, dass man bei größtenteils unveränderter Codebasis moderne, schnelle und kleine (oder auch große) Anwendungen erstellen kann und dass das sogar richtig Spaß macht – pardon: eine gute „Developer Experience“ hat.

Programmiermodell

Werfen wir zunächst einen Blick darauf, wie wir Anwendungen mit Jakarta EE schreiben. Im Listing 1 ist ein beispielhafter Ausschnitt einer Web-Service-Klasse zu sehen. Wir arbeiten in JEE stark deklarativ, das heißt wir geben dem Code mit Annotationen wie @ApplicationScoped Eigenschaften mit, statt sie selbst zu implementieren. Die Verknüpfung der verschiedenen Komponenten untereinander programmieren wir ebenso nicht aus, sondern lassen benötigte Services per Dependency Injection mit @Inject anliefern.

Möglich wird diese Vorgehensweise dadurch, dass in einer JEE-Plattform standardisierte Subsysteme zur Verfügung stehen, die die jeweilige technische Implementierung enthalten und dabei die angeführten Annotationen als Steuerparameter konsumieren. So übernimmt z. B. RESTEasy als eine der sogenannten kompatiblen Implementierungen für RESTful Web Services die Aufgabe, Fachdaten wie Person-Objekte in HTTP-Requests zu packen und dabei die Annotationen @Path, @GET und @Produces für die Konfiguration von Zugriffspfaden, Methoden und Serialisierungsformen zu berücksichtigen. Für uns Entwickelnde bedeutet das, dass wir uns im Wesentlichen auf unsere Fachlogik konzentrieren können und uns die notwendige Technik ohne großen Aufwand zur Verfügung gestellt wird.

@ApplicationScoped
@Path("persons")
public class PersonResource {

  @Inject
  PersonRepository personRepository;

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public List<Person> get() {
    return this.personRepository.findAll();

Stay tuned

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

 

Das klassische Laufzeitmodell

Davon unabhängig ist die Art und Weise, wie Anwendungen betrieben werden. Die klassische Weise nutzt einen Application Server wie WildFly, Open Liberty oder Payara. Die Anwendung wird von unserem Build-Tool – z. B. Maven – in ein Deploy-File paketiert, heute meist ein .war-File. Da es nur die Anwendung, einige wenige Konfigurationsdateien und eventuelle Third-Party Libraries enthält, ist das .war-File vergleichsweise klein. Es wird dann in den Server geladen – deployt –, der die Implementierung der Standardsysteme enthält und dadurch deutlich größer ist.

Diese Vorgehensweise wird heutzutage als schwergewichtig empfunden, was aber kaum an der Größe oder der Startgeschwindigkeit des Servers liegt – einige Hundert MB Installationsgröße schrecken uns kaum ab und die oben erwähnten Server starten alle in Sekunden. Es liegt vielmehr daran, dass ein Server zunächst installiert und konfiguriert werden muss, bevor man mit der Anwendung um die Ecke kommen kann. Es kommt erschwerend dazu, dass wir die Möglichkeit, mehrere Anwendungen auf einem Server zu deployen, meist aus organisatorischen Gründen gar nicht nutzen. Vielmehr betreiben wir häufig nur eine Anwendung pro Server, um so die Abhängigkeiten untereinander und zu Server- und Java-Versionen zu minimieren. Ein Server für jede Anwendung ist dann schon ein „schweres Gewicht“.

Viele setzen nun Jakarta EE mit Application Server gleich. Da letzterer schwergewichtig ist, ist Jakarta EE eben auch schwergewichtig. Das wäre mathematisch korrekt, wenn die Einstiegsannahme stimmen würde. Das ist aber nicht so: Wie wir im Folgenden sehen werden, können wir JEE-Anwendungen auch ohne Application Server betreiben.

Migration auf Quarkus

Wenn wir schon nur eine Anwendung pro Server haben, können wir das Ganze auch zu einer kompakten Einheit verschmelzen. Das Modell, den Server in die Anwendung zu integrieren, ist durch Spring Boot im großen Stil bekannt geworden. In seinem Schatten haben sich auch im JEE-Bereich ähnlich ausgerichtete Frameworks entwickelt, die aber nie annähernd die gleiche Popularität erlangt haben. Das ändert sich gerade mit Quarkus. Das Open-Source-Framework hat ähnliche Features wie Spring Boot, aber mit nahezu unveränderten JEE-Programmquellen. Zudem werden durch diverse Optimierungen Ressourcenverbrauch und Startzeiten erheblich reduziert und der Komfort während der Entwicklung deutlich verbessert.

Aber dazu später mehr – schauen wir uns zunächst an, welche Änderungen an einer bestehenden Jakarta-EE-Anwendung zu machen sind, um sie auf Quarkus zu migrieren.

In [1] finden Sie eine Anwendung in zwei Varianten: Im Verzeichnis std-jee liegt der Quellcode einer Standard-Jakarta-EE-Anwendung, die einen zwar kleinen, aber kompletten Stack aus RESTful Web Service, Injection-Container und Jakarta Persistence enthält. Als kleines Schmankerl ist sogar ein mittels Jakarta Faces gebautes Web-UI enthalten – sicher heute kein Mainstream, aber in vielen Altanwendungen durchaus enthalten. Details zur Anwendung können Sie aus der Projektdatei README.adoc entnehmen.

Im Verzeichnis quarkus finden Sie die gleiche Anwendung auf Basis von Quarkus. Schauen wir mal, welche Änderungen für die Migration von std-jee zu quarkus nötig waren. Die größte Änderung ergibt sich in der Projektdefinition für Maven: In der pom.xml wird jetzt statt des .war-Files ein .jar-File gebaut und wir fordern einzelne sogenannte Extensions für die zu nutzenden Basisdienste an – hier REST- und Injection-Container, Hibernate und PostgreSQL-Treiber (Listing 2).

<dependencies>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-reactive-jsonb</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-arc</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
  </dependency>

Weiterhin wird das quarkus-maven-plugin in den Build-Zyklus eingebaut. Es übernimmt die Aufgabe, die gebaute Anwendung in target/quarkus-app mit den notwendigen Bibliotheken zu sammeln und darüber hinaus einige Optimierungen zur Build-Zeit durchzuführen – dazu später mehr. Natürlich müssen Sie die pom.xml nicht selbst schreiben. Für den Bootstrap von Projekten gibt es Kommandozeilentools oder Erweiterungen Ihrer Lieblings-IDE. Details finden Sie unter [2]. Möchten Sie sich lieber das Projekt auf einer Webseite zusammenklicken? Dann schauen Sie mal auf [3].

Konzeptionell anders ist auch die Konfiguration der Anwendung. Bei einer klassischen JEE-Anwendung werden Logging-Einstellungen, DB-Verbindungen etc. auf dem Server konfiguriert. Das geschieht nun in der Projektdatei application.properties (Listing 3).

quarkus.log.level=WARN
quarkus.log.category."io.quarkus".level=INFO
quarkus.log.category."de.gedoplan.showcase".level=DEBUG

%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${db.host:localhost}:5432/showcase
%prod.quarkus.datasource.username=showcase
%prod.quarkus.datasource.password=showcase

Das Präfix %prod. deutet auf ein sogenanntes Configuration Profile hin. Die so benannten Propertys gelten nur im Produktionmodus, das heißt wenn die Anwendung ganz normal gestartet wird. Welche anderen Configuration Profiles es gibt und warum dann auch eine DB-Verbindung verfügbar ist, sage ich Ihnen später. Sie finden .properties-Dateien doof und möchten lieber .yaml einsetzen? Geht auch!

Die Java-Klassen der Anwendung bleiben im Wesentlichen unverändert. Es sind allerdings ein paar Vereinfachungen möglich und sinnvoll, die später beschreibe.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Bau und Start

Mit mvn package wird die Anwendung gebaut. Das oben erwähnte Plug-in erzeugt das Verzeichnis target/quarkus-app, in dem ein .jar-File zum Start und diverse Unterverzeichnisse mit den Anwendungsklassen und Bibliotheken liegen.

Durch java -jar target/quarkus-app/quarkus-run.jar wird die Anwendung gestartet (Listing 4). Wenn Sie das ausprobieren wollen, müssen Sie die PostgreSQL-DB zuvor gestartet haben.

java -jar target/quarkus-app/quarkus-run.jar
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2023-10-27 14:07:57,406 INFO  [io.quarkus] (main) quarkus 1.0-SNAPSHOT on JVM (powered by Quarkus 3.4.1) started in 2.954s. Listening on: http://0.0.0.0:8080
2023-10-27 14:07:57,416 INFO  [io.quarkus] (main) Profile prod activated.
2023-10-27 14:07:57,417 INFO  [io.quarkus] (main) Installed features: [agroal, awt, cdi, hibernate-orm, itext, jdbc-postgresql, myfaces, narayana-jta, poi, primefaces, resteasy-reactive, resteasy-reactive-jsonb, servlet, smallrye-context-propagation, vertx, websockets, websockets-client] 

Vereinfachungen

Wenngleich der Code nahezu unverändert übernommen werden kann, gibt es doch ein paar Dinge, die gegenüber dem Standard kürzer oder einfacher sind. Für Jakarta Persistence (aka JPA) ist es zwar möglich, weiterhin einen Deployment Descriptor zu verwenden. Die Definition von Parametern für die DB-Verbindung und anderer Features des Persistenzproviders ist allerdings mit den oben gezeigten Properties viel kürzer, sodass Quarkus-Anwendungen i. A. keine persistence.xml mehr enthalten. Weiterhin enthält die Persistence Extension von Quarkus bereits einen Producer für einen Standard-Entity-Manager, sodass die Klasse EntityManagerProducer aus der Standardanwendung in der Quarkus-Anwendung entfällt.

Der Injection-Container ArC ist eine Implementierung von CDI Lite. Gegenüber CDI Full, das auf Standard-JEE-Servern angeboten wird, gibt es Einschränkungen, aber auch Erweiterungen [4]. So gibt es beispielsweise Erleichterungen für die Konstruktoren einer CDI Bean. Wenn Sie, wie viele unserer Mitstreitenden, statt der im Listing 1 genutzten Field Injection mit einem Konstruktor arbeiten wollen, müssen Sie im Standard

  • einen passenden Konstruktor mit @Inject annotieren
  • und einen Konstruktor ohne Parameter mindestens in protected-Sichtbarkeit ergänzen.

Für Quarkus dürfen Sie den No-argument Constructor weglassen. Wenn dann der andere Konstruktor der einzige ist, kann auch @Inject darauf entfallen (Listing 5).

@ApplicationScoped
@Path("persons")
public class PersonResource {

  private PersonRepository personRepository;

  // not necessary for Quarkus
  // @Inject
  public PersonResource(PersonRepository personRepository) {
    this.personRepository = personRepository;
  }

  // not necessary for Quarkus
  // protected PersonResource() {
  // }

Die REST-Klassen bleiben komplett unverändert. Eine Besonderheit stellt die sogenannte Application Class dar – im Beispielprojekt RestApplication. Sie dient häufig nur dazu, den Einstiegspfad für das REST API mit Hilfe der Annotation @ApplicationPath festzulegen. In Quarkus-Anwendungen kann die Klasse entfallen, wenn dieser Pfad / ist.

Hat die zu migrierende Anwendung Webressourcen wie .html-Files, .css-Files etc. im Quellordner src/main/webapp, bleiben diese wiederum unverändert, müssen aber an anderer Stelle abgelegt werden, da es den erwähnten Ordner nur für .war-Files gibt. Der Deployment Descriptor web.xml – soweit überhaupt vorhanden – gehört nun in src/main/resources/META-INF. Der restliche Inhalt von src/main/webapp kommt in src/main/resources/META-INF/resources.

Für die Nutzung von Jakarta Faces gibt es eine Quarkus Extension im Apache-Projekt myfaces. Und im sogenannten Quarkiverse, einer Sammlung von weiteren Quarkus Extensions, gibt es eine Extension für Primefaces. Beide werden im Beispielprojekt verwendet, sollen hier aber nicht weiter erläutert werden, um den Rahmen des Artikels nicht zu sprengen – vielleicht nur so viel: Auch das Faces-UI der Standardanwendung konnte im Wesentlichen unverändert für Quarkus übernommen werden.

 

Next Level

Das bisher Gezeigte ist schon mal sehr schön: Jakarta-EE-Anwendungen können also mit weitgehend unverändertem Code nach Quarkus migriert werden, was den JEE-Kundigen das weite Feld der Microservices und Cloud-Anwendungen eröffnet, ohne substanziell umlernen zu müssen. Dieser Punkt kann eigentlich nicht deutlich genug gesagt werden, bedeutet er doch nicht weniger, als dass wir unsere umfangreichen Kenntnisse und Erfahrungen mit Jakarta EE weiter nutzen können.

Aber wie war das denn nun mit dem Komfort in der Softwareentwicklung, der „Developer Experience“? Da bietet Quarkus uns tatsächlich einiges! Ich picke mir mal drei Dinge heraus:

Developer Mode

Quarkus bietet einen sogenannten Developer Mode, der mit mvn quarkus:dev gestartet wird. In diesem Modus überwacht die laufende Anwendung die Programmquellen. Sollten sich zum Zeitpunkt eines externen Requests (z. B. REST Call) Änderungen ergeben haben, erfolgt ein Hot Reload der Anwendung. Dadurch kann bei laufender Anwendung weiterentwickelt werden. Im Vergleich zum häufigen Redeployment einer klassischen Serveranwendung ist das wirklich eine große Erleichterung – wobei zur Ehrenrettung der Application Server gesagt werden muss, dass diese mittlerweile vergleichbare Modi anbieten.

Dev Services

Ist Ihnen aufgefallen, dass es im Developer Mode für unsere Beispielanwendung keine DB-Verbindungsparameter gibt? Die sind ja nur im Configuration Profile prod eingetragen, während der Dev Mode das Profil dev nutzt. Wieso funktioniert die Anwendung trotzdem und welche DB nutzt sie? Des Rätsels Lösung ist ein sogenannter Dev Service: Sind im Dev Mode keine Verbindungsparameter konfiguriert, wird eine passende Instanz als Container gestartet. Sie kennen dieses Verfahren vermutlich als Testcontainers aus dem Testumfeld. Quarkus bietet Dev Services für die meisten Datenbank-Extensions an, aber auch für Message Broker, Identity Manager etc. Durch Dev Services wird die Einstiegshürde für die Entwicklung von Anwendungsteilen mit Zugriff auf externe Subsysteme deutlich verringert: Ich muss nicht zuerst eine lokale Installation von PostgreSQL, Kafka oder Keycloak vornehmen, sondern kann direkt losentwickeln.

Test-Support

Werfen wir schließlich einen Blick auf (Integrations-)Tests. Die sind im Standardumfeld nicht gerade einfach zu schreiben, muss doch der Lifecycle der zu testenden Anwendung gesteuert werden: Server starten, Anwendung deployen, Tests laufen lassen, Anwendung undeployen, Server stoppen. Tools wie Arquillian [5] können das, sind aber durch die Vielfalt der Serverprodukte und Testverfahren vergleichsweise komplex. Man könnte auch sagen: So richtig Spaß macht’s nicht.

Für Quarkus-Anwendungen kann man stattdessen einfache JUnit-Testklassen schreiben und sie mit @QuarkusTest annotieren, wodurch aus dem Unit-Test ein Integrationstest wird. Die zu testende Anwendung erhält ihre Konfiguration aus dem Profil test mit eigenen Ports, Dev Services etc. Wenn man z. B. einen REST Endpoint testen möchte, ist das mit dem als Extension verfügbaren REST Assured möglich (Listing 6), das bereits auf die im Test genutzte Portnummer autokonfiguriert ist. Andere Clientbibliotheken sind aber natürlich auch verwendbar. Die Testklasse darf auch Injektionsziele enthalten, so dass neben Blackbox- auch Whitebox-Tests möglich sind (Listing 7).

@QuarkusTest
public class PersonResourceTest {

  @Test
  void testGetAverageAge() {
    given()
      .when().get("/api/persons/avgAge")
      .then()
      .statusCode(200)
      .body(is(Double.toString(PersonRepositoryMock.getAvgAge())));
  }
}
@QuarkusTest
public class PersonServiceTest {

  @Inject
  PersonService personService;

  @Test
  void testGetAverageAge() {
    assertEquals(PersonRepositoryMock.getAvgAge(), this.personService.getAverageAge(), 0.1);
  }
}

Ein besonderes Goodie stellt aus meiner Sicht das sogenannte Continuous Testing dar, das im Developer Mode aktiviert werden kann. Dann laufen während der Entwicklung stets die Tests, die durch die aktuellen Codeänderungen berührt sind. Ein Blick auf die Shell, in der die Anwendung läuft, offenbart also ständig: „grün: alles OK“ und „rot: ups!“. Schneller geht ein Testfeedback kaum.

Augmentation

Eines bin ich Ihnen noch schuldig: Es war die Rede von Optimierung zur Build-Zeit – worum handelt es sich dabei? Wenn eine klassische JEE-Anwendung deployt wird, läuft zunächst eine Initialisierungsphase, in der insbesondere der CDI-Container die Anwendung nach Beans, Scopes, Producern, Observern u. Ä. scannt, Proxies generiert, Zuordnungen zu Injection Points vornimmt etc. Das ist durchaus zeitaufwendig – und es basiert im Wesentlichen auf Informationen, die schon zur Entwicklungszeit bekannt sind. Das Maven-Plug-in von Quarkus führt diese Aufgabe bereits zur Build-Zeit durch. Die Anwendung wird um Code angereichert (daher der Name Augmentation), der zur Laufzeit anstelle der oben angesprochenen Initialisierung ausgeführt wird. Im Ergebnis startet die Anwendung deutlich schneller.

Stay tuned

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

 

Fazit und Ausblick

Mit Quarkus steht uns ein Serverframework zur Verfügung, das es ähnlich wie Spring Boot ermöglicht, Serveranwendungen aus Bausteinen aufzubauen. Bestehender Jakarta-EE-Code – und wichtiger vielleicht sogar, vorhandenes JEE-Wissen – kann weiterverwendet werden. Der Komfort bei der Softwareentwicklung steigt durch das angebotene Tooling. Alles ist leichtgewichtiger, geht schnell von der Hand, macht (wieder) Spaß.

Ich konnte in diesem Artikel Quarkus natürlich nicht erschöpfend behandeln. Es gibt mittlerweile ein großes, ständig wachsendes Ökosystem und Quarkus-Anwendungen lassen sich unter bestimmten Umständen auch nativ mit Startzeiten im Millisekunden-Bereich ausführen. Genug Futter für weitere Artikel also – stay tuned!


Links & Literatur

[1] https://github.com/GEDOPLAN/next-lvl-jee

[2] https://quarkus.io/guides/tooling

[3] https://code.quarkus.io

[4] https://quarkus.io/guides/cdi-reference

[5] http://arquillian.org

[6] https://gedoplan.de

The post Quarkus: Next Level Jakarta EE appeared first on JAX.

]]>
Java 21 und die Helden von heute https://jax.de/blog/java21-und-die-helden-von-heute/ Wed, 15 Nov 2023 09:43:45 +0000 https://jax.de/?p=89073 In Java 21 sind viele neue Features enthalten. Doch neben den hippen, großartigen Features für Parallelität und funktionaler Programmierung gibt es auch einige eher unscheinbare, die einen großen Einfluss haben werden: Wir nennen sie Hidden Heroes.

The post Java 21 und die Helden von heute appeared first on JAX.

]]>
In diesem Artikel werden „Generational ZGC“ (JEP 439), „Key Encapsulation Mechanism API“ (JEP 452) und „Code Snippets in Java Doc“ (JEP 413) vorgestellt. Beim ersten JEP handelt es sich um eine kleine Anpassung an den neuem Z Garbage Collector, der einen riesigen Performance Boost auf jeder Größe von Heap verspricht. Beim zweiten geht es um ein neues API mit dem Schlüssel für eine besonders sichere symmetrische Verschlüsselung, die mit Public/Private-Key-Methoden übertragen werden kann, und im letzten um die Einbindung von Codebeispielen in der Java-API-Dokumentation.

JEP 439: Generational ZGC

Eine Instanz auf dem Heap kann entfernt werden, wenn sie nicht mehr benötigt wird. In hardwarenahen Sprachen sind die Entwicklerinnen und Entwickler dafür zuständig, den korrekten, sicheren Zeitpunkt zu bestimmen. Der sichere Zeitpunkt ist der, wenn keine andere Instanz mehr eine Referenz auf die Instanz gespeichert hat. Durch Call-by-Reference-Aufrufe von Methoden können sich diese Referenzen sehr weit im System verbreiten und die Analyse wird komplex. Die manuelle Bestimmung des Zeitpunkts der sicheren Entfernung ist sehr fehleranfällig und deswegen setzt die JVM, wie viele moderne Ökosysteme, auf das Konzept des Garbage Collector, kurz GC. Ein Garbage Collector überwacht dabei automatisiert den Speicher und entfernt Instanzen nach dem sicheren Zeitpunkt.

Stay tuned

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

 

Hidden Heroes

Hidden Heroes leitet der Autor von dem Begriff „Hidden Champions“ ab, der von Hermann Simon geprägt wurde. Hidden Champions bezeichnen kleine Firmen, die Weltmarktführer sind und wenig Wahrnehmung in der Öffentlichkeit haben. Analog dazu sind Hidden Heroes Java-Features, die große Auswirkungen auf das JDK-Environment haben, aber gefühlt zu wenig Aufmerksamkeit in der Community bekommen.

In der Java Virtual Machine gibt es seit längerem mehrere verschieden implementierte Garbage Collectors mit diversen Schwerpunkten. Sie alle haben die GC-Pause gemein, in der die Anwendung pausiert werden muss:

  • Serial GC ist der einfachste GC. Er benutzt nur einen Thread und pausiert die Applikation, während er läuft. Er ist für Einzelkernsysteme und nicht für Hardware mit mehreren Kernen geeignet, da kein Nutzen aus mehreren Rechenkernen gezogen werden kann.
  • Parallel GC[1] verhält sich wie Serial GC, verwendet aber mehrere Kerne und ist damit eine bessere Alternative für Anwendungen auf Mehrkernhardware. Wie auch bei Serial GC ist die Länge der GC-Pause primär abhängig von der Größe des Heap-Speichers.
  • G1 GC heißt eigentlich Garbage First GC [2] und wendet Partitionen auf dem Heap an. Die Partitionen werden entsprechend dem freien Speicher aufsteigend priorisiert und analysiert. G1 GC analysiert und entfernt Instanzen, bis die konfigurierte fixe Länge der GC-Pause verstrichen ist. Es werden oft nicht alle Partitionen komplett bearbeitet, denn das Ziel von G1 ist eine möglichst feste Pausenzeit. Zudem eignet er sich gut für Maschinen mit vielen Prozessoren und großen Heaps.

Relativ neu ist der Z Garbage Collector [3], der in Java 15 eingeführt wurde, kurz ZGC genannt. Der ZGC führt die arbeitsintensive Analyse des Heap parallel zur Ausführung der Anwendung in eigenen Threads durch. Dadurch muss die Anwendung nur sehr kurz unterbrochen werden, um die Threads zu synchronisieren. Die GC-Pause ist dabei 1 ms lang und unabhängig von der analysierten Heapgröße. Realisiert wird die Parallelität mit Hilfe von Colored Pointer und Load Barriers. Colored Pointer sind eine Dekoration eines Pointers, einer Speicheradresse mit Metainformationen über den Status der Instanz. Load Barriers werden für Zugriffe auf Referenzen eingebaut. Sie werten Colored Pointer aus und führen potenziell notwendige Weiterleitungen durch, bevor die Anwendung auf die Instanz zugreift, um Adressänderungen zu verschleiern. Der hochoptimierte Algorithmus von ZGC wird im Artikel „Deep-dive of ZGC’s Architecture“ [4] auf Dev.java ausführlich beschrieben und ist in der Lage, sehr kleine und sehr große Heaps bis 16 TB effizient zu bearbeiten. Aktiviert werden kann ZGC durch den Kommandozeilenparameter -XX:+UseZGC. Bei der ersten Verwendung von ZGC wird empfohlen, auch GC-Logging (-Xlog:gc) zu aktivieren, um das Finetuning der Konfiguration zu ermöglichen. Neben der Anzahl der von ZGC verwendeten Threads (-XX:ConcGCThreads) und einigen Linux-spezifischen Parametern kann auch das Zurückgeben von Speicher an das Betriebssystem aktiviert werden (-XX:+ZUncommit).

Mit JEP 439: Generational ZGC [5] wird ZGC um die Partitionierung des Heap in Generationen, also je einen Bereich für „jung“ und „alt“, erweitert. Der Bereich für die jungen Instanzen unterteilt sich noch in „Eden“ und „Survivor“. Neu erzeugte Instanzen werden in der Regel in Eden erzeugt und, wenn sie den ersten Durchlauf des GC „überleben“, nach Survivor kopiert. Nachdem sie eine feste Anzahl GC-Läufe in Survivor „überlebt“ haben, werden Instanzen in den Bereich für alte Instanzen kopiert. Die Partitionierung ermöglicht die Anwendung der Weak Generational Hypothesis [6], die im Kern aussagt: „Junge Instanzen haben die Tendenz, jung zu sterben“. Als Konsequenz kann unter Instanzen im Bereich „jung“ und besonders unter denen im Bereich Eden vermutet werden, dass sie nicht mehr referenziert werden und zu entfernen sind. Im Bereich „alt“ sind wahrscheinlich kaum zu entfernende Instanzen enthalten und diese werden nicht so häufig analysiert. Durch die getrennte Behandlung verringert sich der durchschnittliche Aufwand der Analyse und der GC-Prozess wird effizienter. Aktiviert werden kann diese Partitionierung mit dem Parameter -XX:+ZGenerational. Zukünftig soll ZGC nur noch den Ansatz mit Generationen verwenden, dann ist der Parameter nicht mehr notwendig.

Mit seiner effizienten Behandlung von riesigen Heaps, der geringen GC-Pause und dem Fokus auf das Entfernen von jungen Instanzen ist Genrational ZGC optimiert für datenintensive Anwendungen, die eine kurze Antwortzeit erfordern. Damit kann Generational ZGC eine gute Wahl für moderne datengetriebene Systeme im Enterprise-Umfeld sein. Durch die hohe Komplexität und viel notwendiger Theorie in diesem Themenfeld fehlt es Generational ZGC ein wenig an Aufmerksamkeit, deswegen ist Generational ZGC ein Hidden Hero des Java-Ökosystems.

Stay tuned

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

 

JEP 452: Key Encapsulation Mechanism API

In der elektronischen Kommunikation wird an vielen Stellen auf Verschlüsselung gesetzt. Viele der heute gängigen Verfahren gelten als nicht sicher für ein Zeitalter mit Quantencomputern. An einem solchen Quantencomputer könnte zum Beispiel Mallroy forschen – und vor kurzen hatte er einen großen Durchbruch. Alice und Bob wollen deswegen eine Überraschungsparty für ihren Freund Mallroy organisieren. Damit Mallroy unwissend bleibt, wollen Alice und Bob nur über sichere, verschlüsselte Kanäle miteinander kommunizieren. Sie wollen zahlreiche Nachrichten austauschen und Mallroy könnte schon den Quantencomputer zur Verfügung haben, deswegen kommt nur ein effizientes und hochsicheres Verfahren in Frage.

Bei ihrer Recherche erfahren sie, dass es symmetrische und asymmetrische Verfahren gibt. Bei einem asymmetrischen Verschlüsselungsverfahren werden zwei verschiedene Schlüssel zum Ver- und Entschlüsseln verwendet. Der Schlüssel zum Verschlüsseln kann gefahrenlos übermittelt werden, leider sind diese Algorithmen in der Regel nicht hochperformant und auch nicht besonders sicher. Bei symmetrischen Verfahren wird derselbe Schlüssel zum Ver- und Entschlüsseln verwendet, sie punkten bei Effizienz und Sicherheit ganz klar. Leider benötigen beide Seiten das Wissen über den verwendeten Schlüssel und dieser kann, da geheim, nicht trivial versendet werden.

Im 2001 von Cramer und Shoup veröffentlichten Artikel „Design and Analysis of Practical Public-Key Encryption Schemes Secure against Adaptive Chosen Ciphertext Attack“ [7] wird in § 7.1 der Mechanismus „Key Encapsulation“ beschrieben, mit dem es möglich ist, Schlüssel für symmetrische Verschlüsselungsverfahren unter Zuhilfenahme asymmetrischer Verschlüsselung sicher zu übertragen. Dieses unter der Abkürzung KEM bekannte Verfahren wird unter anderem vom BSI und der NIST in ihren Post-Quanten-Kryptografie-Konzepten als Basisbaustein betrachtet [8], [9]. In Abbildung 1 ist der KEM-Ablauf zwischen Alice und Bob dargestellt. Zuerst generiert Alice ein Schlüsselpaar aus öffentlichem und privatem Schlüssel. Der öffentliche Schlüssel wird an Bob übertragen. An dieser Stelle ist eine Transportverschlüsselung zwischen Alice und Bob wichtig, aber nicht Gegenstand des Verfahrens. Bob erzeugt einen zufälligen Schlüssel für das später verwendete symmetrische Verschlüsselungsverfahren und verschlüsselt diesen mit dem öffentlichen Schlüssel von Alice. Dieses als „Encupsulated“ bekannte Datenpaket wird an Alice zurückübertragen, und nur sie kann es mit ihrem privaten Schlüssel entschlüsseln. Von nun an können Alice und Bob ihre Nachrichten mit einer symmetrischen Verschlüsselung austauschen, das Schlüsselpaar wird nicht mehr gebraucht.

Abb. 1: Bob und Alice tauschen den Schlüssel für eine symmetrische Verschlüsselung (gelb) mit KEM aus

Da die Implementierung sicherheitsrelevanter Features und insbesondere von Verschlüsselungen ein hochkomplexes Feld ist, gleichzeitig aber die Verschlüsselung eine grundlegende Anforderung moderner Systeme ist, wird mit JEP 452 [10] ein API zur Durchführung eines KEM-Prozesses in Java 21 eingeführt. Das API folgt dem von der ISO 18033-2 definierten und oben beschriebenen Prozess. Dabei bilden drei Bausteine das Fundament für eine sichere Verschlüsselung. Der javax.crypto.KEM.Encapsulator verwendet den öffentlichen Schlüssel und den erzeugten symmetrischen Schlüssel, um ein Encapsulated zu erzeugen. Das Encapsulated wird durch eine Instance von javax.crypto.KEM.Encapsulated repräsentiert und kann direkt als Bytearray von Bob übertragen werden. Auf Alices Seite wird die Klasse javax.crypto.KEM.Decapsulator verwendet, um mit dem passenden privaten Schlüssel den symmetrischen Schlüssel zu erlangen.

Im nun folgenden Beispiel bilden die Methoden sendToBob/sendToAlice und retrieveFromBob/ retrieveFromAlice die jeweilig sendenden beziehungsweise empfangenden Enden der unsicheren Kommunikationskanäle zwischen Alice und Bob ab. Listing 1 zeigt, wie das Set-up des KEM-Prozesses bei Alice mit dem KeyPairGenerator implementiert wird. Die Methode KeyPairGenerator#getInstance(String) erzeugt einen Schlüsselpaarerzeuger für das RSA-Verfahren. Über ein Service Provider Interface ist es möglich, hier weitere Algorithmen zu hinterlegen. Mit dem Aufruf von KeyPairGenerator#generateKeyPair() wird ein Paar aus öffentlichem und privatem Schlüssel erzeugt. Dieses Paar wird bei Alice hinterlegt und der öffentliche Schlüssel an Bob versandt.

var keyGen = KeyPairGenerator.getInstance("RSA");
var pair = keyGen.generateKeyPair();

sendToBob(pair.getPublic());

In Listing 2 wird die erste Phase der Key Exchanges auf der Seite von Bob gezeigt. Nachdem der öffentliche Schlüssel von Alice empfangen wurde, wird mir KEM#getInstance(String) eine für RSA konfigurierte Implementierung gewählt. Neue Implementierungen können auch hier durch ein SPI bereitgestellt werden. Auf der konkreten KEM-RSA-Instanz wird mit der Methode KEM#newEncapsulator(java.security.PublicKey) eine Encapsulator-Instanz für RSA-KEM erzeugt. Der zufällig erzeugte Schlüssel kann und sollte über Encapsulated#key() abgerufen und bei Bob hinterlegt werden (hier in der Variable secret). Mit diesem Schlüssel wird die spätere symmetrische Verschlüsselung durchgeführt. Auf das zu versendende Encapsulate kann über die Methode Encapsulated#encapsulate() zugegriffen werden und es wird von Bob an Alice versendet.

var publicKey = retrieveFromAlice();

var encapsulator = KEM.getInstance("RSA-KEM").newEncapsulator(publicKey);
var secret = encapsulate.key();
var encapsulate = encapsulator.encapsulate();

sendToAlice(encapsulate.encapsulation());

In Listing 3 wird die zweite Phase der Key Encapsulation bei Alice gezeigt. Nachdem das Encapsulate von Bob empfangen und der private Schlüssel aus dem Schlüsselpaar des Set-ups entnommen wurde, kann mit dem Entpacken des symmetrischen Schlüssels begonnen werden. Zuerst wird wieder mit der Methode KEM#getInstance(String) die konkrete Implementierung von KEM für RSA-KEM geladen. Mit dem Aufruf KEM# newDecapsulator(PrivateKey) wird unter Zuhilfenahme des privaten Schlüssels der symmetrische Schlüssel entpackt.

byte[] encapsulate = retrieveFromBob();
var privateKey = pair.getPrivate();

var decapsulator = KEM.getInstance("RSA-KEM").newDecapsulator(privateKey);
var secret = decapsulator.decapsulate(encapsulate);

Von diesem Moment an ist das verwendete öffentliche und private Schlüsselpaar nicht mehr notwendig und kann gelöscht werden. Die Kommunikation kann nun sicher und effizient mit einem symmetrischen Verschlüsselungsverfahren durchgeführt werden. Mit diesem sehr neuen Feature in Java 21 rüstet die OpenJDK-Community das Ökosystem Java für das Post-Quantum-Zeitalter auf. Die Verwendung von Java in einer Welt voller Quantencomputer ist nun möglich und aus diesem Grund sollte dieses Feature mehr Aufmerksamkeit bekommen. Es ist auf jeden Fall ein Hidden Hero von Java 21.

JEP 413: Code Snippets in Java API Documentation

An Schnittstellen stellen Entwickler und Entwicklerinnen ganz eigene Anforderungen. Oft sind Benutzbarkeit und verständliche Dokumentation die wichtigsten Aspekte. Aus diesem Grund ist Javadoc als Dokumentationswerkzeug für Source Code bereits seit Java 1 Teil des JDK. Mit Javadoc können Kommentare mit beschreibenden Tags angereichert werden und in eine navigierbare und durchsuchbare Schnittstellendokumentation überführt werden. Neben Beschreibungen der Parameter und des Verhaltens ist es sinnvoll, die beabsichtigte Verwendung zu dokumentieren, um die Hürden der Einarbeitung zu verringern. Es haben sich einige unterschiedliche Ansätze etabliert:

  1. Separate Tutorials wie die Referenzdokumentation von Spring [11] oder Extensions Guides von Quarkus [12]. Hier liegt der Fokus eher auf der Verwendung des Frameworks und weniger auf dem API.
  2. Bei Code Snippets als HTML mit <pre>{@code } </pre> liegt das API im Fokus. Das Snippet wird leider nicht ansehnlich formatiert und muss bei jeder Änderung an der Schnittstelle bedacht werden. Ein Beispiel ist java.util.stream.Stream [13], [14].
  3. Unit Tests als Dokumentation zu betrachten, ist ein guter Ansatz, vorausgesetzt die Tests sind gut und die verwendenden Entwickler und Entwicklerinnen haben Zugriff auf den Code. Bedauerlicherweise fehlt bei diesem Ansatz oft die Verknüpfung zwischen Code und Tests.

Mit JEP 413: Code Snippets in der Java-API-Dokumentation [15] hält Java seit Version 18 eine Möglichkeit bereit, Source-Code-Auszüge mit Syntax-Highlighting und Testbarkeit zu vereinen, um Entwicklerinnen und Entwicklern das Einbinden von guten Beispielen in der API-Dokumentation zu ermöglichen. Um ein Snippet in einem Javadoc-Kommentar einzubinden, wird der neue {@snippet: …  }-Tag verwendet.

/**
 * Berechnung der MwSt für einen Privatkunden beim Einkauf in Höhe von 1055
 * {@snippet :
 *   var kunde = new Privatkunde("Merlin", "");
 *   var wert = 1055d;
 *   // ...
 *   var mwst = MwStRechner.PlainOOP.calculateMwSt(kunde, wert);
 * }
 */

In Listing 4 wird in einem Inline Snippet die erwartete Interaktion mit dem API des MwStRechner aus dem Data-Oriented-Programming-Beispielprojekt des Autors [16] dargestellt. In der generierten Dokumentation ist der Bereich ab der neuen Zeile hinter dem Doppelpunkt bis zur letzten Zeile vor der schließenden Klammer als formatierter Source Code mit Syntax-Highlighting enthalten. Bei Inline Snippets gibt es zwei Randbedingungen:

  • Ein mehrzeiliger Kommentar mit /* */ ist nicht erlaubt.
  • Für jede geöffnete Klammer muss auch eine schließende enthalten sein.

Ohne diese Randbedingungen ist es dem Generator nicht möglich, die Passage korrekt zu konvertieren. Zusätzlich muss die syntaktische Korrektheit manuell geprüft und Schnittstellenänderungen beachtet werden. Für ein externes Snippet gelten diese Rahmenbedingungen allesamt nicht. Bei einem externen Snippet wird der Inhalt nicht im selben Kommentar angegeben, sondern aus einer vorhandenen Java-Datei entnommen.

/**
 * Berechnung der MwSt für einen Privatkunden beim Einkauf in Höhe von 1055
 * {@snippet file="SwitchExpressionsSnippets.java" region="example"}
 */

// Datei: snippet-files/Snippets.java
class Snippets {
  public void snippet01() {
    void snippet01() {
    // @start region="example"
    // @replace region="replace" regex='(= new.*;)|(= [0-9]*d;)' replacement="= ..."
    // @highlight region="highlight" regex="\bMwStRechner\b"
    var kunde = new Privatkunde("Merlin", "[email protected]"); // @replace regex="var" replacement="Kunde"
    var wert = 1055d; // @replace regex="var" replacement="double"
    /* .. */
    var mwst = MwStRechner.PlainOOP.calculateMwSt(kunde, wert); // @link substring="PlainOOP.calculateMwSt" target="MwStRechner.PlainOOP#calculateMwSt"
    // @end @end @end
  }
}

In Listing 5 wird ein externes Snippet verwendet, das auf eine Datei Snippets.java (auch Listing 5) im Ordner snippet-files verweist. Dieser Ordner liegt direkt neben der Datei, in der das Snippet eingebunden wird und kann mit Hilfe der Konfiguration -snippet-path überschrieben werden. Den Pfad auf den Ordner mit dem Test zu konfigurieren, erscheint dem Autor als guter Standard. Dadurch ist es möglich, die geschriebenen Tests als Beispiele wiederzuverwenden. In Listing 2 ist zudem eine Region definiert. Dadurch werden nur bestimmte Bereiche der referenzierten Java-Datei verwendet und die Datei kann so Beispiele für verschiedene Anwendungsfälle enthalten.

Stay tuned

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

 

Neben Bereichen sind in dem Snippet in Listing 5 noch weitere Anpassungen durchgeführt worden. Mit den @replace Tags werden per regulärem Ausdruck zuerst alle Initialisierungen durch ersetzt, da sie nicht direkt zum Beispiel beitragen. Das Schlüsselwort var wird in den entsprechenden Zeilen durch den Datentyp ersetzt. Hierbei wird keine Region angeben, also die Ersetzung nur auf diese Zeile angewandt. Mit dem Tag @highlight wird jedes Vorkommen von MwStRechner hervorgehoben und mit @link ein Link zur Methode MwStRechner.PlainOOP#calculateMwSt erstellt. In Abbildung 2 wird das Ergebnis der Javadoc-Generierung des Listings 5 gezeigt.

Abb. 2: Aus Listing 5 generierte Dokumentation mit Ersetzungen, Links und Highlights

Durch die Möglichkeit zur Verknüpfung zwischen Source Code und API-Dokumentation kann gewährleistet werden, dass auch Listings in Javadoc aktuell und von hoher Qualität sind. Die Dokumentation von APIs gewinnt dadurch an Qualität, Aktualität und einem hohen Grad an Verständlichkeit. Werden alle Tags in einem Snippet verwendet, wird das Lesen des zugrundeliegenden Codes erschwert. Das betrifft vor allem die Entwicklerinnen und Entwickler von Frameworks und Tools. Von ihrem Mehraufwand werden aber alle profitieren, deswegen sind Code Snippets in Java API Documentation auf jeden Fall ein Hidden Hero im Java-21-Ökosystem.

Zusammenfassung

In diesem Artikel wurden drei der vielen Hidden Heroes im JDK-Ökosystem abseits von Virtual Threads und Pattern Matching gezeigt. Es gibt noch einige mehr, wie beispielsweise Class Data Sharing und den Simple Web Server, aber das ist Material für einen weiteren Beitrag auf Konferenzen, User Groups oder für das Selbststudium.


Links & Literatur

[1] https://docs.oracle.com/en/java/javase/20/gctuning/parallel-collector1.html

[2] https://docs.oracle.com/en/java/javase/20/gctuning/garbage-first-garbage-collector-tuning.html

[3] https://docs.oracle.com/en/java/javase/20/gctuning/z-garbage-collector.html

[4] https://dev.java/learn/jvm/tool/garbage-collection/zgc-deepdive/

[5] https://openjdk.org/jeps/439

[6] https://docs.oracle.com/en/java/javase/17/gctuning/garbage-collector-implementation.html

[7] Design and Analysis of Practical Public-Key Encryption Schemes Secure against Adaptive Chosen Ciphertext Attack, Crammer und Shoup 2001, https://eprint.iacr.org/2001/108.pdf

[8] https://www.bsi.bund.de/DE/Themen/Unternehmen-und-Organisationen/Informationen-und-Empfehlungen/Quantentechnologien-und-Post-Quanten-Kryptografie/Post-Quanten-Kryptografie/post-quanten-kryptografie_node.html

[9] https://csrc.nist.gov/News/2022/pqc-candidates-to-be-standardized-and-round-4

[10] https://openjdk.org/jeps/452

[11] https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

[12] https://quarkus.io/guides/resteasy-reactive

[13] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html

[14] https://github.com/openjdk/jdk/blob/4de3a6be9e60b9676f2199cd18eadb54a9d6e3fe/src/java.base/share/classes/java/util/stream/Stream.java#L52-L57

[15] https://openjdk.org/jeps/413

[16] https://github.com/MBoegers/DataOrientedJava

The post Java 21 und die Helden von heute appeared first on JAX.

]]>
Techniken asynchroner Web-APIs https://jax.de/blog/techniken-asynchroner-web-apis/ Fri, 06 Oct 2023 11:09:56 +0000 https://jax.de/?p=89033 Neulich musste ein Kunde ein HTTP-basiertes Web-API für seine Daten bereitstellen. Das Problem: Die angebotenen Daten waren nicht direkt vorhanden, sondern erst nach einiger Zeit. Das API musste also eine Antwort nachliefern. Aber wie kann diese Art eines API umgesetzt werden? Ohne die Alternativen zu kennen, ist der Kunde schlecht beraten. Grund genug, sich verschiedene Techniken anzuschauen und zwischen deren Trade-offs abzuwägen.

The post Techniken asynchroner Web-APIs appeared first on JAX.

]]>
Klassische HTTP-basierte-APIs sind häufig synchron. Auf eine Anfrage folgt prompt eine Antwort. Nicht immer sind Antworten, wie im Falle unseres Kunden, jedoch sofort verfügbar, z. B., weil noch externe Systeme angefragt werden müssen. Das API ist also asynchron. Doch wie kann ein HTTP API asynchron abgebildet werden, wenn das HTTP-Protokoll doch konzeptionell synchron ist?

Wenn die Daten schon da sind

Normalerweise passieren Daten, damit sie von einem System in ein anderes gelangen, immer eine Schnittstelle (API). Klassischerweise ist diese HTTP-basiert, wie auch in diesem Fall. Das bedeutet, dass, um Daten anzufragen, ein HTTP Request gesendet wird, wie in Abbildung 1 gezeigt.

Abb 1: Synchrones API

Das Web-API eines Systems übersetzt diese Anfrage z. B. in eine SQL-Abfrage, transformiert das Ergebnis in ein Serialisierungsformat, wie beispielsweise JSON – manchmal sogar HTML oder XML –, und sendet dem Anfragenden das Ergebnis. Diese Art von Prozess hat allerdings eine Implikation: Der gesamte Prozess blockiert, weil er synchron ist. Ist die konsumierende Applikation des API beispielsweise ein Browser, sieht der Nutzer für die gesamte Laufzeit eine Ladeanzeige.

Stay tuned

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

 

Erst wenn das API die Antwort geliefert hat, wird in der Applikation weiterverfahren. Sowohl Applikation als auch API müssen eine Verbindung offenhalten, bis die Daten übertragen wurden. Der Zustand des Prozesses, erkennbar durch die grünen Balken in der Spalte der Applikation, wird damit (kurzzeitig) über die TCP-Verbindung abgebildet. Solch eine Kommunikation des API ist glücklicherweise relativ trivial und kurzlebig. Diese Art der Kommunikation ist konzeptionell synchron und für die meisten Web-APIs ausreichend.

Die Daten warten noch

Komplexer ist es, wenn der API-Server nicht mit einer eigenen Datenhaltung kommuniziert, die Daten direkt bereithält, sondern ein komplexerer Prozess existiert, der eine Antwort erst viel später – asynchron – liefert. Im folgenden Beispiel also das Anlegen einer Bestellung. In diesem Falle ist es notwendig, für den API-Server ein API bereitzustellen, das diese Asynchronität unterstützt. Die wohl simpelste Möglichkeit, Daten eines asynchronen API bereitzustellen, ist die über die Methode Polling (Abb. 2).

Abb. 2: Polling: das wiederholte Anfragen des Zustands

Beim Polling führt eine Applikation im Beispiel ein Erstellen von Daten aus und erhält dabei den aktuellen Zustand, während auf einem asynchronen externen System (z. B. einer Message Queue) eine Anfrage gestellt wird. Asynchronität bedeutet in der Implementierung immer, dass es einen Zwischenzustand („Wartend“) gibt, der ein nicht vorhandenes Ergebnis abbilden kann. Nach dem Erstellen erhält der Konsument in der Regel deshalb einen Datensatz, der eben diesen Zustand enthält. Ob das Ergebnis im Zustand „Wartend“ ausreichend ist, kann nur der Konsument des API selbst entscheiden. In den meisten Fällen ist das nicht der Fall. Der Zustand der Kommunikation bleibt deshalb weitgehend dem Konsumenten des API überlassen, denn dieser muss entscheiden, ob alle Daten vorhanden sind oder ob weitere Aktualisierungsschritte eingeleitet werden müssen.

Genau für diese weiteren Aktualisierungsschritte muss der API-Server einen zweiten Endpunkt implementieren, der den aktuellen Stand einer Bestellung (Order) für den aktuellen Zustand abfragbar macht. Der Konsument muss also selbst einen Mechanismus implementieren, innerhalb dessen er wiederholt den API-Server anfragt, je nachdem, ob die Daten aktuell genug sind. Im Beispiel wird die Bestellung mit der ID 1 wiederholt angefragt.

DIE KUNST DER SOTWARE-ARCHITEKTUR

Architecture & Design-Track entdecken

 

Die Schwierigkeit bei einem solchen Modell ist das Finden eines geeigneten Intervalls. Es ergibt wenig Sinn, bei einem Prozess, der Tage in Anspruch nimmt, im Minutentakt eine Aktualisierung zu erfragen. Ein API-Server muss deshalb bei tendenziell vielen Clients viele „leere Nachrichten“ verarbeiten. Der Vorteil liegt aber klar auf der Hand: Es ist Aufgabe des Clients, die aktuellen Daten zu erhalten. Der API-Server ist im Kommunikationsprozess relativ zustandslos.

Verbindungen reduzieren durch Long Polling

Eine Alternative für das regelmäßige Polling mit Intervall ist das Long Polling (Abb. 3). Dabei wird nach einer Anfrage die technische Verbindung so lange offengehalten, bis das Ergebnis letztlich eintritt, ähnlich wie es von TCP-Verbindungen bekannt ist.

Abb. 3: Long Polling: Verbindungen reduzieren

Diese Technik impliziert, dass der API-Server sehr lange eine HTTP-Verbindung offenhalten muss. Das kostet Speicher und muss von der Infrastruktur und deren Konfiguration unterstützt werden. Sowohl der Konsument als auch der API-Server, der die Daten bereitstellt, müssen eine Verbindung offenhalten bzw. regelmäßig erneuern wie beim Polling. Auch bei dieser Technik muss der Konsument des API einen Zustand vorhalten, der ein erneutes Abfragen des API anspricht. Es wird also ein technisches Problem gelöst, nicht aber der konzeptionelle Umstand der Datenverwaltung.

Stay tuned

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

 

Verbindungen minimieren durch Webhooks

Da es sich um einen Anwendungsfall von zwei vertrauenswürdigen Applikationen – Backend genannt – handelte, ist noch eine weitere Art von API möglich: Webhooks (Abb. 4).

Die Idee von Webhooks ist es, einen Kommunikations-Overhead der zuvor genannten Techniken zu mitigieren und stattdessen den Umstand auszunutzen, dass sowohl der Konsument als auch der Anbieter des API selbst eine Schnittstelle anbieten kann.

Abb. 4: Webhooks

Der Konsument des API bietet bei diesem Ansatz selbst ein beliebiges Callback API an – im Beispiel erreichbar unter dem URL /orders-received. Genau diesen URL teilt der Konsument beim Erstellen der Anfrage mit. Im Bild also foo.bar/orders-received. Der API-Server selbst speichert diesen in einer beliebigen Datenhaltung ab und stellt die Anfrage an das asynchrone System. In diesem Falle die Message Queue.

Erhält der API-Server eine Antwort auf die initial gestellte Anfrage durch die Message-Queue, kann dieser durch die zuvor vorgehaltene Abbildung von erstelltem Objekt und Callback-URL das entsprechende API des Konsumenten aufrufen. Eins ist damit klar erkennbar: Im Gegensatz zum Polling ist es bei Webhooks die Aufgabe des Servers, die Daten an den Konsumenten zu liefern. Die Zuständigkeiten haben sich damit verändert.

Durch eine Kommunikation über Webhooks werden die Nachteile des Polling vermieden. Diese Technik ist aber nicht überall einsetzbar – schließlich kann nicht jeder Konsument eines API, z. B. Browser, auch selbst ein API anbieten. Diese Technik ist damit nur eingeschränkt nutzbar. Ist der Konsument ein Browser, ist ein solches API kaum abbildbar, da es keine mir bekannte Möglichkeit gibt, dass der Browser als zuverlässiger Server fungiert.

Natürlich hat diese Art der Kommunikation auch noch weitere Nachteile. Beispielsweise ist der Konsument gezwungen, ein Callback API anzubieten und wird damit vom Konsumenten eines API zum Anbieter eines API.

 

Ist der Konsument des API unzuverlässig, z. B., weil die Applikation regelmäßig abstürzt, hat der API-Server nun außerdem die Aufgabe, gewisse Fehlertoleranzen zu implementieren. Das kann zum Beispiel durch Retry-Mechanismen geschehen – Aufgaben, die beim Polling-Ansatz nicht existieren würden, da der Client dort reiner Konsument ist und es sich um zustandslose Beziehung handelt.

Fazit

Die meisten HTTP-basierten Schnittstellen sind Request-Response-basiert und damit relativ trivial umzusetzen. Auf eine Anfrage folgt eine Antwort. Schwieriger wird es, wenn Asynchronität ins Spiel kommt. Für die Asynchronität muss ein Mechanismus her. In verteilten Systemen wird häufig das erneute Anfragen von Daten, genannt Polling, angewendet, um Daten regelmäßig zu untersuchen. Durch das Long Polling kann diese Technik dahingehend verändert werden, dass ein ständiges Auf- und Abbauen von Verbindungen verhindern.

Bei einer Backend-zu-Backend-Kommunikation wird häufig auf Webhooks gesetzt. Webhooks machen einen asynchronen Prozess Event- statt aktualisierungsgetrieben. Dadurch werden unnötige Kommunikationswege vermieden. Das ist nicht komplett kostenlos: Der API-Konsument ist nun in der Pflicht, selbst ein API bereitzustellen, und der API-Anbieter muss den Zustand der Kommunikation – also „wer ist an welchen Daten interessiert?“ – übernehmen.

Eins ist allerdings klar: Bei beiden Ansätzen muss auch die implementierende Seite des Clients Hand anlegen, sei es in Form einer Aktualisierungsschleife oder in Form eines eigenen HTTP API.

The post Techniken asynchroner Web-APIs appeared first on JAX.

]]>
Neues in Java 21: JEP 430 und 431 https://jax.de/blog/neues-in-java-21-jep-430-und-431/ Wed, 04 Oct 2023 11:55:16 +0000 https://jax.de/?p=89017 Java 21 bringt viele Verbesserungen in Syntax und APIs. In diesem Beitrag stehen die beiden Neuerungen JEP 430: „String Templates (Preview)“ sowie JEP 431: „Sequenced Collections“ im Fokus. Durch diese Funktionalitäten wird Java komfortabler und angenehmer in der Handhabung und besser für die Zukunft gerüstet. String Templates adressieren das Aufbereiten textueller Informationen, die aus fixen und variablen Bestandteilen bestehen, und Sequenced Collections vereinfachen den Zugriff und die Verwaltung von Daten am Anfang und Ende einer Collection.

The post Neues in Java 21: JEP 430 und 431 appeared first on JAX.

]]>
Regelmäßig müssen Strings aus fixen und variablen Textbausteinen zusammengesetzt werden. Um das zu tun, gibt es verschiedene Varianten, angefangen bei der einfachen Konkatenation mit + bis hin zur formatierten Aufbereitung. Die mit Java 21 neu eingeführten String Templates (JEP 430) ergänzen die bisherigen Varianten um eine elegante Möglichkeit, Ausdrücke angeben zu können, die zur Laufzeit ausgewertet und passend in den String integriert werden. Allerdings sind die String Templates noch ein Preview-Feature und müssen beim Kompilieren und Ausführen explizit aktiviert werden.

Bisherige Vorgehensweise

Schauen wir kurz zurück. Um formatierte Ausgaben in Java zu erzeugen, verwenden viele Entwickler die folgenden Varianten, um aus Variablen und Textbausteinen zu kombinieren:

String result = "Calculation: " + x + " plus " + y + " equals " + (x + y);
System.out.println(result);

und

String resultSB = new StringBuilder().append("Calculation: ").append(x).append(" plus ").append(y).append(" equals ").append(x + y).toString();
System.out.println(resultSB);

Die Stringkonkatenation mit + ist leicht verständlich und oft auch durchaus lesbar. Die Lesbarkeit nimmt jedoch bei einer zunehmenden Anzahl von Verknüpfungen ab. Noch dramatischer ist der Effekt beim Einsatz eines StringBuffers oder StringBuilders und dessen Methode append(): Je mehr Elemente zu verknüpfen sind, desto unleserlicher und schwieriger nachvollziehbar wird das Konstrukt.

Stay tuned

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

 

Weitere, in der Praxis seltener anzutreffende Möglichkeiten sind die Methoden format() und formatted() aus der Klasse String. Beide nutzen die gleichen Platzhalter, variieren aber ein wenig in der Handhabung, wobei meine Präferenz bei formatted() liegt.

var resultF1 = String.format("Calculation: %d plus %d equals %d", x, y, x + y);
System.out.println(resultF1);

var resultF2 = "Calculation: %d plus %d equals %d".formatted(x, y, x + y);
System.out.println(resultF2);

Darüber hinaus existiert noch die Klasse java.text.MessageFormat. Von ihr werden zunächst per Konstruktoraufruf mit fixem Text sowie integrierten Platzhaltern Instanzen erzeugt und danach mit format() befüllt – sowohl die Platzhalter als auch die Angabe der Werte unterscheiden sich von den beiden vorherigen Varianten:

var msgFormat = new MessageFormat("Calculation: {0} plus {1} equals {2}");
System.out.println(msgFormat.format(new Object[] { x, y, x + y }));

Möglichkeiten anderer Programmiersprachen

Viele Programmiersprachen unterstützten alternativ zur Stringkonkatenation die sogenannte Stringinterpolation oder auch formatierte Strings. Dabei kommt ein String zum Einsatz, in den an verschiedenen Positionen speziell gekennzeichnete Platzhalter integriert sind, die dann zur Laufzeit durch die entsprechenden Werte ersetzt werden. Das gilt beispielsweise für die f-Strings in Python. Etwas Ähnliches bieten Kotlin, Swift und C# durch unterschiedliche Notationen mit Platzhaltern:

  • Python: “Calculation: {x} + {y} = {x + y}”
  • Kotlin: “Calculation: $x + $y = ${x + y}”
  • Swift: “Calculation: \(x) + \(y) = \(x + y)”
  • C#: $”Calculation: {x} + {y}= {x + y}”

In allen Fällen werden die Platzhalter im String durch die entsprechenden Werte der Variablen, insbesondere auch von Berechnungen, ersetzt.

Die Alternative: String Templates (Preview)

Mit JEP 430 werden String Templates eingeführt, die wir uns nun anschauen wollen. Bilden wir das einführende Beispiel damit nach – aber bitte bedenken Sie, dass es sich um ein Preview-Feature handelt, das extra aktiviert werden muss, etwa wie folgt für die JShell:

$ jshell --enable-preview
|  Willkommen bei JShell - Version 21
|  Geben Sie für eine Einführung Folgendes ein: /help intro

Lernen wir zunächst die grundlegende Syntax kennen. Dabei muss dem String Template das Kürzel STR vorangestellt werden. Es aktiviert einen sogenannten Template Processor, der dann die Platzhalter im Format \{expression} ersetzt, im einfachsten Fall einfache Variablennamen durch deren Wert (Listing 1).

jshell> int x = 47
x ==> 47

jshell> int y = 11
y ==> 11

jshell> System.out.println(STR."Calculation: \{x} + \{y} = \{x + y}")
Calculation: 47 + 11 = 58

Bei der Angabe der Ausdrücke ist man nicht auf einfache mathematische Operationen beschränkt, sondern es lassen sich beliebige Java-Aufrufe einfügen, also auch Methoden (Listing 2).

jshell> int x = 7
x ==> 7

jshell> int y = 2
y ==> 2

jshell> STR."Berechnung: \{x} mal \{y} = \{Math.multiplyExact(x, y)}"
$3 ==> "Berechnung: 7 mal 2 = 14"

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Praxistipp

Bei der Einführung gab es diverse Diskussionen über das Format der Ausdrücke. Weil viele Third-Party-Bibliotheken etwa $, # oder {} dafür nutzen, hat man sich für ein Format entschieden, das nur innerhalb von String Templates gültig ist. Interessanterweise ist das STR per Definition public static final und automatisch in jeder Java-Datei bereits vorhanden.

Praktischerweise kann man String Templates auch in Kombination mit Textblöcken nutzen (Listing 3).

jshell> int statusCode = 201
statusCode ==> 201

jshell> var msg = "CREATED"
msg ==> "CREATED"

jshell> String json = STR."""
  ...>     {
  ...>       "statusCode": \{statusCode},
  ...>       "msg": "\{msg}"
  ...>     }""";
  ...> 

json ==> "{\n  \"statusCode\": 201,\n  \"msg\": \"CREATED\"\n}"

Tatsächlich gefällt mir in dem Zusammenhang die Methode formatted() ziemlich gut, und ich würde sie String Templates sogar vorziehen:

jshell> String json = STR."""
  ...> {
  ...>     "statusCode": %d,
  ...>     "msg": "%s"
  ...> }""".formatted(statusCode, msg);
json ==> "{\n    \"statusCode\": 201,\n    \"msg\": \"CREATED\"\n}"

String Templates lassen sich auch zur Aufbereitung einer simplen HTML-Seite nutzen (Listing 4). Nach dem Ersetzen entsteht das HTML in Listing 5.

String title = "My First Web Page";
String text = "My Hobbies:";
var hobbies = List.of("Cycling", "Hiking", "Shopping");
String html = STR."""
<html>
  <head><title>\{title}</title></head>
  <body>
    <p>\{text}</p>
    <ul>
      <li>\{hobbies.get(0)}</li>
      <li>\{hobbies.get(1)}</li>
      <li>\{hobbies.get(2)}</li>
    </ul>
  </body>
</html>""";
<html>
  <head><title>My First Web Page</title></head>
  <body>
    <p>My Hobbies:</p>
    <ul>
      <li>Cycling</li>
      <li>Hiking</li>
      <li>Shopping</li>
    </ul>
  </body>
</html>

Speichert man das Ganze als Datei und öffnet diese in einem Browser, so erhält man in etwa die Darstellung in Abbildung 1.

Abb. 1: Simple HTML-Seite mit String Templates

Besonderheiten

Interessanterweise ist es möglich, auch komplexere Aktionen in einem Platzhalter auszuführen. Als eher einfaches Beispiel haben wir bereits einen Methodenaufruf gesehen. Es sind aber auch Aktionen wie die in Listing 6 möglich, also ein Postinkrement, der ?-Operator oder Zugriffe auf das Date and Time API – in diesem Muster lassen sich dann einfache Anführungszeichen ohne Escaping nutzen.

int index = 0;
var modified = STR."\{index++}, \{index++}, \{index++}, \{index++}";
System.out.println(modified);

String filePath = "tmp.dat";
File file = new File(filePath);
  String old = "The file " + filePath + " " + file.exists() ? "does" : "does not" + " exist");
 String msg = STR. "."The file \{filePath} \{file.exists() ? 
                  "does" : "does not"} exist";

String currentTime = STR."Current time: \{DateTimeFormatter.ofPattern("HH:mm").format(LocalTime.now())}";
System.out.println(currentTime);

Stay tuned

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

 

Neben STR existieren weitere vordefinierte Prozessoren, etwa FMT, um eine Ausgabe wie mit String.format() zu erzielen. Betrachten wir dazu die Methode in Listing 7.

private static void alternativeStringProcessors() {
  int x = 47;
  int y = 11;
  String calculation1 = FMT."%6d\{x} + %6d\{y} = %6d\{x + y}";
  System.out.println("fmt calculation 1: " +calculation1);

  float base = 3.0f;
  float addon = 0.1415f;

  String calculation2 = FMT."%2.4f\{base} + %2.4f\{addon}" +
                        FMT."= %2.4f\{base + addon}";
  System.out.println("fmt calculation 2 " + calculation2);

    String calculation3 = FMT."Math.PI * 1.000 = %4.6f\{Math.PI * 1000}";
    System.out.println("fmt calculation 3 " + calculation3);
}

Als Ausgabe erhält man:

fmt calculation 1:     47  +     11 =     58
fmt calculation 2: 3.0000  + 0.1415 = 3.1415
fmt calculation 3: Math.PI * 1.000 = 3141.592654

Während STR standardmäßig verfügbar ist, muss man FMT auf geeignete Weise importieren:

import static java.util.FormatProcessor.FMT;

Eigene Stringprozessoren

Die Arbeitsweise von STR und FMT ist eingängig. Möchte man selbst steuernd eingreifen, ist es sogar möglich, eigene Template Processors zu erstellen, indem man das Interface StringTemplate.Processor implementiert. Dabei existieren diverse Methoden, die man für eine angepasste Variante nutzen kann. Folgendes Beispiel zeigt sowohl STR als auch die eigene Ausprägung in Form der Klasse MyProcessor im Einsatz:

String name = "Michael";
int age = 52;
System.out.println(STR."Hello \{name}. You are \{age} years old.");

var myProc = new MyProcessor();
System.out.println(myProc."Hello \{name}. You are \{age} years old.");

Betrachten wir die Ausgaben, die auch verschiedene Resultate der internen Methoden zeigen, um das Verständnis dafür zu erhöhen, wie die Abläufe und Verarbeitungsschritte innerhalb eines Template Processor erfolgen. Ziel des eigenen Prozessors ist es, die Werte jeweils speziell mit >> und << zu markieren:

Hello Michael. You are 52 years old.
-- process() --
=> fragments: [Hello , . You are ,  years old.]
=> values: [Michael, 52]
=> interpolate: Hello Michael. You are 52 years old.
Hello >>Michael<<. You are >>52<< years old.

Tatsächlich ist gar nicht viel Arbeit in der Methode process() zu erledigen, insbesondere würde man die Ausgaben weglassen, hier dienen sie lediglich dem Verständnis und der leichteren Nachvollziehbarkeit (Listing 8).

static class MyProcessor implements StringTemplate.Processor<String,
                                    IllegalArgumentException> {
    @Override
    public String process(StringTemplate stringTemplate) 
                  throws IllegalArgumentException {
        System.out.println("\n-- process() --");
        System.out.println("=> fragments: " + stringTemplate.fragments());
        System.out.println("=> values: " + stringTemplate.values());
        System.out.println("=> interpolate: " + stringTemplate.interpolate());
        System.out.println();

        var adjustedValues = stringTemplate.values().stream().
                                            map(str -> ">>" + str + "<<").
                                            toList();

        return StringTemplate.interpolate(stringTemplate.fragments(),
                                          adjustedValues);
    }
}

JEP 431: Sequenced Collections

Das Java Collections API ist eins der ältesten und am besten konzipierten APIs im JDK und bietet die drei Haupttypen List, Set und Map. Was allerdings fehlt, ist so etwas wie eine geordnete Reihenfolge von Elementen in Form eines Typs. Einige Collections haben eine sogenannte Encounter Order/Iteration Order (Begegnungsreihenfolge), d. h., es ist definiert, in welcher Reihenfolge die Elemente durchlaufen werden, etwa

  • List: indexbasiert, von vorne nach hinten
  • TreeSet: indirekt durch Comparable oder übergebenen Comparator definiert
  • LinkedHashSet: gemäß der Einfügereihenfolge

Dagegen definiert etwa HashSet keine derartige Encounter Order – und auch HashMap tut das nicht.

 

Auf Basis der Encounter Order ist auch das erste bzw. letzte Element definiert. Zudem lässt sich so eine Append-Funktionalität für vorn und hinten realisieren. Darüber hinaus ergibt sich auch die umgekehrte Reihenfolge. All das wird von Sequenced Collections adressiert.

Einführende Beispiele

Bis Java 21 war es mühsam, auf das letzte Element einer Collection zuzugreifen. Schlimmer noch – es existieren diverse, jeweils vom Typ der Collection abhängige unterschiedliche Wege:

var lastListElement = list.get(list.size() - 1);
var lastSortedElement = sortedSet.last();
var lastDequeElement = deque.getLast();

Fast ebenso unhandlich waren mitunter Zugriffe auf das erste Element, insbesondere für Sets:

var firstElement = list.get(0);
var firstUnordered = hashSet.iterator().next();
var firstOrdered = treeSet.iterator().next();
var firstLhs = linkedHashSet.iterator().next();

Sich all diese Besonderheiten zu merken, ist mühsam und fehleranfällig. Selbst mit IDE-Unterstützung bleibt noch die mangelnde Eleganz. Schauen wir uns nun Sequenced Collections an und wie diese nicht nur diese Aufgabenstellung, sondern auch das Anfügen und Löschen von Elementen vereinfachen.

Abhilfe: Sequenced Collections

Bislang bietet Java keinen Typ, der eine geordnete Folge von Elementen repräsentiert. Wie angedeutet, füllt Java 21 diese Lücke durch die Sequenced Collections, genauer die Einführung der Interfaces SequencedCollection, SequencedSet und SequencedMap. Sie bieten Methoden zum Hinzufügen, Ändern oder Löschen von Elementen am Anfang oder Ende der Collection sowie zur Bereitstellung einer Collection in umgekehrter Reihenfolge.

Das Interface SequencedCollection

Betrachten wir das Interface SequencedCollection (Listing 9).

interface SequencedCollection<E> extends Collection<E> {
  SequencedCollection<E> reversed();
  void addFirst(E);
  void addLast(E);
  E getFirst();
  E getLast();
  E removeFirst();
  E removeLast();
}

Es ist leicht ersichtlich, dass SequencedCollection das Interface Collection erweitert und Methoden zum Hinzufügen, Ändern oder Löschen von Elementen am Anfang oder Ende bietet. Die Methoden addXyz() und removeXyz() lösen für Immutable Collections eine UnsupportedOperationException aus. Die Methode reversed() ermöglicht die Verarbeitung der Elemente in umgekehrter Reihenfolge. Tatsächlich handelt es sich dabei um eine View, ähnlich wie bei subList(). Dadurch werden Änderungen in der View auch in der Original-Collection sichtbar und vice versa.

Stay tuned

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

 

Ein kurzer Blick hinter die Kulissen zeigt, wie es mit Hilfe von DefaultMethoden möglich wurde, die Interfaces in die bestehende Interfacehierarchie einzupassen (Listing 10).

public interface SequencedCollection<E> extends Collection<E> {
  SequencedCollection<E> reversed();

  default void addFirst(E e) {
    throw new UnsupportedOperationException();
  }

  default void addLast(E e) {
    throw new UnsupportedOperationException();
    }

  default E getFirst() {
    return this.iterator().next();
  }

  default E getLast() {
    return this.reversed().iterator().next();
  }

  default E removeFirst() {
    var it = this.iterator();
    E e = it.next();
    it.remove();
    return e;
  }

  default E removeLast() {
    var it = this.reversed().iterator();
    E e = it.next();
    it.remove();
    return e;
  }
}

Mit dieser Umsetzung sehen wir die Interfacehierarchie und die drei grün markierten neuen Interfaces der Sequenced Collections in Abbildung 2.

Abb. 2: Die Interfacehierarchie (neue Interfaces sind grün markiert) (Bildquelle: [1])

Die Interfaces SequencedSet und SequencedMap

Betrachten wir nun noch die beiden Interfaces SequencedSet und SequencedMap. Beginnen wir mit SequencedSet. Es erweitert Set und basiert auch auf SequencedCollection, allerdings ohne neue Methoden zu definieren und mit einer kleinen Abweichung bei reversed(), das eine kovariante Überschreibung mit geändertem Rückgabewert ist:

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
  SequencedSet<E> reversed(); // covariant override
}

Analog zu SequencedCollection bietet SequencedMap die folgenden Methoden:

  • Entry<K, V> firstEntry() – liefert das erste Schlüssel-Wert-Paar
  • Entry<K, V> lastEntry() – liefert das letzte Schlüssel-Wert-Paar
  • Entry<K, V> pollFirstEntry() – entfernt das erste Schlüssel-Wert-Paar und gibt es zurück
  • Entry<K, V> pollLastEntry() – entfernt das letzte Schlüssel-Wert-Paar und gibt es zurück
  • V putFirst(K, V) – fügt ein Schlüssel-Wert-Paar am Anfang ein
  • V putLast(K, V) – fügt ein Schlüssel-Wert-Paar am Ende an
  • SequencedMap<K, V> reversed() – gibt eine View in umgekehrter Reihenfolge zurück

Damit ergibt sich die Interfacedefinition aus Listing 11.

interface SequencedMap<K,V> extends Map<K,V> {
  SequencedMap<K,V> reversed();
  SequencedSet<K> sequencedKeySet();
  SequencedCollection<V> sequencedValues();
  SequencedSet<Entry<K,V>> sequencedEntrySet();
  V putFirst(K, V);
  V putLast(K, V);
  Entry<K, V> firstEntry();
  Entry<K, V> lastEntry();
  Entry<K, V> pollFirstEntry();
  Entry<K, V> pollLastEntry();
}

SequencedMap-API

Das API von SequencedMap fügt sich nicht so gut in die Sequenced Collections ein. Es verwendet NavigableMap als Basis, daher bietet es statt getFirstEntry() die Methode firstEntry() und statt removeLastEntry() bietet es pollLastEntry(). Diese Namen korrespondieren nicht mit denen der SequencedCollection. Allerdings hätte der Versuch, dies zu tun, dazu geführt, dass NavigableMap vier neue Methoden erhalten hätte, die das Gleiche tun, wie die vier anderen Methoden, über die es bereits verfügt.

Sequenced Collections in Aktion

Zum Abschluss wollen wir die neuen Möglichkeiten einmal in Aktion erleben. Dazu definieren wir eine Liste mit Buchstaben und fragen im Anschluss das erste und das letzte Element ab. Danach erzeugen wir mit reversed() eine Collection mit umgekehrter Reihenfolge, die wir durchlaufen, zur Demonstration in einen Stream wandeln und die drei Elemente überspringen sowie schließlich das erste und letzte Element der umgekehrten Reihenfolge abfragen (Listing 12). Die Ausgabe zeigt Listing 13.

public static void sequenceCollectionExample() {
    System.out.println("Processing letterSequence with list");
    SequencedCollection<String> letterSequence = List.of("A", "B", "C", 
                                                         "D", "E");
    System.out.println(letterSequence.getFirst() + " / " +
                       letterSequence.getLast());

    System.out.println("Processing letterSequence in reverse order");
    SequencedCollection<String> reversed = letterSequence.reversed();
    reversed.forEach(System.out::print);
    System.out.println();
    System.out.println("reverse order stream skip 3");
    reversed.stream().skip(3).forEach(System.out::print);
    System.out.println();
    System.out.println(reversed.getFirst() + " / " + 
                       reversed.getLast());
    System.out.println();
}
Processing letterSequence with list
A / E
Processing letterSequence in reverse order
EDCBA
reverse order stream skip 3
BA
E / A

 

Variieren wir nun die Datenstruktur und nutzen Sets zur Verwaltung der Elemente. Zunächst einmal wird durch mehrmaliges Ausführen deutlich, dass die mit Set.of() erzeugten Daten keine fixe Reihenfolge besitzen, sondern dass diese von Ausführung zu Ausführung variiert. Am Beispiel eines TreeSet schauen wir uns die Möglichkeiten der Sequenced Collections, genauer des SequencedSet, an (Listing 14). Die Ausgabe zeigt Listing 15.

public static void sequencedSetExample() {
    // plain Sets do not have encounter order ... 
    // run multiple times to see variation
    System.out.println("Processing set of letters A-D");
    Set.of("A", "B", "C", "D").forEach(System.out::print);
    System.out.println();
    System.out.println("Processing set of letters A-I");
    Set.of("A", "B", "C", "D", "E", "F", "G", "H", "I").
        forEach(System.out::print);
    System.out.println();

    // TreeSet has order
    System.out.println("Processing letterSequence with tree set");
    SequencedSet<String> sortedLetters = 
                         new TreeSet<>((Set.of("C", "B", "A", "D")));
    System.out.println(sortedLetters.getFirst() + " / " + 
                       sortedLetters.getLast());
    sortedLetters.reversed().forEach(System.out::print);
    System.out.println();
}
Processing set of letters A-D
DCBA
Processing set of letters A-I
IHGFEDCBA
Processing letterSequence with tree set
A / D
DCBA

Fazit

Wir haben uns exemplarisch mit den Sequenced Collections als finalem Feature und mit den String Templates als vielversprechendem Preview Feature beschäftigt.

Sequenced Collections bereichern die ohnehin schon ausgefeilte Funktionalität des Collections-Frameworks und vereinheitlichen vor allem Zugriffe und Veränderungen des ersten und des letzten Elements sowie das Bereitstellen einer umgekehrten Reihenfolge.

Für die Konkatenation von fixen und variablen Textbestandteilen zu einem String gab es bisher schon diverse Varianten, alle mit spezifischen Stärken und Schwächen. String Templates werden die Art und Weise, wie man diese Aufgabe bewältigt, revolutionieren und stellen ein Feature dar, das Java wieder ein wenig moderner macht und zu anderen Sprachen wie Python oder Kotlin aufschließen lässt.

Java 21 bringt viele weitere Neuerungen, die die Programmiersprache voranbringen und attraktiver machen. Auch für komplette Newbies wird die Einstiegshürde gesenkt: Dank Unnamed Classes und Instance Main Methods lassen sich kleinere Java-Programme viel schneller und mit deutlich weniger Zeilen und für Anfänger schwierigen Begrifflichkeiten erstellen.

In diesem Sinne: Happy Coding mit dem brandaktuellen Java 21!


Links & Literatur

[1] https://cr.openjdk.org/~smarks/collections/SequencedCollectionDiagram20220216.png

The post Neues in Java 21: JEP 430 und 431 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.

]]>
Chaos Monkey für Spring Boot https://jax.de/blog/chaos-monkey-fuer-spring-boot-chaos-engineering/ Tue, 15 Aug 2023 07:04:38 +0000 https://jax.de/?p=88898 Mit Chaos Engineering wird bewusst Chaos in ein System eingeführt, um dessen Reaktion auf unvorhergesehene Ereignisse zu testen und Schwachstellen aufzudecken. Der „Chaos Monkey for Spring Boot“ ist eine populäre Implementierung für Chaos Engineering in Spring-Boot-Anwendungen. Es ist ein leistungsfähiges Werkzeug, das die Robustheit von Spring-Boot-Anwendungen durch die gezielte Einführung von Chaostests verbessert.

The post Chaos Monkey für Spring Boot appeared first on JAX.

]]>
Chaos Monkey for Spring Boot [1] ermöglicht es Entwicklern, Ausfallszenarien zu simulieren und die Reaktion ihres Systems darauf zu überwachen. Dadurch können potenzielle Schwachstellen identifiziert und Engpässe aufgedeckt werden, um die Stabilität und Ausfallsicherheit der Anwendung zu verbessern. Beispielsweise ermöglicht Chaos Monkey den Entwicklern das plötzliche Abschalten eines Dienstes, Netzwerkstörungen oder die Simulation hoher Lasten auf das System. Durch diese gezielte Einführung von Chaostests und die Behebung der Schwachstellen ist sichergestellt, dass ihre Anwendung auch unter schwierigen Bedingungen zuverlässig funktioniert. Der Chaos Monkey for Spring Boot bietet zahlreiche Funktionalitäten und Konfigurationsoptionen, um den Testprozess zu steuern und die gewünschten Szenarien zu simulieren. Dieser Artikel bietet einen detaillierten Einblick in den Chaos Monkey for Spring Boot. Darüber hinaus werden seine Einsatzszenarien, Best Practices und Implementierungsmöglichkeiten diskutiert.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Vorstellung

Das Projekt Chaos Monkey for Spring Boot wurde ursprünglich anno 2017 von Benjamin Wilms bei codecentric ins Leben gerufen und über die Jahre von ihm und seinen Kollegen weiterentwickelt. Das Projekt wird aktiv gepflegt und an neue Spring-Boot-Versionen angepasst. Inspiriert von den Principles of Chaos Engineering [2] und mit einem Fokus auf Spring Boot bietet das Chaos-Monkey-Projekt eine Möglichkeit, Anwendungen besser zur Laufzeit zu testen. Das kann lokal, aber auch in der Cloud geschehen. Die Fragen, die sich jeder Entwickler stellt, egal wie viele Unit- und Integrationstests existieren: Wie verhält sich unsere Anwendung im Betrieb? Was passiert bei langsamen Datenbankabfragen? Wie verhält sich die Anwendung bei langsamen Antworten oder Fehlern anderer Services? Was passiert, wenn Discovery Service oder Discovery Client Fehler produzieren? Oder sogar zu viel Speicher oder CPU verbrauchen? Um all diese Probleme zu simulieren und Störungen zu injizieren, unterstützt uns nun der Chaos Monkey.

Implementierung und Konfiguration

Es gibt zwei Möglichkeiten, Chaos Monkey for Spring Boot in einer bestehenden Spring-Boot-Anwendung zu aktivieren. Entweder fügt man ihn zu den regulären Anwendungsabhängigkeiten im Build- und Dependency-Management-Tool hinzu (Listing 1) oder man bindet ihn als externe Abhängigkeit beim Start der Spring-Boot-Anwendung ein. Da sich das Tool nicht im offiziellen Release-Train von Spring Boot befindet, muss die passende Version selbst definiert werden.

// Maven
<dependency>
  <groupId>de.codecentric</groupId>
  <artifactId>chaos-monkey-spring-boot</artifactId>
  <version>3.0.1</version>
</dependency>
 
// Gradle
plugins {
  id 'java'
  id 'org.springframework.boot' version '3.0.1'
  id 'io.spring.dependency-management' version '1.1.0'
}

Beim Start der Spring-Boot-Anwendung mit aktiviertem „chaos-monkey“-Profil wird die Funktionalität für die ausgewählten Watchers aktiv. Die Eigenschaften können über Umgebungsvariablen, System Properties oder Application Properties konfiguriert werden (Listing 2). Die Konfiguration über Properties bietet alle verfügbaren Optionen, erfordert aber einen Neustart, um Änderungen zu übernehmen. Um das zu vermeiden, können die Konfiguration über Actuator REST API vorgenommen werden.

spring.profiles.active=chaos-monkey
chaos.monkey.enabled=true
management.endpoints.web.exposure.include=health,info,chaosmonkey
 
chaos.monkey.watcher.service=true
chaos.monkey.assaults.latencyActive=true

Stay tuned

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

 

Chaos Monkey Watchers und Assaults

Ein Watcher ist eine Komponente von Chaos Monkey for Spring Boot, die die Anwendung nach Spring Beans durchsucht. Je nach Konfiguration werden Annotation Watchers (Abb. 1) z. B. für @Component, Actuator Watchers und Outgoing Request Watchers z. B. für RestTemplate und WebClient registriert. Der Alternative Bean Watcher verwendet im Gegensatz zu den anderen Watchers ausschließlich den Bean-Namen. Mit Hilfe von Spring AOP wird ein Proxy für die Watcher-Funktionalität zur Verfügung gestellt. Beim Aufruf einer öffentlichen Methode auf einer Watched Bean entscheidet dann der Chaos Monkey, ob ein Angriff oder eben keine Aktion ausgeführt wird. Dieses Verhalten kann durch die Konfiguration beeinflusst werden.

Abb. 1: Annotation Watchers und Assaults

Assaults sind das Kernelement von Chaos Monkey und ihre Anwendung erfolgt je nach Konfiguration. Es gibt verschiedene Arten von Assaults: Request Assaults greifen spezifische Punkte der Anwendung an und können Latenz oder Exceptions verursachen. Runtime Assaults hingegen greifen die gesamte Anwendung an und können diese sogar zum Absturz bringen (AppKiller Assault). Möglich ist auch, nur den Speicher (Memory Assault) oder die CPU (CPU Assault) der Java Virtual Machine anzugreifen. Dank Chaos Monkey for Spring Boot können alle Arten von Runtime Exceptions geworfen und die benötigten Exceptions zur Laufzeit konfiguriert werden. Die Runtime Assaults können entweder durch Scheduling oder durch Chaos Monkey Endpoints ausgelöst werden.

Konfiguration des Chaos Monkey

Essenziell im Umgang ist, dass eine Änderung der Watcher-Konfiguration stets einen Neustart der Anwendung zur Folge hat. Das liegt an der Proxy-basierten Implementierung des Tools. Daher muss die gewünschte Konfiguration folglich bereits beim Start der Anwendung festgelegt werden. Jedoch können Watchers via Spring Boot Actuators auch später noch zur Laufzeit umkonfiguriert werden. Da es Spring Boot mittels des Actuators ferner ermöglicht, diese über JMX auszulesen bzw. anzupassen, bietet der Chaos Monkey diese Funktionalität auch für die Konfiguration an.

Welche Spring Beans benötigen nun gegenwärtig einen Watcher? In vielen Fällen ist ein Watcher für Service-Komponenten ausreichend. Runtime Exceptions werden z. B. durch die Spring-Data-Implementierung geworfen und erst im Web-Layer als Fehler an den Aufrufer zurückgegeben. Ob nun die Data Access Exception im Repository oder im Service durch den Assault erzeugt wird, ist in diesem Fall nicht relevant. Sollte der Service jedoch Fallback-Strategien implementieren, wäre die Repository-Schicht vorzuziehen (Listing 3).

#spring.profiles.active=chaos-monkey
 
management.endpoint.chaosmonkey.enabled=true
management.endpoint.chaosmonkeyjmx.enabled=false
 
chaos.monkey.watcher.controller=false
chaos.monkey.watcher.restController=false
chaos.monkey.watcher.service=true
chaos.monkey.watcher.repository=false
chaos.monkey.watcher.component=false
chaos.monkey.watcher.beans=<List of bean names>
chaos.monkey.watcher.exclude=<List of class names>

Wie zuvor erwähnt, erfolgt die Konfiguration der Assaults fortan über den Actuator Endpoint. Dies ermöglicht auch die Ausführung von Chaostests in einer CI/CD Pipeline. Folgende Beispiele zeigen mögliche Problemfälle. Die in Listing 4 gezeigte Konfiguration erzeugt zufällige Runtime Exceptions mit dem Exception Assault. Der Assault-Level in der Konfiguration definiert, wie viele Requests angegriffen werden sollen. In unserem Beispiel ist das jeder fünfte. Ob nun eine normale Runtime Exception oder eine andere ausgelöst werden soll, kann konfiguriert werden.

curl -X POST http://localhost:8080/actuator/chaosmonkey/assaults \
-H 'Content-Type: application/json' \
-d \
‘
{
  "level": 5,
  "latencyActive": false,
  "exceptionsActive": true,
  "killApplicationActive": false
}’

Im nächsten Beispiel (Listing 5) wird der AppKill Assault konfiguriert. Dieser kann über eine CronExpression oder einen Actuator-Aufruf ausgelöst werden.

curl -X POST http://localhost:8080/actuator/chaosmonkey/assaults \
-H 'Content-Type: application/json' \
-d \
‘
{
  "latencyActive": false,
  "exceptionsActive": false,
  "killApplicationActive": true,
  "killApplicationExpression": "*/1 * * * * ?"
}'

Im letzten Beispiel (Listing 6) sollen hängende Threads simuliert werden. Das Hinzufügen einer zusätzlichen Latenz von 100 s bei jeder Antwort (Level 1) führt dazu, dass die Anwendung nicht mehr reagiert.

curl -X POST http://localhost:8080/actuator/chaosmonkey/assaults \
-H ‘Content-Type: application/json’ \
-d \
‘
{
  "level": 1,
  "latencyRangeStart": 99999,
  "latencyRangeEnd": 100000,
  "latencyActive": true,
  "killApplicationActive": false,
  "exceptionsActive": false
}

Das sind nur drei Beispiele, die verschiedene Konfigurationen aufzeigen. Darüber hinaus existieren noch unzählige weitere Möglichkeiten – der Kreativität sind keine Grenzen gesetzt. Sind die Experimente einmal definiert und konfiguriert, kann man mit einem Tool wie Gatling [3] oder dem Chaos Toolkit [4] Tests durchführen und die Reports und Metriken analysieren.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Metriken der Attacken

Die Chaos-Monkey-Metriken sind über das Spring Boot Actuator API und Micrometer [5] verfügbar. Damit können die gesammelten Metriken für die Chaostests aus der Anwendung extrahiert werden (Listing 7). Diese Metriken bieten wertvolle Einblicke in das Verhalten der Anwendung während der Chaostests. Sie ermöglichen es Entwicklern, die Auswirkungen der verschiedenen Angriffe auf Performance, Latenz, Speicherverbrauch und andere wichtige Aspekte zu überwachen und zu analysieren. Der Einsatz von Chaos-Monkey-Metriken ermöglicht es Entwicklern, die Auswirkungen von Störungen und Unregelmäßigkeiten auf die Anwendung besser zu verstehen und gezielte Optimierungen vorzunehmen. Außerdem ist es möglich, die Metriken einzusetzen, um die Effektivität der implementierten Resilienz-Patterns zu bewerten und potenzielle Schwachstellen zu identifizieren.

curl -X GET http://localhost:8080/actuator/metrics \
-H 'Content-Type: application/json'
 
#Application Metrics
chaos_monkey_application_request_count_total
chaos_monkey_application_request_count_assaulted
#Assault Exception
chaos_monkey_assault_exception_count
#Assault Latency
chaos_monkey_assault_latency_count_gauge
chaos_monkey_assault_latency_count_total
#Assault KillApp
chaos_monkey_assault_killapp_count
#Watcher Metrics
chaos_monkey_assault_component_watcher_total
chaos_monkey_assault_controller_watcher_total

Patterns für die Behebung der Schwachstellen

In einer verteilten Architektur, in der verschiedene Services miteinander kommunizieren, treten oftmals unvermeidbare Fehler und Störungen auf. Resilience Patterns [6] dienen dazu, diese Fehler abzufangen, abzuschwächen und die Gesamtstabilität des Systems zu gewährleisten. Ein häufig verwendetes Pattern ist das Circuit Breaker Pattern. Dieses unterbricht die Kommunikation mit einem Dienst, falls dieser nicht ordnungsgemäß funktioniert, und stattdessen Fehlerbehandlungsmechanismen einsetzt. Das Fallback Pattern stellt alternativ Funktionalität oder Daten bereit, sofern ein Dienst nicht verfügbar ist. Retry Patterns ermöglichen das erneute Senden von Anfragen, wenn diese aufgrund von temporären Netzwerkproblemen fehlschlagen. Das Timeout Pattern definiert maximale Zeitlimits für Anfragen, um lange Wartezeiten zu vermeiden und die Reaktionsfähigkeit des Systems zu gewährleisten. Das Bulkhead Pattern und das Rate-Limiter Pattern sind weitere wichtige Resilience Patterns. Sie zielen darauf ab, die Auswirkungen von Fehlern in einem Dienst zu begrenzen. Daneben schützen sie andere Teile des Systems vor Ausfällen, indem sie Ressourcen isolieren und in separate Pools aufteilen. Durch den Einsatz dieser und weiterer Resilience Patterns können Microservices optimaler auf unvorhergesehene Situationen reagieren, Ausfälle isolieren und eine höhere Verfügbarkeit und Ausfallsicherheit erreichen. Die effektive Anwendung dieser Patterns ist entscheidend, um die Zuverlässigkeit des gesamten Microservices-Ökosystems zu gewährleisten.

Die Bibliothek Resilience4j [7] bietet umfassende Unterstützung für die Implementierung von Resilience Patterns in Microservices und lässt sich nahtlos in Spring-Boot-Anwendungen integrieren. Durch die Integration mit Spring Boot können Entwickler die Funktionalität von Resilience4j nutzen, indem sie kurzerhand die entsprechenden Abhängigkeiten und Konfigurationen hinzufügen. Die Resilience4j Annotations und Wrapper erleichtern die Anwendung der Resilience Patterns auf Spring-Boot-Komponenten wie Services, Controller oder Repository-Klassen. Ferner ermöglicht die Integration mit Spring Boot die Verwendung von Spring Boot Actuator zur Überwachung und Erfassung von Metriken für die Resilience Patterns. Infolgedessen können Entwickler das Verhalten ihrer Microservices in Bezug auf Fehlertoleranz und Resilienz effektiv überwachen und analysieren. Die nahtlose Integration erleichtert Entwicklern die Implementierung einer robusten und widerstandsfähigen Architektur in ihren Microservices.

Stay tuned

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

 

Fazit

Chaos Monkey for Spring Boot ist ein sehr nützliches Werkzeug für alle Softwareentwickler, die sich mit Chaos Engineering beschäftigen möchten. Durch die Implementierung mit Spring AOP Proxies können Seiteneffekte innerhalb der Anwendung simuliert werden, ohne die reale Infrastruktur zu beeinflussen. Dies ermöglicht es Entwicklern, Probleme auf einfache Weise zu simulieren und zu reproduzieren, sogar auf ihren eigenen Computern. Sobald man damit beginnt, Chaosexperimente zu definieren, wird deutlich, dass zunächst normalerweise das Monitoring verbessert werden muss, um die auftretenden Probleme zu identifizieren. Anschließend werden typische Resilienz-Patterns verwendet, um die entdeckten Schwachstellen zu beheben. Chaos Monkey for Spring Boot erleichtert also nicht nur das Experimentieren mit Chaos, sondern unterstützt auch die Entwicklung resilienter Anwendungen.


Links & Literatur

[1] Chaos Engineering for Spring Boot: https://github.com/codecentric/chaos-monkey-spring-boot

[2] Principles of Chaos Engineering: https://principlesofchaos.org

[3] Gatling: https://gatling.io

[4] Chaos Toolkit: https://chaostoolkit.org

[5] Micrometer: https://micrometer.io

[6] Resilience Patterns: https://github.com/lucasnscr/Resilience-Patterns

[7] https://resilience4j.readme.io/docs

The post Chaos Monkey für Spring Boot appeared first on JAX.

]]>