Serverside Java - JAX https://jax.de/blog/serverside-enterprise-java/ Java, Architecture & Software Innovation Fri, 18 Oct 2024 13:30:25 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Exactly Once in verteilten Systemen: Realität oder Utopie? https://jax.de/blog/exactly-once-idempotenz-java/ Mon, 10 Jun 2024 13:24:45 +0000 https://jax.de/?p=89825 Sind verteilte Systeme im Einsatz, wie es beispielsweise bei Microservices der Fall ist, ist eine verteilte Datenverarbeitung – häufig asynchron über Message Queues – an der Tagesordnung. Werden Nachrichten ausgetauscht, sollen diese häufig genau einmal verarbeitet werden – gar nicht so einfach, wie sich herausstellt.

The post Exactly Once in verteilten Systemen: Realität oder Utopie? appeared first on JAX.

]]>
Bei verteilten Systemen wird eine asynchrone Kommunikation häufig über einen Message Broker abgedeckt. Dadurch soll eine Entkopplung zwischen zwei Diensten erreicht werden, die unter Umständen separat skaliert werden können. Eine Kommunikation über einen Message Broker ist inhärent immer mindestens zweigeteilt, nämlich in Producer und Consumer.

Ein Producer erstellt dabei Nachrichten, wie in Abbildung 1 gezeigt, während ein Consumer sie verarbeitet.

Abb.1: Einfacher Nachrichtenaustausch

Der Message Broker ist häufig ein zustandsbehaftetes System – eine Art Datenbank – und vermittelt Nachrichten zwischen Producer und Consumer. Als zustandsbehaftetes System hat ein Message Broker die Aufgabe, Nachrichten vorzuhalten und abrufbar zu machen. Ein Producer schreibt also Nachrichten in den Broker, während ein Consumer sie zu einer beliebigen Zeit lesen kann. Exactly once, also einmaliges Ausliefern, bedeutet, dass der Producer genau eine Nachricht produziert und der Consumer diese genau einmal verarbeitet. Also ganz einfach, oder?

Stay tuned

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

 

Es kann so einfach sein

Da eine Kommunikation zwischen den Systemen über eine Netzwerkebene stattfindet, ist nicht gewährleistet, dass die Systeme über den gleichen Wissensstand verfügen. Es muss also einen Rückkanal geben, der einzelne Operationen bestätigt, um einen Zustand zu teilen. Nur so wird sichergestellt, dass Nachrichten, die erstellt wurden, auch korrekt angekommen und verarbeitet worden sind. Aktualisiert sieht der Fluss also eher so aus, wie es Abbildung 2 zeigt.

Abb. 2: Durch Bestätigungen wird der Zustand zwischen Systemen geteilt

Erstellt ein Producer eine Nachricht, die vom Consumer gelesen werden soll, benötigt dieser eine Bestätigung (Abb. 2, Schritt 2). Nur dadurch weiß der Producer, dass die Nachricht korrekt im Broker persistiert vorliegt und er sie nicht erneut übertragen muss.

Der Consumer wiederum liest die Nachricht und verarbeitet sie. Ist die Verarbeitung fehlerfrei abgeschlossen, bestätigt der Consumer das. Der Broker muss die Nachricht deshalb nicht noch einmal ausliefern.

Immer Ärger mit der Kommunikation

Bei einer verteilten Kommunikation über ein Netzwerk kann es leider immer passieren, dass diverse Kommunikationskanäle abbrechen oder Fehler entstehen – und zwar an vielen Stellen: beim Erstellen, vor dem Konsumieren und danach. Genau diese Eigenschaft macht es so schwer oder gar unmöglich, eine Exactly-once-Semantik zu erreichen.

Angenommen, ein Producer produziert eine Nachricht (Abb. 3). Um sicherzustellen, dass diese auch vom Broker gespeichert wurde, wartet der Producer auf eine Bestätigung. Bleibt sie aus, ist nicht garantiert, dass der Broker diese Nachricht wirklich ausliefern wird.

Abb. 3: Der Producer erhält keine Bestätigung und sendet die Nachricht erneut

Der Producer muss diese Nachricht folglich erneut ausliefern. Genau das ist in Schritt 2 der Abbildung 3 auch geschehen, weshalb der Producer in Schritt 3 eine weitere Nachricht mit demselben Inhalt sendet. Da jetzt zwei Nachrichten vorliegen, verarbeitet der Consumer in den Schritt 4 und 5 beide Nachrichten – wohl eher nicht „exactly once“. Die Nachricht wird durch den Retry-Mechanismus „at least once“ – mindestens einmal, nicht genau einmal – übertragen. Denn wie im Bild zu erkennen ist, überträgt der Producer dieselbe Nachricht zweimal, um sicherzustellen, dass sie mindestens einmal vom Broker bestätigt wurde. Nur so ist sichergestellt, dass die Nachricht nicht verloren geht.

ALL ABOUT MICROSERVICES

Microservices-Track entdecken

 

Natürlich kann die Bestätigung auch ignoriert werden. Schritt 2 kann also ausbleiben. Ein Retry-System würde folglich fehlen. Der Producer überträgt also eine Nachricht, ohne auf eine Bestätigung des Brokers zu warten. Kann der Broker die Nachricht selbst nicht verarbeiten oder wegspeichern, hat er keine Möglichkeit, das Fehlschlagen oder eine erfolgreiche Operation zu quittieren. Die Nachricht würde „at most once“ – maximal einmal oder eben keinmal – übertragen werden. Exactly once ist also grundsätzlich ein Problem verteilter Anwendungen, die mittels Bestätigungen funktionieren.

Leider ist das noch nicht das Ende der Fahnenstange, wenn die Nachricht Ende zu Ende, also vom Producer bis zum Consumer, betrachtet wird. Denn es existiert in einem solchen System zusätzlich ein Consumer, der die Nachrichten wiederum einmalig verarbeiten muss. Selbst wenn garantiert wird, dass der Producer eine Nachricht einmalig erzeugt, ist ein einmaliges Verarbeiten nicht garantiert.

Abb. 4: Ein Consumer verarbeitet die Nachricht und versucht diese danach zu bestätigen

Es kann passieren, dass der Consumer wie in Abbildung 4 gezeigt die Nachricht in Schritt 3 liest und in Schritt 4 korrekt verarbeitet. In Schritt 5 geht die Bestätigung verloren. Das führt dazu, dass die Nachricht mehrmals, aber mindestens einmal – at least once – verarbeitet wird.

Abb. 5: Ein Consumer bestätigt die Nachricht vor dem Verarbeiten

Es ist natürlich umgekehrt auch möglich, die Nachricht vor dem Verarbeiten zu bestätigen. Der Consumer lädt also die Nachricht und bestätigt sie direkt. Erst dann wird in Schritt 5 von Abbildung 5 die Bearbeitung der Nachricht erfolgen. Schlägt jetzt die Bearbeitung fehl, ist die Nachricht in Schritt 4 bereits bestätigt worden und wird nicht erneut eingelesen. Die Nachricht wurde wieder maximal einmal oder keinmal – at most once – verarbeitet.

Wie also zu erkennen, ist es leicht, At-most-once- und At-least-onceSemantiken in den verschiedenen Konstellationen sowohl auf Producer- als auch auf der Consumer-Seite herzustellen. Exactly once ist aber aufgrund der verteilten Systematik ein schwieriges Problem – oder gar unmöglich?

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Lösungen müssen her

Für eine Möglichkeit, eine Exactly-once-Semantik zu erreichen, muss die Verarbeitung der Nachrichten einer Applikation eine bestimmte Eigenschaft unterstützen: Idempotenz. Idempotenz bedeutet, dass eine Operation, egal wie oft sie verarbeitet wird, immer dasselbe Ergebnis zur Folge hat. Ein Beispiel dieses Prinzips könnte das Setzen einer Variablen im Programmcode sein. Hier gibt es etwa die Möglichkeit, dies über Setter oder eben relative Mutationen zu implementieren.

Zum Beispiel setAge oder incrementAge. Die Operation person.setAge(14); kann beliebig oft nacheinander ausgeführt werden, das Ergebnis bleibt immer dasselbe, nämlich 14. Hingegen wäre person.incrementAge(1) nicht idempotent. Wird diese Methode unterschiedlich oft hintereinander ausgeführt, gibt es verschiedene Ergebnisse, nämlich nach jeder Ausführung ein Jahr mehr. Genau diese Eigenschaft der Idempotenz ist der Schlüssel, um eine Exactly-once-Semantik zu etablieren.

Angewandt auf die Systeme von zuvor bedeutet das, dass eine At-least-once-Semantik mit der Eigenschaft der Idempotenz zu einer Exactly-once-Verarbeitung führen kann. Wie eine At-least-once-Semantik umgesetzt werden kann, zeigt das zuvor beschriebene Bestätigungssystem. Was fehlt, ist also ein System von Idempotenz in der Verarbeitung. Aber wie kann eine Verarbeitung von Nachrichten idempotent gemacht werden?

Um das zu erreichen, muss der Consumer die Möglichkeit haben, einen lokalen, synchronisierten Zustand zu erhalten. Um den Zustand einer Nachricht zu erhalten, muss diese eindeutig identifizierbar sein. Nur so werden das Aufsuchen und eine Deduplizierung der Nachricht ermöglicht.

Abb. 6: Eine idempotente Verarbeitung

Anders als zuvor speichert der Consumer mit jedem Aufruf in Schritt 4 der Abbildung 6 die Nachricht zunächst in einer lokalen Zustandshaltung. An dieser Stelle kann, sofern die Nachricht bereits lokal vorhanden ist, ein erneutes Speichern vernachlässigt werden. In Schritt 5 wird die Nachricht bestätigt. Schlägt die Bestätigung fehl und wird die Nachricht folglich erneut übertragen, ist das kein Problem, da in Schritt 4 das erneute Speichern der Nachricht verhindert werden kann. An dieser Stelle lebt also die Idempotenz. Beim Bearbeiten kann der Consumer nun selbst entscheiden, ob eine Verarbeitung notwendig ist, z. B. indem zu einer Nachricht ein Status eingeführt und dieser in Schritt 6 lokal abgefragt wird. Steht dieser bereits auf Processed, muss nichts getan werden. Umgekehrt muss eine verarbeitete Nachricht den Status korrekt aktualisieren.

Stay tuned

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

 

Fazit

Verteilte Systeme haben ein grundsätzliches Problem, eine Exactly-once-Semantik herzustellen. Es kann auf Infrastrukturebene entweder zwischen at least once oder at most once gewählt werden. Erst durch die Eigenschaft der Idempotenz kann auf dem Applikationslevel sichergestellt werden, dass Nachrichten genau einmal von Ende zu Ende verarbeitet werden.

Natürlich ist das nicht kostenlos. Es bedeutet, dass die Applikation selbst eine Verwaltung von Nachrichten übernehmen muss und deren Zustand verwaltet – wirklich exactly once ist das natürlich auch nicht, es kommt diesem durch die Eigenschaft der Idempotenz jedoch im Ergebnis sehr nahe.

The post Exactly Once in verteilten Systemen: Realität oder Utopie? 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.

]]>
Hibernate 6: Die neuen Features vorgestellt (mit Beispielen) https://jax.de/blog/hibernate-6-die-neuen-features-vorgestellt-mit-beispielen/ Mon, 17 Apr 2023 13:09:34 +0000 https://jax.de/?p=88567 Vor wenigen Monaten wurde nach langer Entwicklungszeit Hibernate 6.0.0 veröffentlicht. Neben vielen internen Verbesserungen bietet das inzwischen bereits in Version 6.1 vorliegende Framework auch einige neue Features, die die Entwicklung neuer und bestehender Persistenzschichten vereinfachen.

The post Hibernate 6: Die neuen Features vorgestellt (mit Beispielen) appeared first on JAX.

]]>
Hibernate 6 enthält viele Änderungen, von denen einige Anpassungen am bestehenden Anwendungscode erfordern. Wenn man diese Anpassungen vorgenommen hat, steht einer erfolgreichen Migration nichts mehr im Wege. Im Anschluss erhält man nicht nur eine verbesserte Abfragegenerierung, die zu einer besseren Anwendungsperformanz führen soll, sondern auch die Möglichkeit mit Hilfe der neu eingeführten Features die eigene Persistenzschicht zu verbessern und zu vereinfachen. In diesem Artikel werden wir einige der nützlichsten Änderungen in Hibernate 6 genauer betrachten.

Reihenfolge von @ManyToManyBeziehungen beibehalten

Laut Javadoc soll eine java.util.List eine geordnete Sammlung von Werten darstellen. Viele Entwickler erwarten daher, dass die Elemente einer To-many-Beziehung ihre Reihenfolge beibehalten, wenn diese als java.util.List modelliert ist. Dies ist im Standardfall allerdings nicht der Fall, da Hibernate die Reihenfolge der Elemente nicht persistiert. Daher ist diese nicht mehr definiert, wenn die Beziehung aus der Datenbank gelesen wird.

Bis Hibernate 6.0.0 konnte das ausschließlich mit einer zusätzlichen @OrderColumn-Annotation an der beziehungsdefinierenden Seite einer @ManyToMany-Beziehung geändert werden (Listing 1). Hibernate persistiert den Index jedes Elements in einer separaten Spalte der Beziehungstabelle. Dieser Index wird für alle Elemente während Einfüge-, Aktualisierungs- und Löschoperationen verwaltet und bei Leseoperationen mit ausgelesen. Falls der Name dieser Spalte nicht über die @OrderColumn-Annotation definiert wird, erzeugt Hibernate den Spaltennamen aus dem Namen der Beziehungseigenschaft und dem Postfix _ORDER.

Listing 2 zeigt ein Beispiel der ausgeführten SQL-Operationen, wenn eine neue PurchaseOrder-Entität mit einer Liste von drei Product-Entitäten persistiert und anschließend gelesen wird. Die Beziehungsdatensätze in der Tabelle PurchaseOrder_Product enthalten neben den Referenzen zur PurchaseOrder und dem jeweiligen Product auch die entsprechende Position innerhalb der Produktliste. Hibernate verwendet diese während der Entitätsinstanziierung, um die ursprüngliche Reihenfolge der referenzierten Product-Entitäten wiederherzustellen.

@Entity
public class PurchaseOrder {
  
  @ManyToMany
  @OrderColumn
  private List<Product> products = new ArrayList<>();
 
  ...
}
16:33:39,608 DEBUG [org.hibernate.SQL] - insert into PurchaseOrder (customer, version, id) values (?, ?, ?)
16:33:39,617 DEBUG [org.hibernate.SQL] - insert into PurchaseOrder_Product (orders_id, products_ORDER, products_id) values (?, ?, ?)
16:33:39,623 DEBUG [org.hibernate.SQL] - insert into PurchaseOrder_Product (orders_id, products_ORDER, products_id) values (?, ?, ?)
16:33:39,626 DEBUG [org.hibernate.SQL] - insert into PurchaseOrder_Product (orders_id, products_ORDER, products_id) values (?, ?, ?)
16:33:39,694 DEBUG [org.hibernate.SQL] - select p1_0.id,p1_0.customer,p1_0.version from PurchaseOrder p1_0 where p1_0.id=?
16:33:39,723 DEBUG [org.hibernate.SQL] - select p1_0.orders_id,p1_0.products_ORDER,p1_1.id,p1_1.name,p1_1.price,p1_1.version from PurchaseOrder_Product p1_0 join Product p1_1 on p1_1.id=p1_0.products_id where p1_0.orders_id=?

Hibernate 6 unterstützt die @OrderColumn-Annotation auch weiterhin. Darüber hinaus kann die Behandlung einer java.util.List für die besitzende Seite einer @ ManyToMany-Beziehung und für ElementCollections nun auch global definiert werden. Dazu muss die Konfigurationseigenschaft hibernate.mapping.default_list_semantics in der Datei persistence.xml auf LIST gesetzt werden (Listing 3). Das führt zu der bereits beschriebenen Persistierung der Reihenfolge der Listenelemente, ohne dass jede @ManyToMany-Beziehung mit einer zusätzlichen @OrderColumn-Annotation versehen werden muss.

<persistence>
  <persistence-unit name="my-persistence-unit">
    ...
    <properties>
      <property name="hibernate.mapping.default_list_semantics" value="LIST" />
      ...
    </properties>
  </persistence-unit>
</persistence>

Bevor man die Persistierung der Reihenfolge von @ ManyToMany-Beziehungen global aktiviert, sollte man jedoch die Auswirkungen auf die Performanz genau abwägen. Die Verwaltung des Index erfordert einigen Aufwand, durch den vor allem Änderungen an einer Beziehung verlangsamt werden.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Hibernate persistiert die Position eines jeden Elements in der Beziehungstabelle. Das erfordert kaum zusätzlichen Aufwand, wenn neue Elemente am Ende der Liste hinzugefügt oder die letzten Elemente aus einer Liste entfernt werden. Anders sieht es aus, wenn ein Element am Anfang oder in der Mitte hinzugefügt oder entfernt wird.

Listing 4 zeigt ein Beispiel, in dem ein neues Product angelegt und an zweiter Stelle zur Liste der Produkte einer PurchaseOrder hinzugefügt wird. Dadurch werden die Elemente, die sich bisher an Position zwei oder dahinter befunden haben, um einen Platz nach hinten geschoben. Unter Verwendung der Standardkonfiguration würde Hibernate die Reihenfolge der Produkte nicht persistieren, und das in Listing 4 gezeigte Beispiel würde ausschließlich zwei SQL-INSERT-Operationen auslösen. Da in der persistence.xml der Parameter default_list_semantics auf LIST konfiguriert wurde (Listing 3), müssen nun die Beziehungsdatensätze aller Elemente aktualisiert werden, deren Position durch das neue Produkt verändert wurde (Listing 5). Je nach Anzahl der betroffenen Listenelemente kann das zu einem deutlichen Mehraufwand führen, der die Performanz der Anwendung negativ beeinflusst. Dieselbe Problematik besteht für alle Löschoperationen, die nicht den letzten Listeneintrag betreffen.

// Bestehende PurchaseOrder lesen
// PurchaseOrder order = em.find(PurchaseOrder.class, order.getId());
 
// Ein neues Produkt an 2. Stelle einfügen
Product prod = new Product("Product 4", 2.00);
prod.getOrders().add(order);
order.getProducts().add(1, prod);
em.persist(prod);
16:58:54,095 DEBUG [org.hibernate.SQL] - insert into Product (name, price, version, id) values (?, ?, ?, ?)
16:58:54,098 DEBUG [org.hibernate.SQL] - update PurchaseOrder set customer=?, version=? where id=? and version=?
16:58:54,104 DEBUG [org.hibernate.SQL] - update PurchaseOrder_Product set products_id=? where orders_id=? and products_ORDER=?
16:58:54,107 DEBUG [org.hibernate.SQL] - update PurchaseOrder_Product set products_id=? where orders_id=? and products_ORDER=?
16:58:54,110 DEBUG [org.hibernate.SQL] - insert into PurchaseOrder_Product (orders_id, products_ORDER, products_id) values (?, ?, ?)

Um diese zusätzlichen Operationen so weit wie möglich zu vermeiden, sollte daher für jede @ManyToMany-Beziehung genau geprüft werden, ob die Speicherung der Reihenfolge erforderlich ist. Wie die Erfahrung aus Projekten mit Hibernate 4 und 5 gezeigt hat, ist das für die meisten Projekte und Beziehungen nicht der Fall. Wenn das auch in der untersuchte Anwendung so ist, sollte auf die Aktivierung des neuen Konfigurationsparameters hibernate.mapping.default_list_semantics verzichtet werden. Wenn die Reihenfolge nur für einzelne Beziehungen persistiert werden soll, kann das mit Hilfe der bereits bekannten Annotation @OrderColumn definiert werden. Der neue Konfigurationsparameter sollte hingegen nur eingesetzt werden, wenn für alle @Many-ToMany-Beziehungen die Reihenfolge der Elemente persistiert werden muss.

Behandlung zeitzonenbehafteter Zeitstempel

Seit mit Java 8 das Date and Time API eingeführt wurde, sind die Klassen OffsetDateTime und ZonedDateTime die offensichtlichsten und am häufigsten verwendeten Typen zur Modellierung eines Zeitstempels mit Zeitzoneninformationen. Und man könnte erwarten, dass die Auswahl eines dieser Typen das Einzige ist, was man tun muss, um einen solchen Zeitstempel in der Datenbank zu persistieren.

Aber leider ist das nicht der Fall, wenn diese Informationen mit Hilfe von JPA in einer relationalen Datenbank abgelegt werden sollen. Obwohl der SQL-Standard den Spaltentyp TIMESTAMP_WITH_TIMEZONE definiert, wird dieser nicht von allen Datenbanken in gleicher Weise unterstützt. Aus diesem Grund unterstützt die JPA-Spezifikation OffsetDateTime und ZonedDateTime nicht als Attributtypen.

 

In Hibernate 5 wurde eine proprietäre Unterstützung eingeführt, bei der der Zeitstempel in die Zeitzone der Java-Anwendung normalisiert und ohne Zeitzoneninformationen gespeichert wird. Das führte zu Pro-blemen, falls sich die Zeitzone der Anwendung änderte bzw. mehrere Java-Anwendungen aus unterschiedlichen Zeitzonen auf dieselbe Datenbank zugriffen. Zeitzonen mit Sommer- und Winterzeit führten ebenfalls zu Problemen.

In Hibernate 6 wurde die Behandlung von Zeitstempeln mit Zeitzoneninformationen flexibler gestaltet und bietet dadurch verschiedene Möglichkeiten, diese Probleme zu vermeiden. Die in Hibernate 5 verwendete Normalisierung wird aber weiterhin unterstützt, sodass bestehende Entitätsabbildungen nicht migriert werden müssen.

Die bevorzugte Abbildung von OffsetDateTime und ZonedDateTime kann in Hibernate 6 entweder global konfiguriert oder mittels Annotation für jede Entitätseigenschaft festgelegt werden. Die globale Konfiguration erfolgt über den Parameter hibernate.timezone.default_storage, der in der persistence.xml-Datei konfiguriert werden kann (Listing 6). Die gültigen Parameterwerte werden dabei durch das TimeZoneStorageType-Enum definiert. Mit Hibernate 6.1.4 stehen die folgenden Werte zur Auswahl:

<persistence>
  <persistence-unit name="my-persistence-unit">
    ...
     <properties>
      <property name="hibernate.timezone.default_storage" value="NORMALIZE"/>
      ...
    </properties>
  </persistence-unit>
</persistence>
  • NATIVE bildet die Zeitstempel auf eine Datenbankspalte vom Typ TIMESTAMP_WITH_TIMEZONE ab. Dieser Spaltentyp muss von der verwendeten Datenbank unterstützt werden.

  • NORMALIZE führt zu einer Normalisierung des Zeitstempels in die Zeitzone der Java-Anwendung oder die durch den Konfigurationsparameter hibernate.jdbc.time_zone definierte Zeitzone. Anschließend wird der Zeitstempel ohne Zeitzoneninformationen in der Datenbank gespeichert. Das entspricht dem Verhalten von Hibernate 5 und wird von Hibernate 6 weiterhin als Standardabbildung verwendet.

  • NORMALIZE_UTC führt zu einer Normalisierung des Zeitstempels in die Zeitzone UTC. Der Vorteil dieser Normalisierung liegt darin, dass sie nicht von der Zeitzone der Java-Anwendung abhängt und nicht über Sommer- und Winterzeit verfügt. Somit werden die vorher genannten Probleme der Normalisierung vermieden.

  • COLUMN bestimmt die Differenz zwischen der Zeitzone des Zeitstempels und UTC. Anschließend werden die Differenz und der nach UTC normalisierte Zeitstempel in getrennten Datenbankspalten gespeichert. Somit wird für den Zeitstempel 1.1.2023 15:00 CET eine Zeitzonendifferenz von +01:00 bestimmt, der Zeitstempel nach 1.1.2023 14:00 normalisiert und die beiden Informationen in zwei getrennten Datenbankspalten gespeichert.

Neben der globalen Konfiguration des Zeitstempels kann die Zeitzonenbehandlung auch für jede Entitätseigenschaft vom Typ OffsetDateTime und ZonedDateTime einzeln definiert werden. Dazu muss die Entitätseigenschaft mit @TimeZoneStorage annotiert und ein Wert des TimezoneStorageType-Enum übergeben werden. Listing 7 zeigt ein Beispiel für eine Abbildung der ZonedDateTime orderDateTime-Eigenschaft auf eine Datenbankspalte vom Typ TIMESTAMP_WITH_TIMEZONE. Dieser Spaltentyp ist von der SQL-Spezifikation zur Speicherung von zeitzonenbehafteten Zeitstempeln vorgesehen. Wenn die verwendete Datenbank diesen Spaltentyp unterstützt, ist das daher die empfohlene Abbildungsart.

@Entity
public class PurchaseOrder {
  
  @TimeZoneStorage(TimeZoneStorageType.NATIVE)
  private ZonedDateTime orderDateTime;
  
  ...
}

Sollte die Datenbank diesen Typ jedoch nicht unterstützen, vermeiden TimeZoneStorageType.NORMALIZE_UTC und TimeZoneStorageType.COLUMN die aus Hibernate 5 bekannten Abbildungsprobleme. TimeZoneStorageType.NORMALIZE_UTC kann auf dem in Listing 7 gezeigten Weg konfiguriert werden. Bei der Verwendung von TimeZoneStorageType.COLUMN kann zusätzlich der Name der Datenbankspalte, in der die Zeitzonendifferenz abgelegt wird, mit Hilfe der Annotation @TimeZoneColumn definiert werden (Listing 8). Sollte der Name der Spalte nicht konfiguriert werden, generiert Hibernate diesen aus dem Namen der Datenbankspalte, auf die der Zeitstempel abgebildet wird, und dem Postfix _tz.

@Entity
public class PurchaseOrder {
  
  @TimeZoneStorage(TimeZoneStorageType.COLUMN)
  @TimeZoneColumn(name = "orderDateTime_timezone")
  private ZonedDateTime orderDateTime;	
  ...
}

Getrennte Interfaces für Lese- und Schreiboperationen

Eine der kleineren Änderungen in Hibernate 6 ist die Einführung der Interfaces MutationQuery und SelectionQuery. Diese ermöglichen die Trennung zwischen Abfragen, die Daten ändern, und solchen, die Daten lesen.

Stay tuned

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

 

Ältere Hibernate-Versionen und die JPA-Spezifikation bilden beide Arten von Abfragen über das Query- und das TypedQuery-Interface ab. Der einzige Unterschied zwischen diesen Interfaces liegt in der strikteren Typisierung des TypedQuery-Interface. Beide Interfaces erweitern ab Hibernate 6 die neu eingeführten SelectionQuery– und MutationQuery-Interfaces und können auch weiterhin verwendet werden. Die beiden neuen Interfaces sind jedoch deutlich übersichtlicher, da sie jeweils nur die Methoden definieren, die mit dem entsprechenden Abfragetyp verwendet werden können. Somit stellt SelectionQuery z. B. die Methoden getResultList, getResultStream und getSingleResult bereit. Die Methoden setFirstResult und setMaxResult sind ebenfalls dem SelectionQuery-Interface vorbehalten. Die Methode executeUpdate hingegen wird ausschließlich vom MutationQuery-Interface definiert.

Da es sich hierbei um zwei Hibernate-spezifische Interfaces handelt, können diese nur über die Methoden createSelectionQuery und createMutationQuery von Hibernates Session-Interface, jedoch nicht über den EntityManager von JPA erzeugt werden. Eine Implementierung des Session-Interface kann man entweder über Hibernates SessionFactory erzeugen oder mit Hilfe der unwrap-Methode aus einer Instanz des EntityManager extrahieren (Listing 9).

EntityManager em = emf.createEntityManager();
Session s = em.unwrap(Session.class);
 
SelectionQuery<Book> q = s.createSelectionQuery("SELECT b FROM Book b WHERE b.title = :title", Book.class);
q.setParameter("title", "Hibernate Tips - More than 70 solutions to common Hibernate problems");
List<Book> books = q.getResultList();

Flexible Instanziierung von Embeddables

Eine weitere Neuerung in Hibernate 6 ist das EmbeddableInstantiator-Interface, mit dem eine Klasse zur Instanziierung von Embeddables implementiert werden kann. Embeddables sind einfache Java-Klassen, mit denen eine Reihe von Attributen definiert werden kann, die Teil von Entitäten werden (Listing 10). Man verwendet sie häufig, um wiederverwendbare Abbildungsinformationen zu erstellen, die in verschiedenen Entitäten verwendet und durch die Geschäftslogik auf dieselbe Art verarbeitet werden.

@Embeddable
public class Address {
 
  private String street;
  private String city;
  private String postalCode;
 
  public String getStreet() {
    return street;
  }
 
  public String getCity() {
    return city;
  }
 
  public String getPostalCode() {
    return postalCode;
  }
  
  public void setStreet(String street) {
    this.street = street;
  }
 
  public void setCity(String city) {
    this.city = city;
  }
 
  public void setPostalCode(String postalCode) {
    this.postalCode = postalCode;
  }
}

Die JPA-Spezifikation und Hibernate bis Version 6.0.0 verlangen, dass jedes Embeddable einen Standardkonstruktor anbietet. Hibernate ruft diesen zur Objektinstanziierung auf, wenn eine Entität mit einem Embeddable aus der Datenbank gelesen wird. Für die Verwendung im eigenen Anwendungscode ist dieser Konstruktor jedoch häufig nicht geeignet. Hier wäre ein Konstruktor, der alle erforderlichen Eigenschaften setzt, oftmals die bessere Wahl.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Seit Hibernate 6.0.0 kann mit Hilfe des Embeddable-Instantiator-Interface eine Klasse erzeugt werden, die Hibernate anstatt des Standardkonstruktors zur Instanziierung eines Embeddables aufruft, womit der Standardkonstruktor überflüssig wird.

public class AddressInstantiator implements EmbeddableInstantiator {
 
  Logger log = LogManager.getLogger(this.getClass().getName());
 
  public boolean isInstance(Object object, SessionFactoryImplementor sessionFactory) {
    return object instanceof Address;
  }
 
  public boolean isSameClass(Object object, SessionFactoryImplementor sessionFactory) {
    return object.getClass().equals( Address.class );
  }
 
  public Object instantiate(ValueAccess valuesAccess, SessionFactoryImplementor sessionFactory) {
    // valuesAccess enthält die Eigenschaftswerte in alphabetischer Reihenfolge der     // Eigenschaftsnamen
    final String city = valuesAccess.getValue(0, String.class);
    final String postalCode = valuesAccess.getValue(1, String.class);
    final String street = valuesAccess.getValue(2, String.class);
    log.info("Instantiate Address embeddable for "+street+" "+postalCode+" "+city);
    return new Address( street, city, postalCode );
  }
 
}

Wie in Listing 11 zu sehen ist, werden für die Implementierung des EmbeddableInstantiator lediglich drei Methoden benötigt. Zur Instanziierung von Embeddable ruft Hibernate die Methode instantiate auf und übergibt ein ValueAccess- und ein SessionFactoryImplementor-Objekt. Das ValueAccess-Objekt enthält alle Eigenschaftswerte des zu instanziierenden Embeddable in alphabetischer Reihenfolge der Eigenschaftsnamen. In dem in Listing 11 gezeigten Beispiel gehört der erste Wert im ValuesAccess-Objekt somit zur Eigenschaft city des AddressEmbeddable, der zweite Wert zur Eigenschaft postalCode und der dritte Wert zur Eigenschaft street. Nachdem diese aus dem ValuesAccess-Objekt extrahiert wurden, können sie zur Instanziierung des Embeddable verwendet werden.

Nachdem das EmbeddableInstantiator-Interface implementiert ist, muss es noch mit dem Embeddable verknüpft werden. Das kann auf dem Embeddable mit Hilfe der EmbeddableInstantiator-Annotation definiert werden. Zusätzlich benötigt das AddressEmbeddable den in der instantiate-Methode aufgerufenen Konstruktor, und der Standardkonstruktor kann entfernt werden (Listing 12). Nachdem diese Änderungen durchgeführt wurden, ruft Hibernate für jede Instanziierung des AddressEmbeddable die Methode initiate der AddressInstantiator-Klasse auf.

@Embeddable
@EmbeddableInstantiator(AddressInstantiator.class)
public class Address {
 
  private String street;
  private String city;
  private String postalCode;
 
  public Address(String street, String city, String postalCode) {
    this.street = street;
    this.city = city;
    this.postalCode = postalCode;
  }
 
  public String getStreet() {
    return street;
  }
 
  public String getCity() {
    return city;
  }
 
  public String getPostalCode() {
    return postalCode;
  }
}

Zusammenfassung

Hibernate 6 bietet neben vielen internen Änderungen auch einige neue Features. Einige der interessantesten wurden in diesem Artikel vorgestellt. Dazu zählen neben neuen Interfaces zur Separierung von lesenden und modifizierenden Abfragen auch die Möglichkeit, für alle als java.util.List modellierten @ManyToMany-Beziehungen die Reihenfolge ihrer Elemente zu persistieren. Das kann nun mit Hilfe des Konfigurationsparameters hibernate.mapping.default_list_semantics global aktiviert werden. Damit kann die initiale Sortierung der Beziehung dauerhaft beibehalten werden. Allerdings erfordert das auch zusätzliche Datenbankoperationen, wenn neue Elemente nicht am Ende der Liste hinzugefügt und andere als das letzte Element der Liste gelöscht werden. Das sollte vor der Konfiguration des neuen Parameters bedacht werden, um das Risiko für daraus entstehende Performanceprobleme frühzeitig bewerten zu können.

Die Abbildung von Zeitstempeln mit Zeitzoneninformationen wurde in Hibernate 6 ebenfalls verbessert. Die in Hibernate 5 verwendete Normalisierung von Zeitstempeln war von einigen Rahmenbedingungen abhängig und führte in der Praxis häufiger zu Problemen. Mit der @TimeZoneStorage-Annotation und dem Konfigurationsparameter hibernate.timezone.default_storage kann nun bestimmt werden, ob der Zeitstempel weiterhin in die Zeitzone der Java-Anwendung oder nach UTC normalisiert oder in einer Datenbankspalte vom Typ TIMESTAMP_WITH_TIMEZONE abgelegt werden soll. Darüber hinaus gibt es die Möglichkeit, die Zeitzonendifferenz zu UTC und den nach UTC normalisierten Zeitstempel in zwei getrennten Datenbankspalten zu speichern. Insbesondere durch die Verwendung einer Datenbankspalte vom Typ TIMESTAMP_WITH_TIMEZONE und die Normalisierung der Zeitstempel nach UTC können die aus Hibernate 5 bekannten Probleme vermieden werden.

Und durch Hibernates neues Interface Embeddable-Instantiator benötigt Version 6 zur Instanziierung eines Embeddable keinen Standardkonstruktor mehr. Stattdessen kann das neue Interface implementiert und mit dem Embeddable verknüpft werden. Dabei gilt es zu beachten, dass das ein Hibernate-spezifisches Feature ist, das nicht von der JPA-Spezifikation und deren anderen Implementierungen unterstützt wird.

Hibernate 6 bietet weitere Neuerungen und Verbesserungen, etwa für die Implementierung von mehrmandantenfähigen Systemen und die Abbildung eigener Datentypen, die in diesem Artikel nicht vorgestellt werden konnten. Weitere Informationen dazu gibt es in der offiziellen Hibernate-Dokumentation und auf dem Blog des Autors.

Stay tuned

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

 

The post Hibernate 6: Die neuen Features vorgestellt (mit Beispielen) appeared first on JAX.

]]>
Mit Ktor und Spring Boot Server- und Clientanwendungen entwickeln https://jax.de/blog/mit-ktor-und-spring-boot-server-und-clientanwendungen-entwickeln/ Wed, 15 Mar 2023 08:17:46 +0000 https://jax.de/?p=88428 Wie können mit Spring Boot und Ktor in Kotlin Server- und Clientanwendungen entwickelt werden? Dieser Artikel begleitet durch das Zusammenspiel von Spring Boot und Ktor. Kotlin eignet sich hervorragend, um Non-blocking-Server- und -Clientanwendungen zu entwickeln. Dafür gibt es mit Ktor sogar ein spezielles Framework. Doch auch der Platzhirsch für Anwendungen im JVM-Ökosystem, Spring Boot, muss sich hier nicht verstecken, sondern kann hervorragend im Zusammenspiel mit Kotlin verwendet werden.

The post Mit Ktor und Spring Boot Server- und Clientanwendungen entwickeln appeared first on JAX.

]]>
Das Kotlin-Ökosystem ist umfangreich. Entwickler JetBrains arbeitet nicht nur an der Sprache selbst, sondern veröffentlicht diverse Frameworks, die speziell für Kotlin entwickelt werden. Ktor [1] ist ein solches Framework, das die Entwicklung von Server- und Clientanwendungen in Kotlin ermöglicht. Ktor sticht vor allem dadurch hervor, dass es auch für native Builds mit Kotlin Native geeignet ist. Das zweite Hauptargument für Ktor ist, dass es ausschließlich non-blocking arbeitet und daher asynchrone Verarbeitung der Standard ist.

Ktor-Server

Der Anfang einer Entwicklung mit Ktor ist dabei so einfach wie bei den meisten anderen aktuellen Frameworks im Umfeld der Java Virtual Machine (JVM). Die Entwickler bieten einen Starter [2], mit dessen Hilfe ein Maven- oder Gradle-Projekt erstellt werden kann. Hierbei können der verwendete Webserver und benötigte Plug-ins direkt ausgewählt werden. Abhängigkeiten zwischen den verschiedenen Komponenten löst der Projektgenerator auf und fügt notwendige, abhängige Plug-ins hinzu, sodass ein lauffähiges Projekt entsteht.

Für ein einfaches „Hello World“-Beispiel verwenden wir Netty als Webserver und die Plug-ins Routing und kotlinx.serialization, um zum einen Routing anhand von Pfaden zu ermöglichen und zum anderen das Kotlin-eigene Serialisierungsmodul zu nutzen. Letzteres fügt als zusätzliches Plug-in noch ContentNegotiation hinzu, das es Ktor ermöglicht, ContentType- und Accept-Header auszuwerten und darauf zu reagieren. Ktor bietet weitere Plug-ins, die für fortgeschrittene Anwendungsfälle geeignet sind, wie zum Beispiel Authentifizierung, Session-Management, LDAP-Anbindung, Template-Engines und WebSocket-Unterstützung, um nur einige Beispiele zu nennen. Die vollständige „Hello World“-Anwendung zeigt Listing 1.

import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
 
fun main() {
  embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
    install(ContentNegotiation) {
      json()
    }
    configureRouting()
  }.start(wait = true)
}
 
fun Application.configureRouting() {
  routing {
    route("/hello") {
      get("/{name?}") {
        val name = call.parameters["name"] ?: "World"
 
        call.respond("Hello $name!")
      }
    }
  }
}

In der main-Methode wird über die Methode embeddedServer die Ktor-Anwendung erzeugt und mit der start-Methode gestartet. Der Parameter wait = true gibt dabei an, dass der Hauptanwendungs-Thread blockiert werden soll, die Anwendung also so lange läuft, bis sie beendet wird.

Stay tuned

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

 

Über die embeddedServer-Methode werden die Server-Engine, der Port und auch die Ktor-Plug-ins konfiguriert. Das ContentNegotiation-Plug-in wird zur Anwendung hinzugefügt und für JSON konfiguriert. Die Konfiguration des Routings könnte ebenso inline erfolgen, wird hier aber in eine Extension Function ausgelagert. Generell kann das Routing in mehrere Extension Functions ausgelagert werden und so eine Gruppierung nach Funktionalität, Pfad oder Ähnlichem erfolgen.

Das Routing selbst geschieht über die Ktor-DSL. Das Beispiel aus Listing 1 konfiguriert die Route / hello/$name, wobei $name ein optionaler Path-Parameter (erkennbar am ?) ist. Mit call.parameters kann auf die Request-Parameter zugegriffen werden, mit call.respond wird die Serverantwort geschrieben. Ein Aufruf von /hello gibt „Hello World!“ zurück, ein Aufruf von / hello/Ktor gibt „Hello Ktor!“ zurück.

Damit ist eine erste, sehr einfache Ktor-Anwendung fertig. Der Non-blocking-Aspekt von Ktor scheint in diesem simplen Beispiel verborgen zu sein, ist aber tatsächlich schon vorhanden. Die Signaturen der Methoden call.parameters und call.respond haben jeweils das Schlüsselwort suspend in ihrer Deklaration, was bedeutet, dass sie auf besondere Weise ausgeführt werden. Zunächst jedoch ein Blick darauf, wofür Non-Blocking relevant ist.

Non-blocking Serveranwendungen

Webserver stellen eine Menge Threads zur Verfügung. Jeder dieser Threads verarbeitet dabei einen Request nach dem anderen. Die Anzahl paralleler Requests ist also durch die Zahl der Threads limitiert. Komplexere Anwendungen können jedoch eine Anfrage oftmals nicht ohne externe Kommunikation verarbeiten. Externe Zugriffe sind nötig, zum Beispiel auf eine Datenbank, das Dateisystem oder auf externe HTTP-APIs. Beim Warten auf diese externen Zugriffe wird der Webserver-Thread blockiert. Der Skalierung der Webserver-Threads sind dabei schnell Grenzen gesetzt, da Threads generell ressourcenintensiv sind und der Bedarf an Arbeitsspeicher schnell ansteigt. Optimierungen im Code selbst, die zum Beispiel Zugriffe auf Datenbanken oder externe APIs via Threads parallelisieren, bringen nicht nur das Ressourcenproblem mit, sondern erhöhen zusätzlich die Komplexität des Codes signifikant, da die Threads synchronisiert werden müssen. Auch wird dabei nicht das Problem gelöst, dass der Webserver-Thread blockiert werden muss, um auf das Beenden aller gestarteten Threads zu warten.

Ein häufig von Frameworks angebotener Lösungsansatz hierfür ist Reactive Programming. Dabei wird durch ein entsprechendes Framework, wie zum Beispiel Project Reactor [3], das Scheduling übernommen. Statt zu blockieren, wird dabei der Thread genutzt, um weitere Anfragen zu verarbeiten. So ist ein höherer Parallelisierungsgrad möglich, ohne zusätzliche Threads zu benötigen. Mit Reactive Programming ist jedoch eine steile Lernkurve verbunden. Das Programmiermodell unterscheidet sich grundlegend von der imperativen Programmierung, da es für fast jedes Problem spezielle Operatoren gibt.

Ktor nutzt für die parallelisierte Verarbeitung einen anderen Ansatz, der es erlaubt, den imperativen Programmierstil beizubehalten: Kotlin Coroutines.

Kotlin Coroutines

Koroutinen [4] sind spezielle Funktionen, die unterbrochen und wieder fortgesetzt werden können. Kotlin unterstützt Koroutinen über eine Core Library namens Kotlin Coroutines [5]. Den Kern von Kotlin Coroutines bilden suspendierbare Funktionen, die mit dem Schlüsselwort suspend fun deklariert werden. Diese können nur innerhalb eines Coroutine Scopes ausgeführt werden. Das einfache Beispiel in Listing 2 startet einen Coroutine Scope mit der Methode runBlocking. Durch diese Methode wird der Hauptthread pausiert, bis alle gestarteten Koroutinen abgeschlossen sind. Mittels launch werden innerhalb einer for-Schleife 1 000 000 Koroutinen gestartet, die jeweils zehn Sekunden pausieren und anschließend einen Wert ausgeben. Die Ausführung zeigt, dass nach einer initialen Verzögerung von zehn Sekunden alle Zahlen kurz nacheinander ausgegeben werden. Die Ausführung – und damit auch die zehn Sekunden Wartezeit – wird also parallelisiert.

fun main() {
  runBlocking {
    for (i in 0..1_000_000) {
      launch { printIn10Seconds(i) }
    }
  }
}
 
suspend fun printIn10Seconds(value: Int) {
  delay(10_000)
  println(value)
}

Listing 3 zeigt denselben Code mit Threads. Dieser läuft (auf durchschnittlicher Hardware) spürbar weniger performant, da er deutlich mehr Systemressourcen benötigt.

fun main() {
  val threads = mutableListOf<Thread>()
  for (i in 0..1_000_000) {
    val thread = Thread { printIn10Seconds(i) }
    threads.add(thread)
    thread.start()
  }
  threads.forEach { it.join() }
}
 
fun printIn10Seconds(value: Int) {
  Thread.sleep(10_000)
  println(value)
}

suspend funs können nur innerhalb eines Coroutine Scopes ausgeführt werden. Das bedeutet in unserem initialen Ktor-Beispiel, das bereits zwei suspend funs benutzt hat, dass Ktor den gesamten Code in einem Coroutine Scope ausführt. Konkret startet Ktor für jeden Request eine eigene Koroutine und ermöglicht somit eine deutlich höhere Parallelisierung, ohne dass der Entwickler sich umstellen oder irgendetwas beachten muss. Jedoch bleibt ihm die Flexibilität erhalten, selbst suspendierbare Funktionen aufzurufen.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Ktor-Client

Ktor bietet nicht nur eine einfache Möglichkeit, eine non-blocking Serveranwendung zu implementieren. Auch non-blocking Clientanwendungen sind möglich. Dass hierbei auch die Clients konsequent mit Hilfe von Coroutines implementiert werden, bringt auch hier den Vorteil, dass der Thread, der die Requests ausführt, nicht blockiert werden muss, während auf den Server gewartet wird, sondern weiterverwendet werden kann.

Als Beispiel wollen wir unsere „Hello World“-Anwendung erweitern, sodass sie User, die bei einem anderen Service via ID abgefragt werden, mit Namen begrüßen kann.

Hierfür benötigen wir weitere Abhängigkeiten, die wir in unsere Build-Konfiguration (Maven oder Gradle) aufnehmen: io.ktor:ktor-client-core, io.ktor:ktor-client-cio und io.ktor:ktor-client-content-negotiation. Den Client implementieren wir so, wie in Listing 4 dargestellt.

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
 
class UserService(
  val client: HttpClient = HttpClient(CIO) {
    expectSuccess = true
    install(ContentNegotiation) {
      json()
    }
  }
) {
 
  suspend fun findUser(userId: String): User =
    client.get("http://localhost:8181/users/$userId").body<User>()
}
 
@Serializable
data class User(
  val id: String,
  val name: String,
)

Wir erzeugen zunächst einen HTTP-Client, der JSON Accept-Header verschickt und JSON-Antworten deserialisieren kann. Außerdem konfigurieren wir ihn via expectSuccess = true so, dass alle nicht 2xx Response Codes zu einer Exception führen.

Die Methode findUser ruft ein HTTP API auf und deserialisiert die JSON-Antwort in ein User-Objekt. Dieses wird mit @Serializeable für das Kotlin-Serialisierungsmodul (kotlinx.serializable) serialisierbar gemacht. client.get ist dabei eine suspend fun, also muss entweder die Methode findUser ebenfalls eine suspend fun sein oder explizit einen Coroutine Scope starten. Da wir die Methode aus dem Ktor-Server heraus nutzen wollen, der bereits einen Coroutine Scope zur Verfügung stellt, definieren wir die Methode als suspend fun. Listing 5 zeigt die Integration des Clients in den Server.

fun Application.configureRouting() {
  routing {
    route("/hello") {
      get("/{name?}") {
        val name = call.parameters["name"] ?: "World"
 
        call.respond("Hello $name!")
      }
      get("/user/{userId}") {
        try {
          val user = userService.findUser(call.parameters.getOrFail("userId"))
          call.respond("Hello ${user.name}!")
        } catch (e: ClientRequestException) {
          if (e.response.status == HttpStatusCode.NotFound) {
            call.respond(HttpStatusCode.BadRequest)
          } else {
            call.respond(HttpStatusCode.InternalServerError)
          }
        }
      }
    }
  }
}

Hierfür wird die Funktion configureRouting erweitert. Wir definieren eine zusätzliche Route als GET Request auf /hello/user/$userId. Ein Aufruf versucht, die übermittelte User-ID aufzulösen und den Namen auszulesen. Wird der User nicht gefunden, wird ein passender Fehler zurückgegeben. Der zugehörige Server ist in Listing 6 dargestellt.

val userService = UserService()
 
fun main() {
  embeddedServer(Netty, port = 8181, host = "0.0.0.0") {
    install(ContentNegotiation) {
      json()
    }
    configureRouting()
  }.start(wait = true)
}
 
fun Application.configureRouting() {
  routing {
    route("/users") {
      get() {
        call.respond(
          userService.users
            .map { User(id = it.key, name = it.value) }
        )
      }
      get("/{id}") {
        val id = call.parameters.getOrFail("id")
        try {
          val name = userService.users.getValue(id)
 
          call.respond(User(id = id, name = name))
        } catch (_: NoSuchElementException) {
          call.respond(HttpStatusCode.NotFound)
        }
 
      }
      post("/{name}") {
      call.respond(userService.createUser(call.parameters.getOrFail("name")))
        }
    }
  }
}
 
@Serializable
data class User(
  val id: String,
  val name: String,
)
 
class UserService(
  val users: MutableMap<String, String> = mutableMapOf(),
) {
  fun createUser(
    name: String,
  ): String {
    val id = "${UUID.randomUUID()}"
    users[id] = name
 
    return id
  }
}

Neben GET Requests wird auch ein POST Request definiert. Außerdem können @Serializable-Klassen von Ktor ebenfalls zu JSON serialisiert werden, wenn diese als Antwort zurückgegeben werden sollen.

Mit Ktor ist es, wie die Beispiele zeigen, sehr einfach möglich, HTTP-Kommunikation zu implementieren. Diese ist standardmäßig non-blocking und damit sehr ressourceneffizient umgesetzt.

Stay tuned

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

 

Kotlin Coroutines und Spring Boot

Den großen Vorteil von Kotlin Coroutines, einfach parallelisierbaren Code zu entwickeln, nutzt auch das am weitesten verbreitete Framework für die JVM, Spring Boot. Mit Version 5 hat das Spring Framework Unterstützung für non-blocking Verarbeitung der Requests erhalten: Spring Webflux [6]. Während Java-Entwickler hier auf den Reactive Programming Stack von Project Reactor angewiesen sind und damit ihre Entwicklungspattern umstellen müssen, können Kotlin-Entwickler ihr gewohntes Entwicklungspattern beibehalten, da jegliche Parallelität auch über Kotlin Coroutines abgebildet werden kann. Verantwortlich hierfür ist das Kotlin-Modul org.jetbrains.kotlinx:kotlinx-coroutines-reactor. Es bindet Kotlin Coroutines an Project Reactor an.

Der Einstieg in die Entwicklung ist dabei genauso einfach wie bei jedem anderen Spring-Projekt. Über den Spring Boot Starter [7] wird das Projekt konfiguriert. Statt der Abhängigkeit Spring Web wird Spring Reactive Web verwendet. Die Anbindung von Kotlin Coroutines an Project Reactor wird für Kotlin-Projekte automatisch hinzugefügt.

Die Implementierung erfolgt dann fast wie gewohnt (Listing 7). Der einzige Unterschied ist die Methodensignatur der Methode hello. Diese ist als suspend fun, also als Koroutine deklariert. Ist eine Methode mit RequestMapping als suspend fun definiert, so startet Spring automatisch einen Coroutine Scope für die Ausführung des Requests.

@RestController
@RequestMapping("/hello")
class HelloController(
  val userService: UserService
) {
 
  @GetMapping(value = ["" ,"/{name}"])
  suspend fun hello(
    @PathVariable(required = false) name: String?
  ) =
    "Hello ${name ?: "World"}!"
}

Der Coroutine Scope ermöglicht es auch, weitere suspend funs aufzurufen. Das ist insbesondere dann von Vorteil, wenn auch die aufgerufenen Funktionen non-blocking implementiert sind. Das ist zum Beispiel beim Spring WebClient der Fall. Dieser basiert auf dem Reactive Stack von Project Reactor und kann auch mit Kotlin Coroutines verwendet werden. Der UserService aus dem Ktor-Beispiel (Listing 4) kann mit dem Spring WebClient implementiert werden, wie in Listing 8 gezeigt.

@Service
class UserService(
  val webClient: WebClient = WebClient.create()
) {
  suspend fun findUser(userId: String): User {
    return webClient.get().uri("http://localhost:8181/users/$userId")
      .accept(APPLICATION_JSON)
      .awaitExchange {
        if (it.statusCode() == HttpStatus.NOT_FOUND) {
          throw UserIdNotFoundException()
        }
        it.awaitBody<User>()
      }
  }
}
 
data class User(
  val id: String,
  val name: String,
)
 
class UserIdNotFoundException: Exception()

Der Controller aus Listing 7 kann dann um den Code aus Listing 9 erweitert werden, sodass dieselbe Funktionalität entsteht wie im Ktor-Beispiel aus Listing 5.

@GetMapping("/user/{userId}")
suspend fun helloUser(
  @PathVariable userId: String
) =
    "Hello ${userService.findUser(userId).name}!"
 
@ExceptionHandler(UserIdNotFoundException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
suspend fun handleUserIdNotFoundException(e: UserIdNotFoundException) { }

Auch mit Spring Boot ist es sehr einfach möglich, die Vorteile von Kotlin Coroutines zur besseren Parallelisierung zu nutzen.

 

Fazit: Spring Boot und Ktor in Kotlin

Kotlin bietet mit Kotlin Coroutines eine sehr einfache Möglichkeit, parallelen Code zu schreiben, ohne die damit verbundene Komplexität explizit selbst verwalten zu müssen. Außerdem benötigen Kotlin Coroutines deutlich weniger Ressourcen als Threads. Coroutines haben außerdem eine sehr geringe Einstiegshürde, da die gewohnten Entwicklungspattern beibehalten werden können.

Basierend auf Kotlin Coroutines können Client- und Serveranwendungen sehr einfach entwickelt werden. Hierfür gibt es mit Ktor ein Kotlin-natives Framework, das einen unkomplizierten Einstieg ermöglicht. Ktor ist dabei ein kleines Framework dessen Funktionsumfang auf das limitiert ist, was zur Entwicklung eines Servers oder Clients benötigt wird. Dafür ist eine Einbindung in Kotlin Native möglich, die Build-Artefakte bleiben kompakt und es kann einfacher mit anderen Libraries oder Frameworks kombiniert werden. Außerdem gibt es die non-blocking Verwendung der Webserver-Threads automatisch dazu.

Non-blocking Anwendungen können aber auch über Spring Boot mit Kotlin Coroutines entwickelt werden. Hier muss man sich allerdings explizit für den Non-blocking-Ansatz entscheiden. Im Gegenzug gibt es das komplette Spring-Ökosystem dazu: Dependency Injection, Spring Data und vieles mehr.


Links & Literatur

[1] https://ktor.io/

[2] https://start.ktor.io/

[3] https://projectreactor.io/

[4] https://de.wikipedia.org/wiki/Koroutine

[5] https://kotlinlang.org/docs/coroutines-guide.html

[6] https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html

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

The post Mit Ktor und Spring Boot Server- und Clientanwendungen entwickeln appeared first on JAX.

]]>
Welche Möglichkeiten bietet GraalVM in Spring? https://jax.de/blog/welche-moeglichkeiten-bietet-graalvm-in-spring/ Mon, 20 Feb 2023 15:25:14 +0000 https://jax.de/?p=88357 Dieser Artikel behandelt GraalVM und ihre Möglichkeiten bzw. Integration in Spring. Wir starten mit einer kleinen Historie, um zu verstehen, warum es Graal überhaupt gibt bzw. woher es kommt. Danach sehen wir uns die Funktionalitäten von Graal für Java im Allgemeinen an und danach, inwiefern Spring 6 bzw. Spring Boot 3 Unterstützung für GraalVM anbietet.

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

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

Historie: GraalVM

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

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

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

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

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

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

Stay tuned

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

 

Das Framework “Truffle” für die GraalVM

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

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

Abb. 1: GraalVM-Architektur mit Truffle

Native Image in der GraalVM

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

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

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

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Set-up GraalVM

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

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

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

Abb. 2: Downloadbildschirm

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

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

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

Abb. 3: Ausführung OpenJDK

Abb. 4: Ausführung GraalVM

Native Image allgemein

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

Installation und Ausführung

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

gu install native-image

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

javac Main
native-image Main

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

./main

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

Abb. 5: Ausführung Native Image

Einschränkungen durch Closed-World-Ansatz

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

Stay tuned

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

 

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

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

Danach kompilieren wir nochmals Main und erstellen das Native Image:

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

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

native-image Main --no-fallback

Nach Neuausführung bekommen wir danach eine altbekannte Fehlermeldung:

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

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

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

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

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

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

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

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

Metadata bei großen Projekten

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

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

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

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

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

 

Zusammenfassung zu Native Images

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

Native Image in Spring

Spring Ahead-of-Time

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

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

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

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

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

Abb. 6: Graal Native Image mit Spring

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

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

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

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

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

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

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

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

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

Native Image als Docker Image

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

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

Stay tuned

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

 

Closed-World in Spring

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

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

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

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

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

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

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

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

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

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

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

Spring und Metadata

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

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

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

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

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

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

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Testen von Native Image mit Spring Boot

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

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

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

./gradlew nativeTest

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

Abschluss: GraalVM in Spring

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

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

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

Stay tuned

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

 


Links & Literatur

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

]]>
Quo vadis Jakarta EE? https://jax.de/blog/quo-vadis-jakarta-ee/ Tue, 23 Aug 2022 14:16:02 +0000 https://jax.de/?p=87299 Proprietäre Frameworks wie das Spring Framework haben einen entscheidenden Vorteil gegenüber dem JEE-Standard: Mit klaren Deprecation- und Migrationsstrategien können regelmäßig bei Major Releases alte APIs und ungenutzte Funktionalitäten entfernt werden. Dadurch kann die Software immer State of the Art bleiben. Java EE hingegen hatte immer das Ziel der Abwärtskompatibilität, um Benutzer:innen zu garantieren, dass auch alte Applikationen weiterhin funktionieren.

The post Quo vadis Jakarta EE? appeared first on JAX.

]]>
Dieser Umstand stellt einen zusätzlichen Vorteil dar, wenn es um Investitionssicherheit geht. Es wurde in Kauf genommen, dass dadurch veraltete Technologien weitergeführt werden mussten. Nach ersten kleineren Versuchen, in der Vergangenheit dieses Vorgehen zu ändern, setzt man jetzt mit Jakarta EE 10 zum großen Wurf an.

Totgesagte leben bekanntlich länger. Speziell im Fall von JEE hatte man sogar mehrfach das Gefühl, dass die Totsagungen eher zu einer deutlichen Trotzreaktion führten. Das letzte Mal wurde JEE 2017 totgesagt, als Oracle verkündete, Java EE nicht mehr (selbst) fortführen zu wollen. Es folgte das letzte Release unter dem Oracle-Mantel (Java EE 8) und die Übergabe an die Eclipse-Foundation, verbunden mit einem Streit über die Package-Namen, der damit endete, dass die javax Packages alle in Jakarta umbenannt wurden. 2019 war die Übergabe an die Eclipse Foundation abgeschlossen. Es folgte das erste Jakarta-Release, das völlig identisch zu Java EE 8 war. Dementsprechend wurden die alten Package-Namen übernommen. Jakarta EE 9 wurde dann komplett dazu genutzt, die Umbenennung der Packages durchzuführen. Es wurde 2020 releast. Selbst das nächste Release (Jakarta EE 9.1), das nur die Umstellung von Java 8 auf Java 11 beinhaltete, brachte keine (echten) neuen Features.

Stay tuned

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

 

In Summe lässt sich also sagen: Seit 2017, was immerhin fünf Jahre her ist, wurden hinsichtlich der Funktionalität keine maßgebenden Veränderungen in EE realisiert. Selbst das Update auf Java 11 ist mittlerweile veraltet. Das aktuelle Long Term Support Release ist Java 17. Umso spannender ist es, zu beobachten, was sich inhaltlich in EE 10 getan hat.

Entfernen alternativer Komponentenmodelle

Bevor CDI mit Java EE 6 eingeführt wurde, war EJB das übergreifende Komponentenmodell von Java EE (und zuvor von J2EE). Der Umstand, dass EJB ein so schwergewichtiges Komponentenmodell war, verdeutlichte, dass nicht alles, was man in JEE gerne als Komponente modelliert hätte, eine EJB sein konnte.

Das führte dazu, dass (nahezu) jede der einzelnen EE-Spezifikationen ein eigenes, spezialisiertes (und leichtgewichtiges) Komponentenmodell als Ergänzung zu EJBs entwickelte. Dies begann bei JSF Managed Beans, ging über JPA-Entitäten bis hin zu mit @Provider annotierten Komponenten in JAX-RS.

Die unterschiedlichen Komponentenmodelle waren von Anfang an eine Herausforderung, wenn es um die Integration der verschiedenen Spezifikationen miteinander ging. Am deutlichsten wird das bei der Securityspezifikation. Je nachdem, in welcher Spezifikation man unterwegs ist, gibt es einen anderen Weg, um herauszubekommen, wer der/die aktuelle Benutzer:in ist und ob er/sie sich in einer bestimmten Rolle befindet (z. B. Http-ServletRequest#getUserPrincipal und HttpServlet-Request#isUserInRole, EjbContext#getCallerPrincipal und EjbContext#isCallerInRole, SecurityContext in JAX-RS und weitere Optionen).

Als CDI mit Java EE 6 kam, war es schnell das vorherrschende Komponentenmodell und wurde nach und nach in nahezu allen anderen EE-Spezifikationen neben dem eigenen Komponentenmodell genutzt. Hier zeigte sich allerdings der Nachteil der strikten Policy der Abwärtskompatibilität von JEE-Spezifikationen: Man wird die spezifischen Komponentenmodelle der einzelnen Specs nicht mehr so ohne Weiteres los. Folglich hatte man anstatt eines übergreifenden Komponentenmodells für ganz Java EE nur ein weiteres Komponentenmodell neben den spezifischen Modellen der Einzelspezifikationen.

Den ersten Schritt ging die EJB-Spec mit den EJB Entity Beans, die schon in Java EE 6 als „pruned“ markiert wurden. In der Praxis waren sie längst durch JPA Entities ersetzt worden. Zu diesem Zeitpunkt wurden sie allerdings noch nicht aus der Spec entfernt. Gleichermaßen stellte die JSF-Spec ziemlich schnell fest, dass lieber CDI Beans als JSF Managed Beans verwendet werden sollten. Mit JSF 2.2 wurden daher für alle JSF Scopes auch CDI Scopes zur Verfügung gestellt.

Wenngleich es diese einzelnen Versuche gab, alternative Komponentenmodelle (zumindest teilweise) loszuwerden, dauerte es jetzt, bis Jakarta EE 10 CDI zum offiziellen Komponentenmodell von JEE machte. Das zeigt sich jetzt dadurch, dass alle Spezifikationen Überlegungen anstellen, wie sie das eigene Komponentenmodell zugunsten von CDI loswerden können. Bei den meisten Spezifikationen erfolgt dies, indem bestehende Annotations entweder deprecated werden oder (z. B. im Fall von JSF), wenn sie bereits deprecated waren, komplett entfernt werden.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Spezialfall EJB

Die schwergewichtigen EJBs waren, wie bereits erwähnt, der Grund für die vielen spezifischen Komponentenlösungen. Gleichzeitig beinhalteten EJBs aber auch viel Funktionalität, die man ab und an benötigt und die anderweitig im EE-Standard nicht verfügbar war. Dazu gehör(t)en @Startup, @Schedule, @Roles-Allowed, @TransactionAttribute, @Asynchronous und @Lock. Um ein leichtgewichtiges Komponentenmodell wie CDI als Ersatz für EJB zu etablieren, wird folglich auch Ersatz für diese Funktionalität benötigt. Bei einigen der Annotations (@RolesAllowed, @Transactional) ist dies bereits in der Vergangenheit geschehen. Seit der Herausgabe von Jakarta EE 10 soll diese Lücke jetzt komplett geschlossen werden und mit @Asynchronous ist damit bereits begonnen worden.

Ziel ist hier offenkundig eine Ablösung des schwergewichtigen EJB-Standards zugunsten von leichtgewichtigeren CDI Beans, die dann von Fall zu Fall mit der benötigten Funktionalität angereichert werden können.

Core Profile

Sicherlich wird es kurzfristig nicht passieren, dass der EJB-Standard komplett aus der Jakarta-EE-Spezifikation entfernt wird. Dennoch ist es schon länger das Ziel, es auch neuen Application-Servern zu ermöglichen, eine Enterprise-Server-Zertifizierung zu erhalten, ohne den kompletten Funktionsumfang von EE implementieren zu müssen. Bereits in Java EE 6 wurde dafür das Konzept der Profile und als erstes Beispiel dafür auch das Web Profile eingeführt. Die Idee war es, Servern zu ermöglichen, moderne Laufzeitumgebungen zu entwickeln, die Java-EE-zertifiziert werden können, aber dennoch nicht den Ballast alter Spezifikationen mitschleppen müssen.

Allerdings enthielt gleichermaßen das Web Profile damals noch einige Spezifikationen, die man aus heutiger Sicht nicht in einem modernen Server erwarten würde. Dazu gehörten z. B. JSP und eine leichtgewichtige Version von EJB. Aus heutiger Sicht darf JSF ebenfalls maximal als optional in der Webentwicklung angesehen werden, SPAs haben dem Standard längst den Rang abgelaufen. Teil des Web Profile ist JSF aber dennoch.

Mit Jakarta EE 10 soll nun ein weiteres Profil dazukommen: das Core Profile. Die Idee des Core Profile ist, die wirkliche Kernfunktionalität eines Application-Servers dort zu bündeln, um so z. B. auch Microservices-Entwicklung mit einem EE-zertifizierten Server zu ermöglichen. Geplant ist aktuell eine Bündelung von CDI, JSON-B und JAX-RS.

Das Core Profile ist allerdings noch nicht finalisiert und zudem ist noch offen, ob eher mehr (Was ist z. B. mit Messaging oder JPA?) oder doch weniger Technologien (Wenn ich einen komplett Message-basierten Microservice habe, brauche ich kein REST Framework.) darin landen.

 

Fazit

Jakarta EE 10 ist das erste Release unter dem Namen Jakarta EE, in dem echte Weiterentwicklung stattgefunden hat. Am interessantesten dürfte dabei die Einführung eines neuen Profils, des Core Profile, sein. Mit ihm besteht die Möglichkeit, minimale EE-Server dahingehend zu spezifizieren, wie sie in Microservices-Landschaften eingesetzt werden können.

Des Weiteren sind die Änderungen im Bereich der Komponentenmodelle spannend. CDI soll das allgemeingültige Komponentenmodell aller EE-Spezifikationen werden. Das bedeutet, dass die vielen verschiedenen Komponentenmodelle der einzelnen Spezifikationen Schritt für Schritt zunächst deprecated und dann entfernt werden sollen. Dazu zählen EJBs genauso wie JSF Managed Beans und auch alle Objekte, die in JAX-RS mit @Context injiziert werden können. Insbesondere bei Letzteren zeigt sich jedoch die Herausforderung des Vorhabens. Bei JSF dürfte es kaum noch Altanwendungen geben, die auf JSF Managed Beans (und nicht auf CDI) basieren (was auch daran liegen könnte, dass es überhaupt noch sehr wenige JSF-Anwendungen gibt).

Bei JAX-RS sieht die Sache anders aus: Diese allgemein verbreitete Spezifikation wird noch von vielen Anwendungen genutzt. Die Deprecation und somit auch das spätere Entfernen betreffen hier demzufolge viele Anwendungen. Im Vergleich zu JSF kommt erschwerend hinzu, dass der Zeitraum zwischen Deprecation und Entfernen recht kurz werden könnte, da die Releasezyklen von Jakarta EE deutlich verkleinert werden sollen.

Der Spagat ist hier, einerseits dafür zu sorgen, dass die Spezifikation dem aktuellen Stand der Entwicklung entspricht, und zudem, wie aus einem Guss wirkt (eben mit einem gemeinsamen Komponentenmodell), und andererseits das hohe Gut des langfristigen Supports alter Applikationen aufrecht zu erhalten. Wenn hier bei der Spezifikation nicht aufgepasst wird, droht eine der Stärken, die Java EE noch ausgezeichnet haben, in Jakarta EE verloren zu gehen. Das ist die Abwärtskompatibilität und damit einhergehend die Investitionssicherheit.

Mit Jakarta EE ist das noch nicht der Fall. Vielmehr gelingt der Spagat aktuell gut. Es gibt aber Kräfte, denen die Erneuerung und das Abschneiden alter Zöpfe nicht schnell genug gehen. Es bleibt also spannend, welchen Weg Jakarta EE in Zukunft geht, insbesondere da kurze Releasezyklen angestrebt werden.

In diesem Sinne – Stay tuned.

 

Links & Literatur

[1] https://jakarta.ee

[2] https://blogs.oracle.com/javamagazine/post/java-for-the-enterprise-what-to-expect-in-jakarta-ee-10

[3] https://newsroom.eclipse.org/eclipse-newsletter/2022/april/top-5-new-features-coming-jakarta-ee-10

The post Quo vadis Jakarta EE? appeared first on JAX.

]]>
Die Untiefen reaktiver Programmierung https://jax.de/blog/die-untiefen-reaktiver-programmierung/ Tue, 22 Feb 2022 11:40:38 +0000 https://jax.de/?p=85630 Vor acht Jahren wurde das Reactive Manifesto [1] veröffentlicht, und ganz allmählich beginnt die reaktive Programmierung auch in der Java-Welt Fuß zu fassen. Deswegen lohnt es sich, die Erfahrungen und Best Practices aus einer anderen Community anzuschauen. Angular hat von Anfang an sehr stark auf RxJS gesetzt und sich damit eine ganze Menge Komplexität eingehandelt. Wie kann man sie beherrschbar machen und was können Java-Entwickler daraus lernen?

The post Die Untiefen reaktiver Programmierung appeared first on JAX.

]]>
Haben Sie sich schon einmal gefragt, warum Quarkus oder Netty so schnell sind? Bei Quarkus könnte die Antwort GraalVM lauten, und das wäre sogar richtig. Aber wie sieht es mit Netty aus? Oder Vert.x? Oder warum fühlt sich ein modernes UI mit Angular oder React.js flotter an als die meisten JSF- oder Spring-MVC-Anwendungen? Die Gemeinsamkeit ist die non-blocking IO. Dahinter steckt die Erkenntnis, dass unser Computer die meiste Zeit mit Warten verbringt. Unsere modernen Gigahertz-CPUs können ihr Potenzial gar nicht ausspielen. Kurze Sprints werden abgelöst von schier endlosen Wartezeiten. Irgendwo ist immer ein Stau. Jeder Datenbankzugriff bedeutet mehrere Millisekunden Pause, jeder Zugriff auf ein REST-Backend dauert oft 10 bis 100 Millisekunden, und das summiert sich schnell auf mehrere Sekunden. Die Idee der reaktiven Programmierung ist, die Wartezeiten sinnvoll zu nutzen. Dafür zerschneidet man einen Algorithmus in zwei Teile. Alles, was nach dem REST-Call kommt, wird in eine eigene Funktion ausgelagert. Euer Framework ruft diese Callback-Funktion auf, wenn die Daten angekommen sind. In der Zwischenzeit kann die CPU andere Aufgaben erledigen. Sie kann zum Beispiel einfach mit der nächsten Zeile weitermachen. Beim traditionellen, blockierenden Programmiermodell definiert die nächste Zeile, was wir mit dem Ergebnis der REST-Calls machen wollen. Aber das ist jetzt nicht mehr der Fall. Dieser Teil des Algorithmus wurde ja in die Callback-Funktion verschoben. Es gibt also keinen Grund mehr, mit der Abarbeitung der nächsten Zeile zu warten. Damit können sehr viele Performanceprobleme gelöst werden. Solche Callback-Funktionen sind erst seit Java 8 sinnvoll möglich, als die Lambdafunktionen eingeführt wurden. Vorher konnte man sie mit anonymen inneren Klassen simulieren, aber das war so unattraktiv, dass es kaum jemand gemacht hat. Dadurch erklärt sich, warum reaktive Frameworks wie Quarkus oder Spring Reactor erst seit relativ kurzer Zeit populär werden. Die JavaScript-Welt hat hier einige Jahre Vorsprung, und deswegen soll jetzt RxJS betrachtet werden.

Nichtlineare Programmierung mit RxJS

Auf den nächsten Seiten bewegen wir uns ausschließlich im JavaScript Frontend. Das hat einige Konsequenzen. JavaScript kennt kein Multi-Threading. Das macht aber nichts, es muss ja nur noch ein einziger Anwender bedient werden. Wir reden der Einfachheit halber jetzt auch nur noch über REST-Calls.

Es wird also ein REST-Call zum Backend geschickt. Die Idee ist, die Wartezeit, bis die Antwort eintrudelt, zu nutzen. Das bedeutet, dass sofort mit der nächsten Zeile im Programmcode weitergemacht werden kann. Erfahrene RxJS-Entwickler werden jetzt mit den Achseln zucken und „Ja, und?“ sagen. Für Neueinsteiger ist das aber ein erhebliches Problem. Die Reihenfolge, in der der Programmcode ausgeführt wird, ist nicht mehr linear. In vielen Fällen ist er auch nicht mehr deterministisch. Schauen wir uns ein einfaches Beispiel an (Listing 1).

Stay tuned

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

 

Alles neu?

Wir sind alle Entwickler und Technologieenthusiasten und daher kommen wir sehr schnell zu der Überzeugung, dass mit Framework X oder Technologie Y alle Probleme schnell gelöst werden und damit auch ganz neuen Herausforderungen und Anforderungen leicht entsprochen werden kann. Einige wollen auch dafür eigene Technologien entwickeln, was gar nicht mal ungewöhnlich ist – man liest und hört nur sehr wenig von diesen Ansätzen. Die meisten Seniorentwickler schreiben die gleiche Art von Anwendung oder lösen die gleiche Art von Problemen Tag für Tag, Jahr um Jahr. Das ist manchmal langweilig, und was gibt es Spannenderes, als ein eigenes Framework zu schreiben? Vielleicht sogar mit der Absicht, es danach als Open-Source-Software zu veröffentlichen? Die Antwort ist: Wahrscheinlich ist es superspannend, aber bringt es unsere Anwendung wirklich weiter? Und viel wichtiger: Lösen wir damit überhaupt die aktuellen Probleme?

Um diese Fragen zu beantworten, müssen die Anwendung und ihre Architektur erst einmal überprüft werden. Bei einer solchen Architekturreview, die idealerweise von Externen durchgeführt wird, hat sich in der Praxis ATAM [1] als Methode bewährt. Dieser Artikel ist zu kurz, um auf ATAM einzugehen, aber ganz kompakt formuliert, definiert man via Szenarien Qualitätsanforderungen an den Sollzustand der Architektur und vergleicht diesen dann mit dem Istzustand des Systems. Das klingt erst einmal furchtbar aufwendig und teuer, daher wird die Investition oft gescheut. Jedoch stehen die Kosten aus unserer Erfahrung in keinem Verhältnis zu den Kosten (über den Lifecycle) einer nicht passenden Architektur oder Technologiewahl. Ähnlich wie bei Fahrzeugen sollte daher ein regelmäßiger TÜV der Architektur zu einer reifen Produktentwicklung dazugehören.

console.log(1);
httpClient.get<BOOK>('https://example.com/books/123').subscribe(
  (book) => console.log(`2 ${book}`));
httpClient.get<BOOK>('https://example.com/books/234').subscribe(
  (book) => console.log(`3 ${book}`);
console.log(4);

Dieser Quelltext schickt zwei GET Requests an einen fiktiven REST-Server ab, um Informationen über zwei Bücher zu bekommen. Sie werden kurzerhand auf der Entwicklerkonsole ausgegeben, zusammen mit einer durchnummerierten Zahl. Wir drucken also zunächst die Zahl 1, rufen dann das erste Buch ab, dann das zweite, und zum Schluss noch die Zahl 4.

In welcher Reihenfolge werden die Konsolenausgaben erscheinen? Die naive Antwort wäre 1, 2, 3, 4. Tatsächlich ist dieser Programmcode aber nichtdeterministisch. Es kommt darauf an, welcher REST-Call zuerst eine Antwort liefert. Es ist nur klar, dass die Zahl 4 vor der 2 und der 3 kommt.

Das ist auch gut so. Genau das soll erreicht werden. Die Informationen über beide Bücher werden gleichzeitig abgefragt, und als Sahnehäubchen ist die Anwendung auch während des Server-Requests bedienbar. In welcher Reihenfolge der Server die Requests beantwortet, ist nicht klar. Die Requests laufen eben wirklich parallel. Und es kommt noch besser: Egal ob der REST-Call 50 Millisekunden oder 50 Sekunden braucht, die Anwendung ist in der Zwischenzeit für weitere Benutzeraktivitäten verfügbar. Geschickt eingesetzt, fühlt sich die Anwendung dadurch sehr viel flüssiger an als beim traditionellen Ansatz, bei dem die Anwendung während der gesamten Wartezeit einfriert. Reactive Programming ist der natürliche Feind des Wait Cursors.

Nichtsdestotrotz gibt es einen gravierenden Nachteil. Das Programm wird nicht mehr Zeile für Zeile abgearbeitet. Stattdessen gibt es einen nichtlinearen Programmfluss. Die Callbacks stehen normalerweise oberhalb der Zeile, die zuerst abgearbeitet werden. Für Neueinsteiger kann das eine nicht zu unterschätzende Hürde sein. An den nichtlinearen Programmfluss muss man sich erst einmal gewöhnen. Und auch wenn die alten Hasen das selten zugeben: Wenn es komplizierter wird, verliert jeder von uns den Überblick. Manche früher, manche später. Wenn man beruflich programmiert, ist das ein wichtiger Punkt. Es gibt ein Mantra, das viele auswendig mitsprechen können: „Es ist nicht so wichtig, dass ihr euren eigenen Quelltext versteht. Es kommt darauf an, dass jeder eurer Kollegen euren Programmcode versteht“. Der Abstraktionslevel, der im aktuellen Team genau richtig ist, kann das nächste Team überfordern. Man muss sich also ständig nach den jeweiligen Teammitgliedern richten, und sie entweder coachen oder das eigene Abstraktionslevel an das Team anpassen.

Die Kunst des Beobachtens

Gehen wir einen Schritt weiter. Als Nächstes wollen wir erst die Liste aller Bücher holen und dann die Einzelheiten für jedes einzelne Buch. Das Prinzip wurde hier schon beschrieben. Es gibt eine subscribe()-Methode, die aufgerufen wird, wenn der Server Daten liefert. Was wäre also einfacher, als den Algorithmus wie in Listing 2 zu formulieren?

httpClient.get<Array<number>>('https://example.com/books')
    .subscribe((isbns) => {
  ids.forEach((isbns: number) => {
    httpClient.get<Book>(`https://example.com/books/${ isbns }`)
      .subscribe((book) => console.log(`${book}`));
  });
});

Das funktioniert, sollte aber besser so nicht umgesetzt werden. Ich habe bei diesem Minibeispiel mehrere Minuten damit verbracht, die Klammern richtig zu setzen. Und reale Projekte sind komplizierter, da ist die Klammersetzung das kleinste Problem. Es sollte also ein einfacher Quelltext geschrieben werden. RxJS bietet dafür ein reiches Repertoire an Operatoren. Meistens muss man überhaupt keinen Quelltext in die subscribe()-Methode schreiben. Ein Aufruf ohne Callback-Funktion reicht. Um die Idee zu illustrieren, beschränken wir uns zunächst auf den REST-Call, der die Liste der ISBNs (oder IDs) der Bücher liefert (Listing 3).

const promisedIsbns: Observable<Array<number>> =
  httpClient.get<Array<number>>('https://example.com/books');
 
promisedIsbns.pipe(
  tap((isbns: Array<number>) => console.log(isbns))
)
promisedIsbns.subscribe();

Listing 3 macht deutlich, dass http.get() ein Observable zurückliefert. Das ist eine sehr schöne Metapher: Man schickt den REST-Call los, und danach wird beobachtet, was der Server macht. Während des Beobachtens hat man Zeit für andere Dinge. Das ist ähnlich wie im wirklichen Leben, etwa beim Bügeln während des Fernsehens. Wenn es im Fernsehen spannend wird, wird das Bügeleisen kurz zur Seite gelegt, und während der Werbepause macht man mit dem Bügeln weiter. Als Nächstes definieren wir eine Pipeline. Auch das ist eine sehr schöne Metapher. Wenn man reaktiv programmiert, sollte man aufhören, Daten als etwas Statisches zu betrachten. Die Daten müssen als Datenstrom betrachtet werden. Wird die Sache weitergedacht, sind die Daten in der Datenbank so gut wie nie interessant. Sie werden immer nur dann interessant, wenn sie sich verändern. Dieser Gedanke führt dann zu Systemen, wie z. B. Apache Kafka. Das ist ein sehr mächtiger und fruchtbringender Paradigmenwechsel. Aber ich greife vor – noch sind wir ja im Frontend unterwegs.

Die Pipeline besteht aus einer Reihe von Operatoren, die der Reihe nach ausgeführt werden. In unserem Fall ist es der Operator tap, der auch Seiteneffektoperator genannt wird. Er lässt den Datenstrom unverändert passieren, erlaubt es aber, mit den Daten zu arbeiten. In diesem Fall wird die Liste der ISBNs einfach auf der Konsole ausgegeben. Als letztes Kommando kommt noch der Aufruf von subscribe(). RxJS evaluiert Ausdrücke erst dann, wenn sie gebraucht werden – das heißt, wenn sie jemand abonniert. Ohne den Aufruf von subscribe() startet der Algorithmus also nicht.

 

Wenn Beobachter Beobachter beobachten

Zurück zu unserer ursprünglichen Aufgabe. Es sollten die Information über alle Bücher gesammelt werden. Wir können also eine Pipeline mit den Operatoren map() und tap() aufbauen (Listing 4).

const ids: Observable<Array<number>> = httpClient.get<Array<number>>('https://example.com/books');
ids.pipe(
  map(
    (isbns: Array<number>) => ibns.map((isbn) => 
       httpClient.get<Book>(`https://example.com/books/${isbn}`)),
  tap((books: Array<Observable<Book>>) => console.log('Hilfe!'))
  )
);
ids.subscribe();

Tja, das hatten wir uns einfacher vorgestellt. Jetzt gibt es zwei Probleme. Zum einen enthält unsere Pipeline ein Array von ISBNs. Die Pipeline funktioniert aber am besten, wenn das Array in einen Strom von Einzelwerten aufgelöst wird. Das wird am doppelten map() deutlich. Und zum anderen liefert der verschachtelte Aufruf von http.get() ein Observable. Das Interessante ist aber der elementare Datentyp. Die Liste der Bücher wird benötigt, denn mit den Beobachtern der Bücher lässt sich nichts anfangen. Keine Panik, das Internet ist voll mit Ratschlägen, wie das Problem zu lösen ist. Und alle preisen euphorisch, dass die Lösung sehr einfach ist: „Du musst nur den forkJoin()-Operator und den flatMap()-Operator verwenden“. Das stimmt, weckt bei mir aber Zweifel, ob das hier der richtige Weg ist. RxJS hat eine sehr große Zahl von Operatoren – und meine Theorie ist, dass die meisten Operatoren Probleme lösen, die es ohne RxJS nicht gäbe. Die Umstellung auf die reaktive Programmierung stellt sich als verblüffend vertrackt heraus. Wohlgemerkt: Ich bin ein sehr großer Fan von RxJS. Es bietet ein sehr elegantes API für viele Anwendungsfälle. Ich bezweifele aber, dass Observables die richtigen Metaphern für REST-Calls sind. Ein Observable erlaubt es, einen potenziell unendlichen Strom von Daten zu beobachten. Observables sind die perfekte Lösung für WebSockets, Tastatureingaben oder auch für einen Apache-Kafka-Stream. Bei REST-Calls kommt die Metapher an ihre Grenzen. Ein REST-Call liefert maximal ein Ergebnis.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Ein Internet voller Versprechungen

Den Entwicklern von RxJS ist das auch klar, und sie bieten eine passende Lösung dafür an. Sie hat, völlig zu Unrecht, einen schlechten Ruf in der Angular-Welt. Ihr könnt ein Observable in ein Promise umwandeln. Und wenn man das mit den JavaScript-Schlüsselwörtern async und await verknüpft, wird der Quelltext fast immer dramatisch einfacher (Listing 5).

const isbns: Array<number> = await firstValueFrom(
  httpClient.get<Array<number>>('https://example.com/books'));
const books: Array<Book> = await isbns.map(
  async (isbn) => await firstValueFrom(
    httpClient.get<Book>(`https://example.com/books/${isbn}`)));
books.forEach((book) => console.log(book));

Unser Ziel ist erreicht. Es ist wieder ein linearer Quelltext entstanden, der leicht verständlich ist. Aber da fehlt doch noch etwas. In der map()-Methode steht das Schlüsselwort await. Im Endeffekt gibt es eine for-Schleife, die den Programmfluss blockiert. Aber wir wollten doch eine non-blocking IO haben.

Blockadebrecher

Zum Glück legt JavaScript ohnehin sein Veto ein und bricht mit einer Fehlermeldung ab. Das await vor dem map() kann nicht funktionieren, weil map() kein einzelnes Promise, sondern gleich ein ganzes Array von Promises liefert (Listing 6). Also rufen wir die Funktion Promise.all() zu Hilfe.

const isbns: Array<number> = await firstValueFrom(
  httpClient.get<Array<number>>('https://example.com/books'));
const promisedBooks: Array<Promise<Book>> = 
  isbns.map((isbn) => firstValueFrom(
    httpClient.get<Book>(`https://example.com/books/${isbn}`)));
const books = await Promise.allSettled(promisedBooks);
books.forEach((book) => console.log(book));

Retries, Timeouts und take(1)

In meinem Projekt verwende ich diesen Ansatz sehr erfolgreich und mit großer Begeisterung. Umso erstaunlicher, dass der Rest der Angular-Welt die Möglichkeit, Observables in Promises zu verwandelt, ignoriert oder sogar leidenschaftlich bekämpft. Ein beliebtes Gegenargument ist, dass async/await den Vorteil der reaktiven Programmierung verspielt, indem es alles künstlich wieder linearisiert. Die Gefahr besteht, doch man kann, wie gerade gesehen, leicht gegensteuern. Hierfür packen wir einfach Promise.allSettled() in den Werkzeugkoffer. Auf der Suche nach Gegenargumenten bin ich noch darauf gestoßen, dass firstValueFrom() weder Retries noch Timeouts unterstützt. Das ist aber ein Scheinargument. Auch wenn sie in Promises verwandelt werden, man hat es immer noch mit Observables zu tun. Damit haben wir die gesamte Power von RxJS zur Verfügung (Listing 7).

const isbns: Array<number> = await firstValueFrom(
  httpClient.get<Array<number>>('https://example.com/books')
    .pipe(
      timeout(1000), 
      retry(5))
);

Wem das zu umständlich ist, der kann einfach eine Funktion definieren, die sowohl firstValueFrom() als auch Operatoren timeout() und retry() aufruft. Etwas unangenehmer ist eine Eigenschaft der Methode toPromise(), die in der älteren Version RxJS 6 anstelle von firstValueFrom() verwendet werden muss. toPromise() feuert erst, wenn das Observable das complete Event schickt. Bei REST-Calls ist das kein Problem, aber wenn man das Ergebnis in einem Subject zwischenspeichert, wird das complete Event niemals geschickt. Dieses Pattern ist umständlich, löst aber das Problem:

const isbns: Array<number> = 
      await books$.pipe(take(1)).toPromise();

 

Wenn das alles so einfach ist – warum verwendet es nicht jeder?

Jetzt wird es philosophisch. Observables erzwingen einen funktionalen Programmierstil, und das wiederum hat eine magische Anziehungskraft auf viele Entwickler. Funktional geschriebene Algorithmen bestehen im besten Fall aus einer langen Kette kurzer Zeilen. Jede Zeile kümmert sich um genau eine Aufgabe. Das fühlt sich einfach gut an und hinterlässt den Eindruck, den Algorithmus optimal strukturiert zu haben. Dieser Eindruck verfliegt schnell, wenn man denselben Algorithmus prozedural neu schreibt und ihn danebenlegt. Man könnte den prozeduralen Algorithmus genauso gut in viele kleine Einzeiler aufteilen. Oft genug finde ich den Quelltext wesentlich übersichtlicher. An dieser Stelle wird die Diskussion meistens sehr emotional. Viele Entwickler finden den prozeduralen Programmierstil unübersichtlich und schwer verständlich. Vielleicht macht es sich bemerkbar, dass ich den funktionalen Programmierstil erst sehr spät kennengelernt habe. Die Idee bei JavaScript – und auch bei Java – ist, einfach beides anzubieten. Es kann der gute alte prozedurale Programmierstil verwendet werden, wenn es passt, und wenn das funktionale Paradigma besser passt, verwendet man eben das. Es ist sogar so, dass meine TypeScript- und JavaScript-Programme deutlich mehr funktionale Anteile enthalten als meine Java-Programme, weil ich das funktionale API von JavaScript für wesentlich eleganter halte. Mein Verdacht ist, dass es einfach eine Frage der Übung, der Mode und des Geschmacks ist. Bei meinen Vorträgen, in denen ich das async/await-Pattern propagiere, bekomme ich fast immer Gegenwind. Wenn ich dann zurückfrage, woher der Widerstand kommt, höre ich selten substanzielle Argumente. Diese Argumente gibt es allerdings durchaus. Lest euch nur mal das leidenschaftliche und gut begründete Plädoyer gegen async/await von Daniel Caldas [2] durch. Das sind aber selten die Argumente, die mir spontan genannt werden. Ich befürchte, das Hauptproblem ist, dass alle bei ihrer Einarbeitung in Angular verinnerlicht haben, dass RxJS und Observables das Mittel der Wahl und ein großer Fortschritt sind. Was auch richtig ist, nur setzt sich dadurch im Unterbewusstsein die Überzeugung fest, dass async/await schlecht ist. Der Witz ist nur, dass sich das Angular-Team zu einem denkbar ungünstigen Zeitpunkt für Observables entschieden hat. AngularJS 1.x hatte noch Promises verwendet, allerdings mit der alten umständlichen Syntax. Wenn man diesen Programmierstil mit dem Programmierstil von RxJS vergleicht, ist beides in etwa gleich unhandlich, aber RxJS bietet sehr viel mehr Möglichkeiten als die native Verwendung von Promises. Die Abkehr von Promises war folgerichtig. Rund ein Jahr später wurde das neue Schlüsselwortpaar async/await in JavaScript eingeführt. Ob sich das Angular-Team für RxJS entschieden hätte, wenn async/await schon Mainstream gewesen wäre?

Wie sieht das in Java aus? Dieser Artikel basiert auf einem Vortrag für Angular-Entwickler. Als ich den Vortrag dem Java Magazin angeboten hatte, dachte ich mir, dass es interessant sein könnte, die Erkenntnisse auf Java zu übertragen. Leider war das nicht so fruchtbringend wie erhofft. In erster Linie habe ich beim Recherchieren festgestellt, dass Java eben anders tickt als JavaScript.

Fangen wir mit dem Offensichtlichen an. In Java gibt es kein async/await. Stattdessen gibt es in den Java-Frameworks, die ich mir angeschaut habe, den Operator block(), der aus einem Mono wieder den eigentlichen Wert machen kann. Analog gibt es bei Java den Operator collectList() für ein Flux. Was sich hinter den Begriffen Mono und Flux verbirgt, erzähle ich euch gleich. Die Operatoren block()und collectList() blockieren den Programmfluss. Das klingt schlimmer als es ist: Im Gegensatz zu JavaScript versteht sich Java auf die Kunst des Multi-Threadings. Man blockiert sich also nur selbst, aber nicht gleich den ganzen Server. Es könnte auch sein, dass das Framework gezwungen ist, von einer optimalen Multi-Threading-Strategie auf eine weniger effiziente Strategie umzuschalten. Aber das ist auch alles. Das Rezept, das ich oben vorgestellt habe, funktioniert in Java also nicht. Man muss sich auf die funktionale Programmierung einstellen. Übertreiben sollte man es aber nicht. Faustregel: Wenn man ein if-Statement braucht, extrahiert man den Code einfach in eine Methode. Ich hatte schon kurz Flux und Mono (oder Uni in Quarkus) angesprochen. Das ist ein interessanter Unterschied zu RxJS. Quarkus und Spring Reactive unterscheiden zwischen Datenströmen, die viele Ergebnisse liefern (Flux) und einfachen Requests, die nur ein Ergebnis liefern (Mono bzw. Uni). Als ich das gelesen habe, fand ich das verblüffend. In der Angular-Welt gibt es eine lebhafte Diskussion, ob Promises oder Observables besser sind, und gleichzeitig unterstützen die Java-Frameworks kurzerhand beides gleichzeitig. Uni bzw. Mono entsprechen in etwa dem Promise und Flux entspricht dem Observable.

 

Zustände

Möglicherweise liegt der Hauptunterschied zwischen der reaktiven Programmierung mit Angular und mit Java ganz woanders. Heutzutage entwickeln die meisten Java-Entwickler REST-Services. Diese sind stateless. Alles ist im Fluss. Man muss sich nicht um statische Daten kümmern. Der Datenstrom, den die reaktiven APIs liefern, reicht vollkommen. In Angular hingegen dreht sich am Ende des Tages alles um den State der Anwendung. Das ist ein Aspekt, den ich in diesem Artikel noch nicht gezeigt hatte und der Angular-Entwickler – und vor allem die Neueinsteiger unter ihnen – immer wieder vor Probleme stellt. Es gehört zu den Best Practices der Angular-Welt, möglichst den kompletten State der Anwendung in Observables zu speichern. So ein Observable ist jedoch nur dafür gedacht, Änderungen des Zustands zu kommunizieren, nicht aber, den aktuellen Zustand zu speichern. Wenn man den aktuellen Zustand braucht, man erst einmal schauen, wo man ihn herbekommt. Für Java-Entwickler entfällt diese Notwendigkeit von vorneherein. Das macht das Leben für die Java-Entwickler leichter. Sie müssen viel seltener zwischen dem reaktiven und dem synchronen Code wechseln. Als Gemeinsamkeit gibt es natürlich die Notwendigkeit, Methoden wie flatMap() zu verwenden. Die Quarkus Sandbox [3] von Hantsy Bai enthält ein paar Beispiele dafür. Und was ist mit Annotationen? Ein komplett anderer Weg, reaktive Programmierung zu vereinfachen, ist mir bei Apache Kafka aufgefallen. Schaut euch mal das Tutorial von Baeldung [4] an. Dort wird eine Annotation verwendet, um eine Methode aufzurufen, wenn der Server ein Ergebnis liefert (Listing 8).

@KafkaListener(topics = "topicName", groupId = "foo") 
public void listenGroupFoo(String message) {
   System.out.println("Received Message in group foo: " + message); 
}

Das ist das Reaktive Pattern konsequent zu Ende gedacht. Die Kafka-Topics bieten eine sehr lose Kopplung zwischen Nachrichtenquelle und Nachrichtenempfänger. Das funktioniert natürlich nicht immer. Im ursprünglichen Beispiel, dem Select-Statement oder dem REST-Call, ist das zu unpraktisch. Es würde auch ein falsches Signal aussenden. Eine Angular-Anwendung ist keineswegs lose mit dem Backend gekoppelt. Wenn das Backend nicht verfügbar ist, ist ein Angular-Frontend nutzlos.

Aber lassen wir das mal kurz außer Acht. Dann kann man sehen, dass Annotationen ein schönes Stilmittel sind, um Algorithmen reaktiv zu implementieren. Der Clou ist, dass es hier nicht um Observables, Monos oder Fluxes geht. Die Parameterliste und der Rückgabetyp der Funktion sind die reinen Datentypen. Das vereinfacht die Programmierung enorm. Das Framework übernimmt die komplette Abstraktion. Wir brauchen nicht zu wissen, dass ein @KafkaListener im Grunde genommen auch nichts anderes ist als ein Observable.

Klingt das verrückt? Dann schaut es euch noch einmal genau an. In Angular hättet ihr vermutlich eine Methode listenToKafka(topic, groupId), die ein Observable zurückliefert. Analog hättet ihr in Spring Reactive eine Methode listenToKafka(topic, groupId), die ein Flux zurückliefert. In beiden Fällen kann in der subscribe()-Methode definiert werden, was mit den Daten passieren würde. Und dieser Algorithmus wiederum ist exakt der Inhalt der Methode listenToFoo() aus Listing 7.

Stay tuned

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

 

Resumée

Das war ein wilder Ritt durch die Untiefen der reaktiven Programmierung. Die Kernbotschaft, die ich vermitteln will, ist aber ziemlich einfach. Reactive Programming ist etwas, mit dem man sich beschäftigen sollte. Die Anwender werden es euch danken. Bessere Performance kommt immer gut an.

Das heißt aber nicht, dass ihr euch blindlings in das Abenteuer stürzen solltet. Oder, doch, das solltet ihr schon. Mut zur Lücke ist immer gut. Irgendwo muss man ja anfangen. Ihr werdet dann aber schnell feststellen, dass reaktive Programmierung ihre Tücken hat. An diesem Punkt angekommen, ist der richtige Zeitpunkt, diesen Artikel (noch einmal) zu lesen. Es gibt Strategien, reaktive Programmierung beherrschbar zu machen. Im Internet wird meistens empfohlen, sich mit Haut und Haaren darauf einzulassen. Das funktioniert für viele Teams ziemlich gut. Man kann aber auch versuchen, die Komplexität zu reduzieren. Bei Angular ist async-await das Mittel der Wahl. Bei Java gibt es diese Möglichkeit nicht. Sie wird auch nicht benötigt. Beim reaktiven Programmieren wird es meistens erst dann schwierig, wenn auf den aktuellen Zustand der Anwendung zugegriffen werden soll. Solange zustandslose REST Services entwickelt werden, habt ihr diese Anforderung nicht. Und falls doch, hat auch Java noch das eine oder andere As im Ärmel. Apache Kafka zeigt, wie mit Annotationen den Abstraktionsgrad der Algorithmen deutlich reduziert werden kann.

 

Links & Literatur

[1] https://www.reactivemanifesto.org

[2] https://goodguydaniel.com/blog/why-reactive-programming

[3] https://hantsy.github.io/quarkus-sandbox/reactive.html

[4] https://www.baeldung.com/spring-kafka

The post Die Untiefen reaktiver Programmierung appeared first on JAX.

]]>
Mit Quarkus gegen Monolithen https://jax.de/blog/mit-quarkus-gegen-monolithen/ Wed, 12 Jan 2022 12:35:44 +0000 https://jax.de/?p=85447 Firmen stehen häufig vor dem Problem, dass über lange Zeit gewachsene Softwareprojekte unwartbar und immer unverständlicher werden. Irgendwann stellt sich die Frage: Alles hinschmeißen und neu entwickeln? Wir möchten euch einen Einblick in unser Vorgehen, die Entscheidungsprozesse und unsere künftige Architektur geben. Insbesondere gehen wir auf das Set-up, das auf Quarkus basiert, ein und zeigen, dass sich damit nicht nur kleine Services bauen lassen.

The post Mit Quarkus gegen Monolithen appeared first on JAX.

]]>
Wer sind wir? Die LucaNet AG hat sich auf den Bereich Finanzsoftware für Konzerne für deren Konsolidierung, Planung, Analyse und Reporting spezialisiert. Seit 1999 entwickelt LucaNet die Softwarelösungen stetig weiter und versucht, den stets wachsenden Anforderungen der Kunden gerecht zu werden und die bestmögliche Performance zu liefern. Die Softwarelösung der LucaNet AG ist eine Client-Server-Anwendung, in der je nach Unternehmensgröße bis zu mehreren Hundert Personen parallel auf einem sehr komplexen Datenmodell arbeiten, das die ganze Zeit im Speicher gehalten wird.

Der Monolith

Am Anfang steht immer eine Idee. Dann schreibt man etwas Software und daraus entwickelt sich eine Applikation gemäß den Paradigmen und Möglichkeiten der Zeit. In den folgenden Jahren werden mehr und mehr Features entwickelt, um die schnell wachsenden Kundenanforderungen zu bedienen. Es bleibt keine Zeit (oder es wird sich keine Zeit genommen) für Wartungsarbeiten oder ein klassisches Refactoring. Die Kunden sind zufrieden mit dem Umfang und der Leistungsfähigkeit der Anwendung – warum sollte man also etwas anders machen? Einige Jahre und viele Personenjahre Aufwand später ist aus der kleinen Applikation eine ausgewachsene Businessanwendung geworden. Unzählige Lines of Code, Klassen, Module usw., die zwar wunderbar funktionieren, aber nur von wenigen „alten Hasen“ wirklich verstanden und gewartet werden können. Aber auch eine ganze Reihe von Hacks und Bugfixes, die das ursprüngliche Architekturpattern unterwandern. Häufig fällt in so einem Umfeld die Floskel „Das ist historisch gewachsen“. Das Muster dürfte den meisten von euch bekannt vorkommen. Denn diese Situation gibt es bei (fast) jedem Softwaresystem, das sehr erfolgreich ist und über viele Jahre weiterentwickelt wurde.

Doch warum nennen wir so etwas Monolith? Das Wort Monolith kommt aus dem Griechischen und heißt so viel wie „einheitlicher Stein“, und wir finden die Analogie sehr passend für Softwaresysteme, die von außen betrachtet undurchdringbar und schwer veränderlich sind. Daraus resultiert auch, dass sich einzelne Teile nur schwer aktualisieren lassen und so gut wie unmöglich herauszutrennen sind.

Stay tuned

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

 

Alles neu?

Wir sind alle Entwickler und Technologieenthusiasten und daher kommen wir sehr schnell zu der Überzeugung, dass mit Framework X oder Technologie Y alle Probleme schnell gelöst werden und damit auch ganz neuen Herausforderungen und Anforderungen leicht entsprochen werden kann. Einige wollen auch dafür eigene Technologien entwickeln, was gar nicht mal ungewöhnlich ist – man liest und hört nur sehr wenig von diesen Ansätzen. Die meisten Seniorentwickler schreiben die gleiche Art von Anwendung oder lösen die gleiche Art von Problemen Tag für Tag, Jahr um Jahr. Das ist manchmal langweilig, und was gibt es Spannenderes, als ein eigenes Framework zu schreiben? Vielleicht sogar mit der Absicht, es danach als Open-Source-Software zu veröffentlichen? Die Antwort ist: Wahrscheinlich ist es superspannend, aber bringt es unsere Anwendung wirklich weiter? Und viel wichtiger: Lösen wir damit überhaupt die aktuellen Probleme?

Um diese Fragen zu beantworten, müssen die Anwendung und ihre Architektur erst einmal überprüft werden. Bei einer solchen Architekturreview, die idealerweise von Externen durchgeführt wird, hat sich in der Praxis ATAM [1] als Methode bewährt. Dieser Artikel ist zu kurz, um auf ATAM einzugehen, aber ganz kompakt formuliert, definiert man via Szenarien Qualitätsanforderungen an den Sollzustand der Architektur und vergleicht diesen dann mit dem Istzustand des Systems. Das klingt erst einmal furchtbar aufwendig und teuer, daher wird die Investition oft gescheut. Jedoch stehen die Kosten aus unserer Erfahrung in keinem Verhältnis zu den Kosten (über den Lifecycle) einer nicht passenden Architektur oder Technologiewahl. Ähnlich wie bei Fahrzeugen sollte daher ein regelmäßiger TÜV der Architektur zu einer reifen Produktentwicklung dazugehören.

Den Monolithen erwürgen

Wenn man bei der Review zu der Erkenntnis gelangt, dass die aktuelle Architektur nicht mehr lange weiterentwickelt und gewartet werden kann, muss man handeln. Aus der Definition oben lässt sich schon ableiten, dass man den Monolithen nicht einfach in ein paar Teile zerlegen und diese dann separat weiterentwickeln kann. Es ist aber auch so gut wie nie möglich, die Entwicklung an dem aktuellen System zu stoppen, zwei bis drei Jahre lang eine neue Architektur zu entwickeln, um dann das neue System präsentieren zu können. Wir müssen also das bestehende System, den Monolithen, zumindest vorübergehend in das neue Gesamtsystem integrieren.

Bei der Umstellung von Monolithen auf Microservices gibt es das sogenannte Strangler Pattern (Kasten: „Strangler Pattern ) [2]. LucaNet entwickelt ein Produkt, das auch on-premise, das heißt direkt beim Kunden, installiert werden kann. Hier kommt für uns ein Microservices-Ansatz nicht infrage. Die Strategie für die Umstellung ist, dass das Produkt technologisch und architektonisch so einfach wie möglich gehalten wird. Das Produkt muss von der Kunden-IT, aber natürlich auch durch uns leicht integrierbar und leicht zu betreiben sein. Das Pattern ist trotzdem sehr interessant, und wir werden es leicht abgewandelt in unsere Strategie einbringen.

Strangler Pattern

Das Strangler Pattern beschreibt kurz gesagt ein Vorgehen, bei dem Stück für Stück Teile eines Monolithen herausgetrennt und als Microservices in die Gesamtarchitektur eingebracht werden. Für unser Vorhaben wandeln wir dieses Vorgehen leicht ab. Den Monolithen selbst können wir nicht umbauen und nur schwer verändern. Wir werden den neu zu entwickelnden Server vor den Monolithen setzen und alle Requests darüber laufen lassen. Damit können transparent Funktionen in den neuen Server eingebaut werden und die alten Funktionen werden einfach nicht mehr angesteuert. An einem bestimmten Punkt in der Entwicklung wird der alte Monolith schließlich überflüssig sein – er wurde quasi Stück für Stück „erwürgt“ (engl. to strangle).

Das grundsätzliche Vorgehen war damit recht schnell gesetzt. Es wird ein neues User Interface entwickelt und der bisherige Monolith muss um notwendige Schnittstellen für den Zugriff erweitert werden. An dieser Stelle kommt dafür nur REST infrage, da wir damit im Front-end komplett technologieunabhängig sind. Das neue Frontend soll natürlich nicht einfach an das bestehende Backend angebunden werden, da wir sonst so gut wie keine Fortschritte gemacht haben. Schlimmstenfalls ist eine weitere Komponente von dem Big Ball of Mud abhängig.

ALL ABOUT MICROSERVICES

Microservices-Track entdecken

 

Evolution statt Revolution

Es müssen jetzt zwei Dinge gleichzeitig entwickelt werden: zum einen das neue Frontend und zum anderen die entsprechenden Schnittstellen im Backend. Damit beides wirklich parallel und ohne große Störungen geschehen kann, haben wir uns für einen API-First-Ansatz entschieden. Da jetzt schon klar ist, dass der Monolith nicht so bleibt, wie er ist, muss er vom Frontend entkoppelt werden. Auch muss das neue JavaScript Frontend von einem Server ausgeliefert werden. Die erste Entscheidung war also auch eine der wichtigsten: Welchen Server bzw. welches Framework sollen wir verwenden (Kasten: „Entscheidungsprozess“)?

Entscheidungsprozess

Als Entwickler sind wir bei der Technologiewahl schnell bei den neuesten Technologien und Frameworks (aka „der neueste heiße Scheiß“). Anders als bei Start-ups, kleineren Projekten oder reinen Webapplikationen müssen wir hier von vornherein über viel größere Zeithorizonte nachdenken. Einer der wichtigsten Punkte bei der Szenariendefinition für die neue Architektur war Langlebigkeit, die zukünftige Architektur soll mindestens zehn Jahre halten. Natürlich kann heute nicht alles für die nächsten zehn Jahre antizipiert werden, aber die Architektur muss in der Lage sein, Veränderungen zu unterstützen [3], dabei aber so stabil bleiben, dass wir nicht befürchten müssen, dass es das Framework in drei Jahren nicht mehr gibt, die Maintainer keine Lust mehr haben oder ein Versionswechsel ein halbes Jahr Arbeit nach sich zieht. Häufig bedeutet das, je langweiliger die Technologie, desto besser. Außerdem sollten bei der Technologiewahl auch der Einsatz und die Wahrung von konzeptionellen Standards Berücksichtigung finden. Jakarta EE (früher Java EE) und insbesondere der MicroProfile-Standard erfüllen den Anspruch von Stabilität, Flexibilität und – entsprechend den Architekturzielen – auch der Nachhaltigkeit. Mittlerweile gibt es einige sehr spannende Umsetzungen des MicroProfile-Standards, weshalb die Lösung gar nicht so langweilig sein muss.

Wir haben uns nach der Evaluation für Quarkus [4] entschieden. Auch wenn das ein relativ neuer Server ist, haben uns hier mehrere Aspekte überzeugt. Zum Teil machte die Entscheidung aus, dass Quarkus ein MicroProfile-Server ist, wir uns also nicht direkt an ein Produkt binden, sondern an einen Standard. Im schlimmsten Fall können wir einen anderen MicroProfile-Server (z. B. Payara, Open Liberty, … ) nehmen und unsere Anwendung mit wenigen Anpassungen damit laufen lassen. Weiterhin ist die Dokumentation dieses Servers exzellent und die Verwendbarkeit denkbar einfach. Es wurde auch nicht alles neu erfunden, sondern man hat sich dediziert für ausgereifte Bibliotheken und Frameworks, wie z. B. Vert.x, entschieden. Das ermöglicht uns wiederum, bei einem potenziellen Serverwechsel diese Bibliotheken oder Frameworks als Third-party Dependency einzubinden, falls wir deren Features verwenden.

Die nächste große Herausforderung ist die Tatsache, dass wir auch bisher keine Microservices hatten bzw. das Produkt direkt beim Kunden im eigenen Datacenter läuft. Die Idee ist, dass wir den MicroProfile-Server zwischen das Frontend und den Monolithen setzen. Quarkus fungiert nun als Anti-corruption Layer (Kasten: „Anti-corruption Layer). Egal wie sehr sich die Schnittstellen im Monolithen ändern werden, nach außen hin wird immer noch das gleiche REST-Interface bedient. Wir können mit diesem Ansatz die Frontend-Entwicklung sogar beschleunigen, indem wir eine Pseudoimplementierung der Schnittstelle Testdaten ausliefern lassen. Das Frontend kann also schon alle Methoden mit unterschiedlichen Parametern aufrufen und wir erhalten in einem sehr begrenzten Umfang Testdaten als Antwort.

Anti-corruption Layer

Das Konzept des Anti-corruption Layers kommt ursprünglich aus dem Domain-driven Design [5]. Hier geht es darum, einen Bounded Context von einem anderen Bounded Context abzugrenzen. Der Anti-corruption Layer sorgt dann dafür, dass Änderungen in Bounded Context A keine ungewollten Änderungen im Bounded Context B verursachen.

Wir wollen mit dem Anti-corruption Layer nicht nur einzelne Bounded Contexts voneinander abgrenzen, sondern den bisherigen Monolithen über neue Schnittstellen isolieren. Mit diesem neuen Layer sind wir auch in der Lage, das Backend jederzeit teilweise oder sogar ganz auszutauschen. Features können so jeweils Stück für Stück in Quarkus selbst wandern und dadurch die Aufrufe in den Monolithen überflüssig machen (Abb. 1). Das hat für uns den weiteren Vorteil, dass wir bei der Entwicklung bereits Erfahrungen mit dem neu zu entwickelnden Backend sammeln können, ohne ein Risiko für den Kunden zu generieren. Somit steuern wir nicht auf ein Big Bang Release zu, sondern erarbeiten eine Umstellung auf ein gut getestetes Backend. Die Umstellung selbst wurde bis dahin vielfach getestet, was uns zusätzliche Sicherheit für den finalen Schritt gibt.

Abb. 1: Schrittweise Ablösung des Monolithen

Nieder mit dem Monolithen

Was bedeutet das jetzt alles für die Entwicklung des neuen Backend? Die größte Herausforderung für uns ist, die Architektur möglichst einfach zu konzipieren. Weiter oben wurde bereits festgestellt, dass ein Microservices-Ansatz derzeit für uns nicht infrage kommt. Entwickeln wir jetzt den nächsten Big Ball of Mud? Sicher nicht. Unsere Architekturstrategie für die Zukunft sieht einen modularen Monolithen vor, dessen einzelne Module vollständig voneinander getrennt sind. Das soll so weit gehen, dass es möglich sein muss, die Module auf verschiedene einzelne Server aufzuteilen. Dieses Vorgehen hat gleich mehrere Vorteile:

  1. Wir verhindern einen neuen Big Ball of Mud

  2. Einzelne Module sind einfacher zu verstehen, einfacher zu debuggen und zu warten

  3. Jedes Modul kann von einem anderen Team entwickelt werden, ohne dass sich die Teams in die Quere kommen

  4. Es bietet die Option, in Zukunft evtl. auf eine Microservices-/SCS-Architektur umzusteigen

Trotz der sehr guten Quarkus-Dokumentation gibt es leider nur sehr wenige Informationen darüber, wie dieser MicroProfile-Server für mehr als einen Microservice eingesetzt werden kann. Die Standardtutorials zeigen, wie schön einfach sich einzelne Services oder kleine Anwendungen ohne Untermodule bauen lassen, aber so gut wie nie, wie eine mögliche Projektstruktur für eine größere Anwendung aussieht. Den Proof of Concept für unsere Zwecke zu erstellen, hat uns etwas Zeit gekostet, deshalb wollen wir unsere Ergebnisse gern hier teilen.

 

Der zukünftige Server soll neben dem Backend auch das neue Frontend ausliefern. Getreu dem Motto „KISS – Keep it simple, stupid“ sparen wir uns damit ein weiteres Stück Komplexität in der Auslieferung und im Betrieb. Das Projekt-Set-up soll so einfach wie möglich bleiben und dabei gut erweiterbar sein. Als Build-Tool haben wir uns für Maven entschieden. Hier ist eine Projektstruktur durch das Konzept „Convention over Configuration“ vorgegeben und kann nur schwierig geändert werden, was wir als großen Vorteil ansehen. Gradle ist in einigen Punkten flexibler, hat aber damit auch den Nachteil, dass der Code für den Build selbst ebenfalls über die Jahre aus dem Ruder laufen kann.

Jedes funktionale Modul im Projekt entspricht in unserem Beispiel dann eins zu eins einem Maven-Modul. In der einfachsten Ausführung haben wir drei Module: Frontend, Backend und Runnable. Das Frontend-Modul enthält sämtlichen Code für das Frontend und produziert ebenfalls ein jar, damit dieses später als Dependency referenziert werden kann. Das Backend-Modul enthält entsprechend den Code für das Backend. Das Runnable-Modul ist ein reines Metamodul, es ist nur dafür da, das Executable Jar (Native Executable, Docker-Container) am Ende zusammenzubauen. In der Beispielgrafik (Abb. 2) sieht man schon, wie sich weitere Backend-Module in die Struktur einfügen.

Abb. 2: Projektstruktur in der Übersicht

Frontend

Die erste Frage, die sich bei dem Frontend-Modul stellt, ist: „Muss man Node, npm/yarn etc. separat installiert haben?“ Einfacher ist es natürlich, wenn jeder Entwickler einfach das Projekt auschecken und direkt damit arbeiten kann. Das macht die automatische Integration, zum Beispiel mit Jenkins, einfacher, weil auch hier Node.js etc. nicht extra installiert werden müssen, was auch potenzielle Fehler durch unterschiedliche Versionen in Development und Build von vornherein verhindert. Da wir ja alles so einfach wie möglich haben wollen, werden wir diesen Build-Prozess ebenfalls integrieren. Hierbei hilft uns das Frontend-Maven-Plug-in [6]. Dieses Plug-in unterstützt die Verwendung von yarn, npm, bower und anderen gängigen Tools im Frontend-Development-Bereich. Mit dessen Hilfe können wir jetzt die Struktur des Front-end-Moduls (Abb. 3) so gestalten, dass ein Frontend-Entwickler sich dort zu Hause fühlt und alle notwendigen Build-Tools zur Verfügung hat. Je nach Betriebssystem wird in unserem Fall die richtige Node.js-Version heruntergeladen und es werden sämtliche Dependencies mit npm aufgelöst und in ein separates Verzeichnis gepackt.

Abb. 3: Frontend-Projektstruktur in IntelliJ

An dieser Stelle ist es wichtig zu erwähnen, dass man beide Verzeichnisse unbedingt der .gitignore-Datei hinzufügen sollte, damit nicht aus Versehen bis zu mehreren Hundert Megabyte an Bibliotheken eingecheckt werden. Auch wollten wir verhindern, dass die Dependencies ständig aufgelöst und heruntergeladen werden, da dies eine Weile dauern kann. Hierfür verwenden wir verschiedene Maven-Profile (Listing 1). Ein Profil genau für den eben beschriebenen Schritt und eins zum reinen Bauen der Sources. Das spart uns bei der täglichen Arbeit eine Menge Zeit. Bei der Backend-Entwicklung braucht man häufig den Frontend-Part gar nicht oder nicht aktuell und kann ihn beim Build weglassen, indem die entsprechenden Profile nicht angegeben bzw. aktiviert werden.

<profiles>
  <profile>
    <id>Install node and npm</id>
    <activation>
      <property>
        <name>ui.deps</name>
      </property>
    </activation>
    <build>
      <plugins>
        <plugin>
          <groupId>com.github.eirslett</groupId>
          <artifactId>frontend-maven-plugin</artifactId>
          <version>${version.frontend-maven-plugin}</version>
          <executions>
            <execution>
              <id>install node and npm</id>
              <goals>
                <goal>install-node-and-npm</goal>
              </goals>
              <phase>generate-sources</phase>
              <configuration>
                <nodeVersion>v14.17.0</nodeVersion>
              </configuration>
            </execution>
            <execution>
              <id>npm install</id>
                <goals>
                  <goal>npm</goal>
                </goals>
                <configuration>
                  <arguments>install</arguments>
                </configuration>
            </execution>
            </executions>
        </plugin>
      </plugins>
    </build>
  </profile>
  <profile>
    <id>Build the UI with AOT</id>
    <activation>
      <property>
        <name>ui</name>
      </property>
    </activation>
    <build>
      <plugins>
        <plugin>
          <groupId>com.github.eirslett</groupId>
          <artifactId>frontend-maven-plugin</artifactId>
          <version>${version.frontend-maven-plugin}</version>
          <executions>
            <execution>
              <id>npm run build</id>
              <goals>
                <goal>npm</goal>
              </goals>
              <configuration>
                <arguments>run build</arguments>
              </configuration>
              <phase>generate-resources</phase>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>

Das Frontend-Plug-in muss noch wissen, in welchem Verzeichnis sich die Frontend-Sources befinden. Das Maven Resources-Plug-in muss auch konfiguriert werden, damit die fertigen Dateien im richtigen Verzeichnis im Target-Ordner landen. Hier muss vor allem darauf geachtet werden, den Applikationsnamen in der Ordnerstruktur nicht zu vergessen: <directory>webapp/dist/my-app</directory> (Listing 2).

Die Konfiguration des Maven Clean-Plug-in ist reine Bequemlichkeit, damit man nicht von Hand die Unterverzeichnisse löschen muss, wenn man das Frontend komplett neu bauen möchte.

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-clean-plugin</artifactId>
      <version>${clean.plugin.version}</version>
      <configuration>
        <filesets>
          <fileset>
            <directory>webapp/dist</directory>
          </fileset>
        </filesets>
      </configuration>
    </plugin>
    <!-- Used to run the angular build -->
    <plugin>
      <groupId>com.github.eirslett</groupId>
      <artifactId>frontend-maven-plugin</artifactId>
      <version>${version.frontend-maven-plugin}</version>
      <configuration>
        <workingDirectory>webapp</workingDirectory>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-resources-plugin</artifactId>
      <version>${resources.plugin.version}</version>
      <executions>
        <execution>
          <id>copy-resources</id>
          <phase>process-resources</phase>
          <goals>
            <goal>copy-resources</goal>
          </goals>
          <configuration>
            <outputDirectory>${basedir}/target/classes/META-INF/resources/</outputDirectory>
            <resources>
              <resource>
                <directory>webapp/dist/my-app</directory>
                <filtering>false</filtering>
              </resource>
            </resources>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Backend

Der Backend-Teil ist relativ straight forward, hat aber auch ein, zwei Punkte, die nicht offensichtlich sind. Das Verzeichnislayout bleibt so, wie wir es von Maven-Projekten gewohnt sind. Wenn man bei seinem Projekt CDI verwendet, muss man die CDI Bean Discovery durch die Verwendung des jandex-maven-plugins [7] explizit aktivieren. Interessant wird es auch bei der Konfiguration durch die application.properties-Datei. Diese wird in dem Runnable-Modul in das final ausführbare jar eingefügt. Quarkus, wie andere MicroProfile-Server auch, liest Properties von mehreren Quellen ein. Nach Standard werden die System- und Umgebungsvariablen gelesen und dann die META-INF/microprofile.properties-Datei. Hier wird noch eine zusätzliche Quelle eingefügt, die application.properties-Datei. Mit Hilfe dieser Datei lassen sich viele Serveroptionen sehr einfach konfigurieren, sie sind allerdings Quarkus-spezifisch und können nicht alle eins zu eins auf andere Server übertragen werden.

Für das Backend-Modul selbst ist diese Datei quasi nicht existent, weil sie im Runnable-Modul liegt. Dieser Umstand fällt erst so richtig auf, wenn man versucht, für Tests mit Wiremock die Ports zu ändern, und sich wundert, dass die geänderten Ports nicht verwendet werden (Listing 3).

# This port is being used e.g. for the WireMock server
# 0 means random port assigned by the OS
quarkus.http.test-port=0
quarkus.http.test-ssl-port=0

Da diese Einstellungen nur für die Tests gebraucht werden, können wir eine separate application.properties-Datei einfach in den src/test/resources-Pfad packen und mit den entsprechenden Werten für die Tests bestücken. Die Ausführung der Tests erfolgt nur im Kontext dieses Moduls, daher ist es völlig irrelevant, wo andere Konfigurationen hinterlegt sind. Wenn das Projekt später weiterwächst und mehrere Backend-Module enthält, kann somit jedes Backend-Modul seine eigenen Test-Properties mitbringen, ohne dass diese sich gegenseitig stören.

 

Runnable

Das Runnable-Modul hat nur eine Aufgabe, und zwar, eine ausführbare Datei zu bauen. Das kann eine ausführbare jar-Datei sein, ein Docker-Container oder ein Native Executable mit Hilfe des Graal Native Image Compilers [8]. Man muss sich auch nicht exklusiv für eine Methode entscheiden, sondern kann alle drei Möglichkeiten vorbereiten und dann das gewünschte Target beim Bauen auswählen. In diesem Modul befindet sich schließlich auch application.properties von Quarkus, welche die Laufzeitkonfiguration für das zu bauende Runnable enthält.

Etwas fehlt noch. Der Development Mode von Quarkus lässt sich mit dem Multi-Module-Set-up nicht mehr so einfach aktivieren. Ein etwas komplizierteres Maven Goal hilft hier weiter. Am besten legt man es direkt als Run-Konfiguration für Maven in der IDE an oder speichert es als Shell Script:

mvn -pl runnable -am compile quarkus:dev

Dieser Aufruf im Projekt-Root sorgt dafür, dass alle Module, welche als Dependency im Runnable-Modul eingetragen sind, kompiliert werden, und zwar im Quarkus-Dev-Modus. Damit der Dev-Modus auch wirklich aktiviert werden kann, muss noch das entsprechende Maven Plug-in im Runnable-Modul konfiguriert werden (Listing 4).

<plugin>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-maven-plugin</artifactId>
  <version>${quarkus-plugin.version}</version>
  <extensions>true</extensions>
  <executions>
    <execution>
      <goals>
        <goal>build</goal>
        <goal>generate-code</goal>
        <goal>generate-code-tests</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Das komplette Beispiel, aus dem die Code-Snippets stammen, ist auf GitHub [9] verfügbar.

 

Ausblick

Wir befinden uns erst am Anfang der Umstellung unserer Softwarearchitektur. Die Überlegungen, die Evaluation und die Vorbereitungen waren sehr umfangreich und haben auch schon einige Prototypen hervorgebracht. Die Erkenntnisse, die wir bis hierhin gewonnen haben, haben wir in diesem Artikel beschrieben. Für uns ist das aber nur der Start in ein sehr großes Unterfangen. Wir wollen auch in Zukunft unsere Erkenntnisse, Entwicklungen und Erfahrungen teilen und werden bestimmt mit dem einen oder anderen Artikel oder auch Vortrag wiederkommen.

Stay tuned

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

 

Links & Literatur

[1] https://resources.sei.cmu.edu/library/asset-view.cfm?assetid=5177

[2] Fowler, Martin: https://martinfowler.com/bliki/StranglerFigApplication.html

[3] Parsons, R., Kua, P., Ford, N.: „Building Evolutionary Architectures: Support Constant Change“, O’Reilly, 2017

[4] https://quarkus.io

[5] Evans, Eric: „Domain-Driven Design: Tackling Complexity in the Heart of Software“, Addison-Wesley Professional, 2003

[6] https://github.com/eirslett/frontend-maven-plugin

[7] https://quarkus.io/guides/maven-tooling#multi-module-maven

[8] https://www.graalvm.org/reference-manual/native-image/

[9] https://github.com/Mr-Steel/quarkus_multimodule

The post Mit Quarkus gegen Monolithen appeared first on JAX.

]]>
Spring Native Hands-on https://jax.de/blog/spring-native-hands-on/ Mon, 05 Jul 2021 07:18:10 +0000 https://jax.de/?p=83728 Mit dem neuen Projekt Spring Native können Spring-Boot-Anwendungen von der GraalVM-Native-Image-Technologie Gebrauch machen und auch für existierende Spring-Boot-Anwendungen Start-up-Zeiten im Millisekundenbereich erzielen. Der Artikel zeigt, wie das funktioniert, wie weit Spring Native schon ist, und wie man die Technologie für eigene Spring-Boot-Anwendungen einsetzen kann.

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

]]>
Die Vorteile der GraalVM-Native-Image-Technologie klingen verlockend: Start-up-Zeiten im Millisekundenbereich und ein deutlich reduzierter Verbrauch an Ressourcen (vor allem Speicher) – wer möchte das nicht?

Jedoch kommt diese Technologie mit einer Reihe von Einschränkungen daher. Reflection funktioniert beispielsweise in einem Native Image nur, wenn der Compiler darüber informiert wird, für welche Elemente (Klassen, Methoden, Attribute) er die Reflection-Informationen zur Compile-Zeit erzeugen und im Binary hinterlegen muss. Ähnliches gilt für Proxys, zusätzliche Ressourcen, JNI-Aufrufe und Dynamic Class Loading. Andere Techniken, wie zum Beispiel invokedynamic, funktionieren in einem Native Image grundsätzlich nicht.

Insofern kann es eine erhebliche Herausforderung sein, eine existierende Java-Anwendung in ein Native Image zu kompilieren. Zum einen muss der Code der eigenen Anwendung frei von nicht unterstützten Techniken sein, und zum anderen müssen passende Konfigurationsdateien erstellt werden, um beispielsweise Reflection zu ermöglichen. Gleiches gilt natürlich auch für alle von der eigenen Anwendung genutzten Libraries.

Was ist mit Spring-Boot-Anwendungen?

Auch für Spring-Boot-Anwendungen gilt: Sie lassen sich mit der GraalVM-Native-Image-Technologie in native Anwendungen kompilieren. Allerdings verwendet das Spring Framework viele der eben genannten Technologien relativ ausgiebig, sodass es mitunter mühsam werden kann, die nötigen Konfigurationen für den Compiler manuell zu erstellen. Grundsätzlich ist das aber möglich.

Was ist Spring Native?

Das Spring-Native-Projekt [1] ermöglicht es Entwicklern, Spring-Boot-Anwendungen mit der GraalVM-Native-Image-Technologie in Executable Binaries zu kompilieren, ohne dass die nötigen Konfigurationsdateien manuell erstellt werden müssen oder die Anwendung speziell angepasst werden muss. Im Idealfall lassen sich also bestehende Spring-Boot-Anwendungen ausschließlich durch wenige zusätzliche Build-Instruktionen zu Native Executables kompilieren (Kasten: „In drei einfachen Schritten zur fertigen Anwendung“).

In drei einfachen Schritten zur fertigen Anwendung


  • Projekt auf https://start.spring.io erzeugen (Spring Web | Spring Native) und auspacken.

  • ./mvnw spring-boot:build-image (Build ausführen, Native Image wird kompiliert, Container-Image wird erzeugt, benötigt nur Docker)

  • docker run –rm -p 8080:8080 demo:0.0.1-SNAPSHOT (Beispielanwendung starten)

Ob es Sinn ergibt, jede Spring-Boot-Anwendung zu einem Native Executable zu kompilieren, anstatt die Anwendung in einer JVM laufen zu lassen, sei einmal dahingestellt. Diese Entscheidung hat weniger mit Spring Boot selbst zu tun als vielmehr mit dem Einsatzkontext der Anwendung.

Erste Schritte mit Spring Native

Wie beginnt man neue Spring-Boot-Projekte? Natürlich auf https://start.spring.io (bzw. den entsprechenden Wizards in der eigenen Lieblings-IDE).

<dependency>
  <groupId>org.springframework.experimental</groupId>
  <artifactId>spring-native</artifactId>
  <version>0.9.2</version>
</dependency>
<plugin>
  <groupId>org.springframework.experimental</groupId>
  <artifactId>spring-aot-maven-plugin</artifactId>
  <version>0.9.2</version>
  <executions>
    <execution>
      <id>test-generate</id>
      <goals>
        <goal>test-generate</goal>
      </goals>
    </execution>
    <execution>
      <id>generate</id>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <image>
      <builder>paketobuildpacks/builder:tiny</builder>
      <env>
        <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
      </env>
    </image>
  </configuration>
</plugin>

Als Beispiel wähle ich hier die Starter Spring Web, Spring Boot Actuator und eben Spring Native aus. Das generierte Projekt hat dann drei verschiedene Komponenten in der pom.xml-Datei, die speziell für Spring Native hinzugefügt wurden:

  1. eine zusätzliche Dependency (Listing 1)

  2. ein Build-Plug-in, das zusätzliche Informationen zur Build-Zeit erzeugt (Listing 2)

  3. ein Build-Plug-in, um ein Container-Image zu erzeugen (Listing 3)

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

Konfigurationen automatisch erzeugen

Die zusätzliche Dependency spring-native (aus Listing 1) beinhaltet vor allem die Spring-spezifische Erweiterung für den GraalVM-Native-Image-Compiler. Diese Erweiterung wird automatisch vom GraalVM-Native-Image-Compiler als Teil des Native-Image-Build-Prozesses ausgeführt.

Das neuartige Spring-AOT-Plug-in für den Build erzeugt die für das Native Image nötigen Konfigurationen automatisch während des Build-Vorgangs. Inhaltlich analysiert diese Build-Erweiterung die zu kompilierende Anwendung auf verwendete Spring-Komponenten und -Annotationen. Je nachdem, welche Spring-Bibliotheken und -Annotationen in der Anwendung verwendet werden, erzeugt Spring Native die passenden Konfigurationsdateien für den GraalVM-Native-Image-Compiler, sodass diese nicht manuell erstellt werden müssen.

Darüber hinaus kann die Spring-AOT-Erweiterung auch mit vielen Schaltern konfiguriert werden, um das Native Image noch genauer auf die eigenen Bedürfnisse zuzuschneiden. Beispielsweise lassen sich diverse Features von Spring komplett ausschalten und somit der dafür benötigte Code komplett aus dem Native Image entfernen.

Der etwas in die Jahre gekommene Support für Spring-XML-Config-Dateien ist ein gutes Beispiel dafür. Verwendet die Anwendung überhaupt keine Spring-XML-Config-Dateien, kann mit dieser Option der komplette XML-Support von Spring inkl. der dazu benötigten Dependencies gar nicht erst in das Native Image hineinkompiliert werden.

Die Spring-AOT-Erweiterung erlaubt es darüber hinaus, dem Native-Image-Support eigene sogenannte Hints mitzugeben. Diese „Hinweise“ geben dem Spring-Native-Support genaue Informationen darüber mit, welche Zusatzinformationen (beispielsweise zu Reflection) benötigt werden – sollten diese nicht automatisch identifiziert werden können.

Ein Beispiel dafür sind eigene Klassen, auf die zum Beispiel eine Library per Reflection zugreift, um sie in JSON zu transformieren.

Container-Images mit Native Executables

Spring Boot bringt schon seit einigen Versionen ein Maven-Build-Plug-in mit, welches automatisch ein Container-Image für die gebaute Spring-Anwendung erzeugt. Dieses Maven-Build-Plug-in nutzt im Hintergrund die Paketo Buildpacks [2], um aus kompilierten Spring-Boot-Anwendungen fertige Container-Images zu erzeugen.

Dieses Maven-Build-Plug-in (spring-boot-maven-plugin) kann so konfiguriert werden, dass vollautomatisch der GraalVM-Native-Image-Compiler verwendet und ein Native Executable erzeugt wird, welches dann in das Container-Image gelegt wird (anstatt eines JREs und den JAR-Dateien der Dependencys und der Anwendung selbst) – siehe Listing 3.

Ein großer Vorteil dieser Buildpack-basierten Methode ist, dass auf der lokalen Maschine kein passendes GraalVM SDK und keine Native-Image-Erweiterung installiert werden muss. Es reicht aus, die entsprechende Konfiguration (Listing 3) in die pom.xml-Datei zu integrieren und den Build auszuführen:

./mvnw spring-boot:build-image

Das Buildpack bringt das nötige GraalVM SDK automatisch mit. Das Resultat ist ein relativ kleines Container-Image. Es enthält weder ein vollständiges JRE noch die kompletten JAR-Dateien, sondern hauptsächlich das Binary der Anwendung.

Die eigentliche Größe des Binarys und dessen Speicherverbrauch im Betrieb hängt stark davon ab, wie gut und exakt zugeschnitten der Native-Image-Compiler konfiguriert wird. Je mehr Reflection-Informationen man beispielsweise konfiguriert, desto größer wird auch das Binary und desto mehr Speicher verbraucht es. Es kann sich also durchaus lohnen, möglichst wenig und möglichst genaue Reflection-Informationen zu konfigurieren, anstatt pauschal einfach alles.

Das gleiche gilt auch für die Erreichbarkeit von Code. Je genauer der Native-Image-Compiler analysieren kann, welcher Code nicht gebraucht wird, desto mehr Code wird er bei der Kompilierung des Binarys entfernen und desto weniger Ressourcen wird das Binary im Betrieb verbrauchen.

Sobald der Build das Container-Image mit dem Native Binary erzeugt hat, können wir den Container per Docker starten:

docker run --rm -p 8080:8080 rest-service:0.0.1-SNAPSHOT

Im Logoutput werden wir sehen: Die Spring-Boot-Anwendung startet innerhalb des Containers in wenigen Millisekunden.

Native Images lokal erzeugen

Ein Native Executable für eine Spring-Boot-Anwendung lässt sich auch ohne Buildpacks erzeugen. Wie im Artikel über die Native-Image-Technologie beschrieben, benötigt man dazu ein GraalVM SDK mit installierter Native Image Extension.

Anschließend lässt sich das GraalVM-Maven-Plug-in dem Build hinzufügen und passend für den Native-Image-Compiler konfigurieren (Listing 4). Zusätzlich sollte man in diesem Profil das Standardverhalten des Spring-Boot-Maven-Plug-ins leicht verändern (Listing 5), um einen Konflikt mit dem Repackaged JAR des Standard-Spring-Boot-Maven-Plug-ins zu vermeiden.

<profiles>
  <profile>
    <id>native-image</id>
    <build>
      <plugins>
        <plugin>
          <groupId>org.graalvm.nativeimage</groupId>
          <artifactId>native-image-maven-plugin</artifactId>
          <version>21.0.0.2</version>
          <configuration>
            <!-- The native image build needs to know the entry point to your application -->
            <mainClass>com.example.restservice.RestServiceApplication</mainClass>
          </configuration>
          <executions>
            <execution>
              <goals>
                <goal>native-image</goal>
              </goals>
              <phase>package</phase>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>
<plugins>
  <!-- ... -->
  <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
      <classifier>exec</classifier>
    </configuration>
  </plugin>
</plugins>

Auch für das lokal erzeugte Native Image muss die bereits erwähnte Spring Native Dependency ergänzt werden (Listing 1) sowie die Spring-AOT-Erweiterung (Listing 2). Diese beiden Erweiterungen im Build sind also für beide Varianten (Buildpack und lokaler Build) wichtig und sinnvoll.

Der Build wird dann (im hier beschriebenen Beispiel) ausgeführt mit:

./mvnw -Pnative-image package

Daraufhin wird im target-Directory das Native Executable abgelegt, das direkt ausgeführt werden kann:

./target/demo

Dieser lokale Native-Image-Compile-Schritt läuft direkt auf der eigenen Maschine ab und verwendet das native-image-Kommando des lokal installierten GraalVM-SDKs. Führt man diesen Build also beispielsweise auf einer Windows-Maschine aus, wird ein Windows Binary erzeugt. Das ist ein bedeutender Unterschied zur Buildpack-basierten Native-Image-Kompilierung. Das Buildpack erzeugt ein Linux-basiertes Container-Image, in dem das Native Image erzeugt wird und per Docker-Runtime ausgeführt werden kann.

Die Roadmap

Die nächsten Schritte für das Spring-Native-Projekt sind zum einen, stetig weiter den Ressourcenverbrauch über die unterschiedlichsten Projekte und Bibliotheken zu reduzieren. Aktuell lassen sich zwar schon recht viele Spring-Boot-Starter-Module mit Spring Native verwenden, aber nicht alle sind schon komplett auf Speicherverbrauch und Performance optimiert. Hier liegt noch einige Arbeit vor dem Spring-Team.

Darüber hinaus arbeitet eine Reihe von Projekten daran, möglichst viel von Spring Native zu unterstützen und automatisch zur Build-Zeit zu erzeugen. Auch hier sind viele Verbesserungen zu erwarten.

Nicht zuletzt werden mit den nächsten Releases auch kontinuierlich mehr Spring-Boot-Starter-Module und deren Dependencys unterstützt werden. Die aktuelle Liste der unterstützten Module kann man in der Dokumentation einsehen [3].

Fazit

Spring Native kann für Spring-Boot-Entwickler zu einem echten Gamechanger werden. Mit Spring Native werden Entwickler von Spring-Boot-Anwendungen in die Lage versetzt, alle Vorteile der GraalVM-Native-Image-Technologie zu nutzen, ohne die Spring-Boot-Anwendungen speziell dafür zu modifizieren oder gar auf ein anderes Framework zu portieren. Existierende und bereits seit Jahren in der Entwicklung und im Einsatz befindliche Spring-Boot-Anwendungen können mit Spring Native von der neuen GraalVM-Native-Image-Technologie profitieren – und so unter Umständen erhebliche Ressourcen einsparen.

Ohne Frage, das Spring-Native-Projekt steht noch ziemlich am Anfang. Es lassen sich noch nicht alle Spring Boot Starter damit nutzen und auch von den unterstützten Projekten sind noch nicht alle komplett für diesen Einsatz optimiert. Aber die Arbeit an dem Projekt geht mit großen Schritten voran und das Ziel ist extrem vielversprechend.

Links & Literatur

[1] Spring Native: https://github.com/spring-projects-experimental/spring-native

[2] Paketo Buildpacks und Spring Boot: https://spring.io/blog/2021/01/04/ymnnalft-easy-docker-image-creation-with-the-spring-boot-maven-plugin-and-buildpacks + https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-container-images-buildpacks

[3] https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/#support-spring-boot

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

]]>