quarkus - JAX https://jax.de/tag/quarkus/ Java, Architecture & Software Innovation Fri, 18 Oct 2024 12:53:13 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 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.

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

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

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

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

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

Stay tuned

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

 

Über 4drums.media

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

IntelliJ IDEA

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

Spring Boot und Bibliotheken

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

YouTube und Vimeo

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

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

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

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

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

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

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Vaadin

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

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

Abb. 3: … und das Theme gestalten

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

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

Abb. 4: Der Output von Listing 2

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

 

PostgreSQL

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

GitHub-Repository und Aktionen

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

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

Stay tuned

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

 

Uberspace

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

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

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

Fazit

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


Links & Literatur

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

[2] https://4drums.media

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

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

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

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

[7] https://joinpeertube.org

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

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

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

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

[12] https://uberspace.de

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

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

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

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

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

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

]]>
GraalVM

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

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

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

SubstrateVM

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

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

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

Mein Hintergrund: Library-Autor

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

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

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

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

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

Frameworks

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

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

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

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

Native Image

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

Installation der GraalVM und des Native-Image-Tools

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

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

 

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

 
package ac.simons.native_story.trivial;

public class Application {

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

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

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

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

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

native-image wird Output wie in Listing 2 erzeugen.

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

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

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

Ein fiktives Framework

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

 
public interface Service {

  String sayHelloTo(String name);

  String getGreetingFromResource();
}

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

 
public class ServiceFactory {

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

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

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

public class ServiceImpl implements Service {

  private final TimeService timeService = new TimeService();

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

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

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

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

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

 
import java.lang.reflect.Method;

public class Application {

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

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

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

  static String invokeGreetingFromResource(Service service, String theName) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Zwei Varianten

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

 

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

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

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

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

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

Damit entsteht ein korrekt lauffähiges, schnelles Binary.

Substitutions (Ersetzungen)

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

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

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

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

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

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

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

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

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

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

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

class CustomSubstitutions {
}

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

Der Tracing-Agent

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

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

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

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

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

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

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

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

Fazit

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

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

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

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

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

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

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

Hinweise

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

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

 

Quarkus-Spickzettel


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

 

Jetzt herunterladen!

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

]]>