JAX https://jax.de/ Java, Architecture & Software Innovation Thu, 24 Apr 2025 09:20:08 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Kubernetes: Clustergrenzen überschreiten mit Multi-Mesh https://jax.de/blog/kubernetes-istio-multi-mesh-strategie/ Thu, 24 Apr 2025 09:19:01 +0000 https://jax.de/?p=107334 Ein Service Mesh, das mehrere Kubernetes-Cluster zu einem logischen Mesh verbindet, wird als Multi-Mesh bezeichnet. Durch die Erweiterung über Clustergrenzen hinweg lassen sich Vorteile wie Ende-zu-Ende-Verschlüsselung, Cross-Cluster-Routing und verbesserte Observability auf alle Cluster ausdehnen. In diesem Artikel erfahren Sie, wie Unternehmen, die bereits ein Service Mesh wie Istio nutzen, ihre Infrastruktur mit Multi-Mesh effizienter und skalierbarer gestalten können.

The post Kubernetes: Clustergrenzen überschreiten mit Multi-Mesh appeared first on JAX.

]]>
In der Anfangszeit von Kubernetes haben Unternehmen häufig mit zwei Arten von Clustern operiert: einem für die Produktion und einem für nichtproduktive, vorgelagerte Tests oder die Entwicklung. Der Grund für dieses Vorgehen war die Vermeidung von Komplexität und das Bestreben, den operativen Aufwand für den Betrieb der Cluster gering zu halten. Falls doch mehrere Stages (Development, Test, Integration, Pre-Production) benötigt wurden, hat man diese Umgebungen über verschiedene Namespace-Suffixe oder -Präfixe realisiert.

Nachdem die neue Komplexität beherrschbar wurde, konnte man darüber nachdenken, ob eine Aufteilung der Cluster in Stages nicht weitere Vorteile mit sich bringen würde. Auch der Betrieb mehrerer produktiver Cluster wurde möglich. Die Abwägung der Vor- und Nachteile für diese Multi-Cluster-Umgebungen ist dabei sehr vielfältig. Mögliche Gründe dafür können unter anderem sein:

  • unterschiedliche Zugriffsberechtigungen (allgemeine Security- oder regulatorische Vorgaben)
  • Blast-Radius bei Systemausfällen (Vermeidung eines Single Point of Failure)
  • operative Flexibilität innerhalb des Clusters und Reduzierung der Clustergröße
  • Zweckbindung des Clusters für bestimmte Workloads
  • Notwendigkeit der Migration/Transition des Clusters

Diese Liste kann sicherlich noch erweitert werden, aber eine tiefergehende Diskussion dieser Argumente würde den Rahmen dieses Artikels sprengen. Für den weiteren Verlauf unserer Diskussion ist es ausreichend, zu wissen, dass wir uns mit mehreren Kubernetes-Clustern beschäftigen müssen.

Multi-Cloud

Auf der anderen Seite haben Unternehmen begonnen, die Public Cloud in die eigene Infrastruktur zu integrieren. Die Infrastruktur wurde in die Cloud ausgeweitet oder man hat sich dafür entschieden, alle notwendigen Ressourcen komplett in die Cloud zu verlagern. Um bei dieser Strategie noch eine gewisse Unabhängigkeit vom Cloud-Provider zu haben, sind in Unternehmen mögliche Cloud-Migrationsszenarien ein wichtiges Thema. Sollte man sich für einen Wechsel des Cloud-Providers entscheiden, hat man zumindest einen Plan, wie dieser umgesetzt werden kann. Andere begeben sich erst gar nicht in diese Abhängigkeit, indem sie von Anfang an ihre Infrastruktur bei mehreren Cloud-Providern betreiben.

Multi-Mesh

Unternehmen, die sich für den Einsatz eines Service Mesh wie Istio entschieden haben, werden nun mit der oben beschriebenen Multi-Cluster-Strategie vor eine neue Entscheidung gestellt. Soll das Service Mesh nur innerhalb der jeweiligen Cluster operieren oder will man seine Vorzüge auch über Clustergrenzen hinweg nutzen? Dann spricht man von Multi-Mesh.

Im Fall einer Multi-Cloud-Strategie muss man sich sogar mit der Option beschäftigen, das Service-Mesh über die Grenzen der Infrastruktur hinweg zu betreiben – also ein Multi-Mesh über den Cloud-Provider und die On-Premise-Umgebung hinweg aufspannen. Sogar der Betrieb über mehrere Cloud-Provider hinweg ist denkbar.

Die erste Frage, die sich bei einem Multi-Mesh stellt, ist: Welche Vorteile bringt es? Im Grunde werden alle Vorteile, die ein Service Mesh mit sich bringt, transparent auf alle Cluster ausgeweitet. Einige der wichtigsten Pluspunkte schauen wir uns in den folgenden Abschnitten genauer an.

Ende-zu-Ende-Verschlüsselung und Berechtigungsprüfungen

Eine Ende-zu-Ende-Verschlüsselung, die mit dem Service Mesh innerhalb eines Clusters möglich war, wird nun auch über die Clustergrenzen hinweg möglich. Die mTLS-Verschlüsselung erstreckt sich ohne Unterbrechung vom Workload im aufrufenden Cluster (Downstream) bis zum Workload im aufgerufenen Cluster (Upstream). Damit kann die kryptografisch gesicherte Identität des Aufrufers im Workload des Upstream-Clusters geprüft werden.

Die Grundlage für Verschlüsselung und Berechtigungsprüfungen für den clusterübergreifenden Aufruf sind damit etabliert. Die notwendigen Regeln für die Berechtigungsprüfungen (Istio AuthorizationPolicy) sind ohne weitere Anpassungen einsetzbar.

Stay tuned

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

 

Cross-Cluster-Routing

Das Routing, das von Istio gesteuert und kontrolliert wird, kann in einem Multi-Mesh für den Aufrufer völlig transparent an den entsprechenden Cluster weitergeleitet werden. Ein Upstream Workload kann also von einem Cluster in einen anderen verschoben werden, ohne dass dies Einflüsse auf den Aufrufer hätte. Es müssen hierbei keine Konfigurationen am Aufrufer geändert werden. Die Verschiebung der Upstream Workloads in einen anderen Cluster kann sogar ohne Unterbrechung (Zero Downtime) im laufenden Betrieb geschehen. Der Service-Name des Upstream Workloads, in der Regel service.namespace.svc.cluster.local muss dafür nicht geändert werden.

Werden die Upstream Workloads in mehreren Clustern deployt, so kann mit einer kleinen Anpassung der Routingregel ein sog. Locality-based Routing erreicht werden. Durch verschiedene Routingstrategien kann der jeweils nächstgelegene Upstream Workload aufgerufen werden. Ist dieser im selben Cluster verfügbar, wird der Request an diese Instanzen weitergeleitet. Andernfalls erfolgt das Routing in einem anderen Cluster. Welcher Zielcluster hierfür verwendet werden soll, kann ebenfalls mit einer passenden Routingregel festgelegt werden. So kann ein gewünschter Kompromiss zwischen Latenz, Verfügbarkeit und Clusterauslastung erreicht werden.

Für dieses Cross-Cluster-Routing können dieselben Resilienzregeln wie bei einer clusterinternen Kommunikation verwendet werden. Das Multi-Mesh kennt zu jedem Zeitpunkt den Zustand der Workloads – genau wie das Service Mesh bei einem einzigen Cluster. Damit ist es möglich, vordefinierte Resilienzpatterns wie Circuit Breaker oder Bulkhead über die Clustergrenze hinweg anzuwenden. Ausfälle einzelner Workloads in anderen Clustern oder sogar ganzer Cluster werden damit für den Aufrufer kompensiert, ohne dass er etwas davon mitbekommt.

Cross-Cluster-Observability

Durch das transparente Routing der Aufrufe über die verschiedenen Cluster hinweg, kann der Überblick zur Laufzeit sehr unübersichtlich werden. Mit Istio im Multi-Mesh-Set-up kann die Frage: „In welchem Cluster wurde der Workload aufgerufen?” sehr einfach beantwortet werden. Das sog. Tracing, das bereits im Service-Mesh vorhanden ist, wird hier auch für die clusterübergreifende Kommunikation verwendet. Es kommen dabei dieselben Trace- bzw. Observability-Header zum Einsatz.

Die Infrastruktur muss hierfür eine einzige zusätzliche Voraussetzung erfüllen. Die Tracedaten der jeweiligen Cluster müssen an ein zentrales Tracing-Backend übermittelt werden. Istio integriert hierfür den OpenTelemetry-Standard, der diese weitreichenden Funktionalitäten mitbringt.

Um ein Multi-Mesh aufzuspannen, sind ein paar Voraussetzungen zu erfüllen. Ohne zu tief in die jeweiligen Themen einzusteigen, soll die folgende Beschreibung eine erste Einschätzung im Sinne Pro und Contra Multi-Mesh bzw. von dessen Aufwand und Nutzen für die eigene Systemlandschaft ermöglichen. In der Regel ist das Upgrade von einem Service auf ein Multi-Mesh mit vertretbarem Aufwand möglich.

Gemeinsame Trust Domain

Für die mTLS-Verschlüsselung über Clustergrenzen hinweg, muss eine sog. Trust Domain etabliert werden. Im klassischen Istio-Betrieb wird jedem Workload ein Istio Proxy als Sidecar zugewiesen (Das klassische Istio verwendet Sidecars für jeden Workload. Das neue Istio Ambient verzichtet darauf. Die Funktionsumfang des Service Mesh ist dabei derselbe). Ein Kubernetes Pod besteht somit aus zwei Containern. Das Sidecar übernimmt transparent für den Workload-Container die Kommunikation und kümmert sich auch um die Verschlüsselung mittels mTLS. Dazu erhält das Sidecar ein Zertifikat, das von der sog. Control Plane von Istio (istiod) alle 24 Stunden neu ausgestellt wird. Die Service-Mesh-Kommunikation innerhalb eines Clusters wird aus Sicht des SSL-Protokolls mit zwei Zertifikaten etabliert, die alle von derselben Certificate Authority (CA) stammen.

Eine mTLS-Verschlüsselung von einem Cluster zu den anderen muss demselben SSL-Protokoll entsprechen. Die Zertifikate in den Sidecars der beiden Cluster müssen von derselben CA ausgestellt worden sein. Um diese Voraussetzung im Multi-Mesh zu erfüllen, müssen die Zertifikate in den jeweiligen Control Planes der Cluster von derselben Root CA abgleitet worden sein (Abb. 1).

Abb. 1: Gemeinsame Trust Domain [1]

Als Vorbereitung wird eine Root CA außerhalb des Clusters verwendet, um davon sog. intermediate CAs abzuleiten. Jede Control Plane erhält dabei ihr eigenes intermediate CA, mit dem sie dann wiederum die Zertifikate der Sidecars signiert. Als Ergebnis besitzt jedes Sidecar im Multi-Mesh ein Zertifikat, das über die Zertifikatskette hinweg von derselben Root CA stammt. Damit sind aus Sicht des SSL-Protokolls die Voraussetzungen geschaffen, um über die Clustergrenze hinweg eine mTLS-Verbindung zu etablieren.

Die Konfiguration dieser Trust Domain lässt sich noch vereinfachen bzw. vereinheitlichen, sofern eine externe Public Key Infrastructure (PKI) vorhanden ist. Die Service Mesh Control Plane kann angewiesen werden, das Signieren der Zertifikate der Sidecars an diese PKI zu delegieren. So können alle Cluster des Multi-Mesh mit dieser PKI verknüpft werden. Die Cloud-Provider bieten solche PKI Services als Managed Services an.

Deployment Model auswählen

Das Multi-Mesh kann in Form verschiedener Topologien eingerichtet werden. Für die Auswahl der passenden Topologie sind hierbei die Netzwerkzonen, in denen die jeweiligen Kubernetes-Cluster eingerichtet werden, und der Grad der Ausfallsicherheit der Control Planes ist ausschlaggebend. Istio unterscheidet dabei folgende Variationen für die Redundanz der Control Planes:

  • Primary Remote
  • Multi-Primary

Bei Primary Remote existiert nur eine Control Plane, die für alle Cluster im Multi-Mesh zuständig ist. Diese Topologie ist einfach einzurichten und zu betreiben, aber bzgl. Ausfallsicherheit nicht so hochwertig wie Multi-Primary. Hier besitzt jeder Cluster seine eigene Control Plane (Abb. 2).

Abb. 2: Multi-Primary Multi-Network [2]

Bei der Auswahl der geeigneten Topologie muss daher ein Kompromiss zwischen Ausfallsicherheit und Konfigurationsaufwand gefunden werden. Da aber eine Multi-Cluster-Umgebung ohne Automatisierung zur Erstellung der Cluster nicht ratsam ist, kann der Konfigurationsaufwand vernachlässigt werden. Es bleibt somit nur das Argument der Ausfallsicherheit und diese ist im Multi-Primary-Modus definitiv besser. Die Anzahl der im Multi-Mesh betriebenen Cluster ist eine weitere Stellgröße für die Auswahl der Topologie. Je mehr Cluster es werden, desto eher sollte man sich für Multi-Primary entscheiden.

Endpoint Discovery

Das Traffic Routing, das ebenfalls von den Sidecars ausgeführt wird, steuert die Control Plane. Diese informiert die Sidecars über alle Workloads, die sich im Cluster bzw. im Multi-Mesh befinden. Darüber hinaus wird den Sidecars noch mitgeteilt, über welche Netzwerkverbindungen sie die Workloads in den anderen Clustern im Multi-Mesh erreichen können (siehe nächster Abschnitt).

Hierfür benötigt die Control Plane aktuelle Informationen bzgl. Kubernetes Services und Kubernetes Endpoints aus der Service Registry der jeweiligen Cluster. Da diese Informationen im Kubernetes API hinterlegt sind, fragen die Control Planes sie regelmäßig dort ab. Für die Workloads eines Partnerclusters muss die Control Plane daher auf das Kubernetes API des Partnerclusters zugreifen (Abb. 3).

Abb. 3: Endpoint Discovery [3]

In der Regel sind diese externen Zugriffe auf das Kubernetes API durch Security geschützt. Folglich benötigt die Control Plane die notwendigen Tokens aller Partnercluster, um sich dort die benötigten Informationen zu holen.

Änderungen im Kubernetes API des Partnerclusters werden periodisch abgefragt und an die Sidecars des eigenen Clusters weitergeleitet. Auf diese Weise sind die Sidecars zeitnah über den Zustand der Workloads aller verknüpften Cluster informiert und können die Kommunikation zielgerichtet dorthin lenken.

East-West Gateways

In der Regel werden die Kubernetes-Cluster in eigenen Netzwerkzonen (Subnetze) installiert. Um diese Subnetze in der übergreifenden Kommunikation zu verbinden, benötigt man einen Proxy, der den Eingang in das jeweilige Subnetz ermöglicht. Das ist für Kubernetes-Cluster nichts Neues, da so in der Regel die Ingress-Kommunikation aufgebaut wird. Im Fall einer Cluster-zu-Cluster-Kommunikation muss ein solcher Proxy ebenfalls zur Verfügung stehen. Istio setzt hierbei auf dasselbe Ingress Gateway, das schon für die Ingress-Kommunikation empfohlen wird. Nur diesmal wird dieses Gateway als sog. East-West Gateway konfiguriert. Das bedeutet, dass eine eingehende Verbindung aus dem Partnercluster über einen Auto-Pass-through-Modus an den aufgerufenen Workload weitergeleitet wird (Abb. 4).

Abb. 4: East-West Gateways [4]

Die vom aufrufenden Sidecar initiierte mTLS-Verbindung wird nicht im East-West Gateway terminiert, sondern an das aufgerufene Sidecar weitergeleitet. Der mTLS Handshake findet zwischen diesen beiden Sidecars statt. Die mTLS-Verbindung erfolgt also analog zur Kommunikation im Service Mesh, die innerhalb eines Clusters stattfindet. Im Wesentlichen ist das der Grund dafür, dass für eine Multi-Mesh-Kommunikation dieselben Funktionalitäten wie bei einer Service-Mesh-Kommunikation zur Verfügung stehen.

Für die genannten Voraussetzungen (Trust Domain, Deployment Model, Endpoint Discovery und East-West Gateway) liefert Istio passende Utilities mit, die die Konfiguration der entsprechenden Cluster sehr stark vereinfachen. Es bleibt also nur noch die Aufgabe, diese Utilities oder deren Ausgaben (Kubernetes-Ressourcendateien) in die DevOps Pipeline zu integrieren.

Stay tuned

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

 

Anwendungsbeispiele

Die folgenden Beispiele stellen nur ein Teil der Funktionalitäten dar, die in einem Multi-Mesh möglich sind. Vorhandene Istio-Regeln, die bereits in einem Service-Mesh zum Einsatz kommen, können ganz ohne oder mit nur geringen Anpassungen spezielle Multi-Mesh-Funktionalitäten nutzen.

Locality-based Routing

Workloads, die in verschiedenen Clustern deployt sind, können über eine definierte Aufrufverteilung angesprochen werden. Diese Cluster werden mit Hilfe von Regions und Zones gekennzeichnet, wobei eine Region in verschiedene Zones unterteilt wird. Die Aufteilung in Region/Zone ist bei den bekannten Cloud-Providern für die Deklaration ihrer Verfügbarkeitszonen üblich.

Mit der Istio DestinationRule in Listing 1 kann beim Aufruf von helloworld.sample.svc.cluster.local folgende Verteilung festgelegt werden: 70 Prozent aller Aufrufe, die innerhalb von region1/ zone1 initiiert werden, sollen in derselben Region und Zone bleiben: from: “region1/zone1/” to: “region1/zone1/”: 70

Zwanzig Prozent der Aufrufe verbleiben in derselben Region (region1), werden aber in die Zone zone2 geleitet, und die restlichen zehn Prozent werden in eine andere Region (region3) und dort in die Zone zone4 geroutet.

Listing 1

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: helloworld
spec:
  host: helloworld.sample.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      localityLbSetting:
        enabled: true
        distribute:
        - from: "region1/zone1/*"
          to:
            "region1/zone1/*": 70
            "region1/zone2/*": 20
            "region3/zone4/*": 10
 Das Sidecar des Pods, in dem der Aufruf initiiert wird, übernimmt diese Regel und verteilt die Aufrufe seines Workload-Containers entsprechend. Aus Sicht des Aufrufers zeigt der DNS-Name des aufgerufenen Hosts (helloworld.sample.svc.cluster.local) auf einen lokalen Servicenamen, d. h., die Weiterleitung in einen anderen Cluster erfolgt für den Aufrufer vollkommen transparent.

Locality-based Routing mit Fail-over

Um sich vor dem Ausfall einer Region oder Zone zu schützen, können sog. Fail-over-Regeln erstellt werden. Eine Weiterleitung in eine andere Region/Zone erfolgt also nur im Fehlerfall. Diese Art der Weiterleitung verwendet ebenfalls eine Istio DestinationRule (Listing 2).

Listing 2

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: helloworld
spec:
  host: helloworld.sample.svc.cluster.local
  trafficPolicy:
    connectionPool:
      http:
        maxRequestsPerConnection: 1
      loadBalancer:
        simple: ROUND_ROBIN
        localityLbSetting:
          enabled: true
          failover:
          - from: region1
            to: region2
      outlierDetection:
        consecutive5xxErrors: 1
        interval: 1s
        baseEjectionTime: 1m
 Sobald ein Aufruf von helloworld.sample.svc.cluster.local als fehlerhaft erkannt wird, greift die sog. outlierDetection wie folgt: ein HTTP-5xx-Statuscode, der in einem Intervall von einer Sekunde (1s) auftritt, führt dazu, dass der aufgerufene Pod für eine Minute (1m) nicht mehr angesprochen wird. Da die einzelnen Pods von helloworld bekannt sind, wird nur dieser eine defekte Pod nicht mehr aufgerufen. Alle anderen Pods, die fehlerfrei arbeiten, werden weiterhin angesprochen.

Fazit

Eine Entscheidung zugunsten Multi-Cluster wird von einem Service Mesh nicht blockiert. Das Gegenteil ist der Fall. Ein Service Mesh, das bei einer Multi-Cluster-Umgebung zu einem Multi-Mesh erweitert wird, kann diese Entscheidung sogar fördern. Ein Wechsel von Single- zu Multi-Cluster, ohne dabei das Service Mesh zu berücksichtigen, ist allerdings wenig sinnvoll. Der Nutzen eines Multi-Mesh überwiegt in der Regel den Aufwand, der für seinen Aufbau erforderlich ist. Das geht sogar so weit, dass auch eine kurzfristige Multi-Cluster-Strategie mit einem Multi-Mesh harmoniert. Eine kurzfristige Multi-Cluster-Strategie kann beispielsweise bei einer Migration von On-Premise in die Cloud (oder umgekehrt) oder bei einem Wechsel des Cloud-Providers vorliegen. Ob nach dieser Migration noch Multi-Cluster benötigt werden, kann dann wieder gesondert überlegt werden.

Links & Literatur

[1] https://istio.io/latest/docs/tasks/security/cert-management/plugin-ca-cert/

[2] https://istio.io/latest/docs/setup/install/multicluster/multi-primary_multi-network/

[3] https://istio.io/latest/docs/ops/deployment/deployment-models/#endpoint-discovery-with-multiple-control-planes

[4] https://istio.io/latest/docs/ops/deployment/deployment-models/#multiple-networks

The post Kubernetes: Clustergrenzen überschreiten mit Multi-Mesh appeared first on JAX.

]]>
Spring Boot vs. Quarkus: Ein direkter Vergleich aus der Praxis für Entwickler https://jax.de/blog/spring-boot-vs-quarkus-praxisvergleich/ Thu, 13 Mar 2025 14:46:58 +0000 https://jax.de/?p=107257 Bei neuen Projekten oder Frameworkmigrationen konzentriert sich die Diskussion über moderne Java-Frameworks oft auf zwei Namen: Spring Boot und Quarkus. Beide haben ihre Vorzüge und versprechen, das Leben von Entwicklern einfacher zu machen. Aber welches Framework ist nun besser geeignet? Ein Praxistest.

The post Spring Boot vs. Quarkus: Ein direkter Vergleich aus der Praxis für Entwickler appeared first on JAX.

]]>
Ich möchte mit meiner persönlichen Reise in der Softwareentwicklung beginnen, um meine Perspektive für den Vergleich zu setzen. Am Anfang meiner beruflichen Laufbahn war ich in einem großen Projekt, das alles andere als modern war. Die Anwendung war ein typischer „Big Ball of Mud“: eine Mischung aus Servlet, JSP, JSF1 und JSF2. Irgendwann haben wir uns entschieden, eine neue einheitliche Architektur mit dem damals modernen Java EE 5 bzw. 6 zu bauen. Das war meine erste Erfahrung mit Java EE. In dieser Zeit habe ich viel über die grundlegenden Konzepte gelernt, wie man Java EE sinnvoll einsetzt.

Nach diesem Projekt arbeitete ich in einem Unternehmen, das Java-EE-Systeme beim Kunden einsetzte. Das System lief meistens on Premise beim Kunden. Wir hatten eine einzige Codebasis, die auf über hundert Kundensystemen lief – mit jeweils unterschiedlichen Konfigurationen, insbesondere mit verschiedenen Kombinationen aus Application-Server und Datenbank. Das war mein erster Kontakt mit der Idee von Kompatibilität in Java EE. Ich habe erlebt, wie robust diese Technologie sein kann, selbst wenn sie auf verschiedenen Application-Servern wie WebSphere, JBoss oder NetWeaver eingesetzt wird.

Stay tuned

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

 

Ab 2018 wechselte ich zu Spring Boot. Und was soll ich sagen: Es war ein Gamechanger. Viele Aufgaben, die früher mühsam waren, wurden durch Spring Boot praktisch und komfortabel automatisiert. Ein besonderes Highlight waren die Datenbank-Repositories mit der automatischen Generierung der JPQL-Querys aus dem Methodennamen. Das fand ich genial.

Als ich dann im Jahr 2023 ein Quarkus-Projekt angeboten bekam, wollte ich es eben wegen Quarkus haben. Meine Erwartung war, dass sich die Vorzüge von Spring Boot und Jakarta EE (ehemals Java EE) vereinen: moderne, komfortable Entwicklung zusammen mit offiziellen Standards, die man nur einmal lernen muss und die einfacher zu migrieren sind. Ich wollte verstehen, wie sich dieses neue Framework im Vergleich zu Spring Boot schlägt. Nach den ersten Monaten mit Quarkus war die Idee zu einem Vortrag geboren, der die Grundlage für diesen Artikel bildet.

Spring Boot und Quarkus

Spring Boot ist ein Open-Source-Framework, das von VMware entwickelt wurde. Es wurde 2013 veröffentlicht und basiert auf dem Spring-Framework, das seit 2003 existiert. Spring Boot ermöglicht es Entwicklern, eigenständige, produktionsreife Anwendungen zu erstellen, die ohne großen Aufwand direkt einsatzbereit sind. Auf GitHub wird es mit 75,1 k Stars, 40,7 k Forks und 3,4 k Beobachtern (Stand: November 2024) verzeichnet, und es gibt über 528 000 Ergebnisse für Spring-Boot-Projekte.

Quarkus wiederum ist ein Open-Source-Framework von Red Hat, das 2019 veröffentlicht wurde. Es ist speziell für Java-Anwendungen optimiert, die in Kubernetes-Umgebungen laufen. Quarkus basiert auf Eclipse MicroProfile und nutzt Technologien wie OpenJDK HotSpot und GraalVM. Auf GitHub hat Quarkus 13,8 k Stars, 2,7 k Forks und 259 Beobachter. Es gibt 24,6 k Quarkus-bezogene Repositories (Stand: November 2024).

Mein Vergleich: Entwicklererfahrungen im Fokus

Im Folgenden will ich nicht die technischen und operativen Vorteile von Quarkus vergleichen. Vergleiche der Startzeiten und des Speicherverbrauchs können an anderer Stelle nachgelesen werden. Hier fokussiere ich mich bewusst auf die Developer Experience.

Der Vergleich zwischen Spring Boot und Quarkus basiert auf meiner praktischen Arbeit mit beiden Frameworks. Als roten Faden habe ich eine Spring-Boot-Anwendung möglichst ähnlich mit Quarkus nachgebildet. Für die Vergleichbarkeit habe ich folgende Rahmenbedingungen gesetzt:

  • Datenbankschema und Frontend unverändert: Die JPA-Entities und das REST-basierte Frontend sollten möglichst eins zu eins austauschbar sein.
  • Im eigenen Ökosystem bleiben: Es gibt Möglichkeiten, Teile eines Frameworks in anderen zu nutzen, z. B. JAX-RS in Spring Boot oder Spring Data in Quarkus. Für den Vergleich der Developer Experience habe ich das bewusst ausgeschlossen.
  • Keine Out-of-the-box-Endpoints: Automatisch generierte Endpoints, wie sie etwa Spring Data REST bietet, wurden nicht berücksichtigt, da hier oft die Möglichkeit fehlt, individuelle fachliche Logik einzubauen.

Für den Vergleich habe ich folgende Aspekte untersucht:

  • Dokumentation
  • Start
  • REST-Interface
  • Dependency Injection
  • ORM
  • Authentication
  • Developer-Tools
  • Docker Builds
  • Testing
  • Native

Bei der Spring-Boot-Anwendung handelt es sich um eine einfache Blogsoftware, die ein REST-Interface hat, zudem Spring Boot 3.3, Spring Security mit Basic Auth, Daten in einer PostgreSQL-Datenbank speichert und über ein einfaches Frontend mit Vanilla JS, Fetch API und Bootstrap verfügt. Diese Software verwende ich üblicherweise als Lehrbeispiel, da man hier alle Aspekte einer modernen Webanwendung findet und sie dennoch einfach genug ist. Die originale Spring-Boot-Anwendung ist unter [1] zu finden, die migrierte Quarkus-Anwendung unter [2].

Dokumentation

Die Dokumentation von Spring Boot [3] ist umfangreich und gut strukturiert. Es gibt offizielle Guides, die von der Installation bis zu fortgeschrittenen Themen reichen.

Quarkus setzt auf einen pragmatischen Ansatz [4]. Die Guides sind oft kürzer, dafür aber praxisorientiert. Besonders hervorzuheben ist die spezifische Dokumentation zur nativen Image-Erstellung und Kubernetes-Integration.

Hier will ich noch anmerken, dass Red Hat das Buch „Quarkus for Spring Developers“ von Eric Deandrea [5] kostenfrei zur Verfügung stellt, was eine gute Quelle für Umlernende ist. Zudem stimme ich Adam Bien zu, wenn er auf die Kritik an der Quarkus-Doku eingeht, dass die Quarkus-Guides zu komplex seien. Da Quarkus auf dem MicroProfile des Jakarta-EE-Standards basiert [6], kann man diese ebenfalls gut nutzen und muss sie nicht extra bei Quarkus aufführen; dazu ist das Video „How To Learn Quarkus“ von Adam Bien empfehlenswert [7].

Mein Fazit zur Dokumentation: Beide Frameworks bieten eine solide Dokumentation. Hier gibt es keinen klaren Gewinner.

Start

Sowohl Spring Boot mit dem Spring Initializr [8] als auch Quarkus [9] bieten Websites an, um ein Projekt-Set-up einfach zu generieren (Abb. 1 und 2). Sie existieren jeweils auch als CLI-Werkzeug und sind auch in den gängigsten IDEs integriert. Mein Fazit zum Start: Beide Frameworks bieten hier die gleiche Funktionalität an.

Abb. 1: Projekt-Set-up mit Spring Initializr …

Abb. 2: … und mit Quarkus

REST-Interface

Spring Boot macht die Arbeit mit REST-Interfaces besonders einfach. Mit Annotationen wie @RestController und @GetMapping fügt es sich perfekt in das Spring-MVC-Framework ein. Besonders praktisch: Funktionen wie die Paginierung sind schon eingebaut und können ohne großen Aufwand genutzt werden. Hier das Beispiel eines GET Endpoint mit Paginierung in Spring Boot:

@GetMapping(path = "/entries", produces = MediaType.APPLICATION_JSON_VALUE)

public Page<Entry> getAllEntries(@ParameterObject Pageable pageable) {

  return entryService.getAllEntries(pageable);

}

Quarkus hingegen setzt auf den JAX-RS-Standard und verwendet Annotationen wie @Path und @GET. Es integriert RESTEasy sowie andere JAX-RS-Implementierungen und bietet damit eine saubere und standardisierte Lösung. Allerdings fehlt eine direkte Unterstützung der Paginierung innerhalb des ORM, was zusätzlichen Entwicklungsaufwand erfordern kann. Die Nachimplementierung dieser Funktionalität empfand ich als besonders umständlich. Das Beispiel eines GET Endpoints mit manueller Paginierung in Quarkus zeigt Listing 1.

Listing 1

@GET

@Transactional

public Response getAllEntries(

  @QueryParam("sort") List<String> sortQuery,

  @QueryParam("page") @DefaultValue("0") int pageIndex,

  @QueryParam("size") @DefaultValue("10") int pageSize) {

  // Idea from https://quarkus.io/guides/rest-data-panache#hr-generating-resources

  // But I did not find the getSortFromQuery, so I implemented it my self

  Page page = Page.of(pageIndex, pageSize);

  Sort sort = getSortFromQuery(sortQuery);

  List<Entry> entires = entryService.getAllEntries(sort, page).list();

  Long allEntriesCount = entryService.getAllEntriesCount();

  PageOutput<Entry> pageOutput = PageOutput.of(entires, pageIndex, pageSize, allEntriesCount);

 

  return Response.ok(pageOutput).build();

}

Mein Fazit zum REST-Interface: Spring Boot punktet hier durch die sofort einsatzbereite Paginierung und die saubere Möglichkeit zur Änderung des Standard-HTTP-Codes mittels der @ResponseStatus-Annotation gegenüber Quarkus.

Dependency Injection

Spring Boot nutzt die für das Spring-Framework typischen Annotationen wie @Autowired, @Component und @Service für die Dependency Injection. Eine Schwäche ist das sogenannte Self-Inject-Problem: Wenn sich eine Bean selbst injiziert, können Probleme auftreten, da man das proxy-Objekt nicht aufruft und so z. B. keine neue Transaktion auslöst, obwohl es so annotiert ist. Die Definition eines Service in Spring Boot mit Konstruktorinjektion zeigt Listing 2.

Listing 2

@Service

public class EntryService {

   private final EntryRepository entryRepository;

  private final TagRepository tagRepository;

   public EntryService(EntryRepository entryRepository, TagRepository tagRepository) {

    this.entryRepository = entryRepository;

    this.tagRepository = tagRepository;

  }

 [...]

}

Quarkus setzt auf den CDI-Standard. Annotationen wie @Inject, @ApplicationScoped und @Singleton sorgen für eine saubere Integration. Auch hier tritt das Self-Inject-Problem auf, was zeigt, dass es kein spezifisches Problem von Spring Boot ist. Die Definition eines Service in Quarkus mit Konstruktorinjektion zeigt Listing 3.

Listing 3

@ApplicationScoped

public class EntryService {

 

  private final EntryRepository entryRepository;

  private final TagRepository tagRepository;

 

  public EntryService(EntryRepository entryRepository, TagRepository tagRepository) {

    this.entryRepository = entryRepository;

    this.tagRepository = tagRepository;

  }

[...]

}

Mein Fazit zur Dependency Injection: Beide Frameworks sind in diesem Bereich gleichwertig.

ORM (Object-relational Mapping)

Spring Boot verwendet Spring Data JPA. Die Möglichkeit, Methoden einfach durch interface-Definitionen zu implementieren, ist ein großer Vorteil. Zudem reicht ein JDBC-Treiber, um eine Datenbank anzubinden. Ein Spring Data Repository mit interface-Definition sieht so aus:

public interface EntryRepository extends JpaRepository<Entry, Long> {

  List<Entry> findByTags_NameOrderByCreatedDesc(String tagName);

  List<Entry> findByAuthor(BlogUser author, Pageable pageable);

}

Quarkus bietet Panache als Ergänzung zu JPA. Diese Lösung ist zwar elegant, erreicht aber nicht den Komfort von Spring Data JPA. Jakarta Data könnte hier irgendwann einmal die gleiche Funktionalität liefern, aber die Bibliothek ist noch nicht so weit, den vollen Umfang abzubilden, den Spring Data bietet. Besonders auffällig ist, dass Quarkus bei der Standardgenerierung von Datenbankschemas Unterschiede aufweist, was bei Migrationen zu Problemen führen kann. Hier musste ich in der application.properties den Wert quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy setzen, um das gleiche Verhalten zu erhalten. Außerdem scheinen die Möglichkeiten, Datenbanken einzubinden, eingeschränkt, da hier ein einfacher JDBC-Treiber nicht ausreicht, sondern eine JDBC Driver Extension dafür existieren muss, siehe [10]. Für die gängigsten Datenbanken findet man diese zwar, aber nicht für alle. Ein Beispiel für Quarkus mit JPQL und PanacheRepository zeigt Listing 4.

Listing 4

@ApplicationScoped

public class EntryRepository implements PanacheRepository<Entry> {

  public List<Entry> findByTags_NameOrderByCreatedDesc(String tagName) {

    return list("SELECT e FROM Entry e JOIN e.tags t WHERE t.name = ?1 ORDER BY e.created DESC", tagName);

  }

  public List<Entry> findByAuthor(BlogUser author, Sort sort, Page page) {

    return find("author", sort, author).page(page).list();

  }

}

Mein Fazit zum ORM: Spring Boot ist hier für mich deutlich überlegen.

Developer-Tools

Spring Boot bietet DevTools für Hot Reload und Actuator für Monitoring. Das erleichtert die Entwicklung und Überwachung von Anwendungen.

Der Dev Mode von Quarkus ist ein echtes Highlight. Änderungen im Code werden (meistens) sofort übernommen, ohne dass die Anwendung neu gestartet werden muss. Die Dev UI bietet eine übersichtliche Darstellung von Beans, Endpunkten und Konfigurationen.

Mein Fazit zu Developer-Tools: Quarkus bietet eine modernere Entwicklererfahrung.

Docker Builds

Spring Boot bietet praktische Möglichkeiten, Docker Images zu erstellen. Mit dem Spring-Boot-Maven-Plug-in können containerisierte Anwendungen direkt gebaut werden. Spring Boot verwendet Buildpacks, um Images ohne die Erstellung eines Dockerfiles zu generieren. Die Imagegröße liegt hier bei etwa 385 MB.

Quarkus hingegen liefert ein Dockerfile direkt mit, was Entwicklern die Konfiguration erleichtert. Darüber hinaus gibt es das Plug-in quarkus.container-image, das den Build-Prozess ebenfalls vereinfacht. Allerdings ist die standardmäßige Imagegröße mit 502 MB etwas größer als bei Spring Boot.

Mein Fazit zu Docker Builds: Spring Boot hat in diesem Bereich einen Vorteil, da es kleinere Images erzeugt und man sich durch Buildpacks die Wartung der Dockerfiles spart.

Stay tuned

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

 

Native Builds

Spring Boot unterstützt die Erstellung nativer Images durch das Plug-in org.graalvm.buildtools:native-maven-plugin. Um ein natives Image zu erstellen, wird der Befehl mvn -Pnative spring-boot:build-image verwendet. Das resultierende Image hat eine Größe von etwa 225 MB.

Quarkus bietet ebenfalls eine einfache Möglichkeit, native Images zu erstellen. Mit dem Befehl ./mvnw package -Dnative wird das native Image erzeugt. Zusätzlich kann mit docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/simple-blog-quarkus-fetch ein Docker Image erstellt werden. Es ist mit etwa 133 MB deutlich schlanker. Quarkus liefert außerdem umfassende Dokumentationen zur Kompatibilität und zu unterstützten Bibliotheken, die auf der GraalVM-Website verfügbar sind.

Mein Fazit zu Native Builds: Quarkus wurde mit Blick auf Native Builds entwickelt. Spring Boot hat in diesem Bereich seit Version 3 deutlich aufgeholt, aber Quarkus bleibt führend. Da Native Builds deutlich länger kompilieren, sollten sie in der täglichen Entwicklung lokal keine Rolle spielen. Man kann sie in Pipelines auslagern. Allerdings muss man bei Native Builds aufpassen: Nicht jede Java-Bibliotheken ist auch kompatibel mit nativen Images [11].

Testing

Spring Boot bietet mit @SpringBootTest eine Annotation, die den gesamten Spring-Kontext für Tests lädt. Damit lassen sich umfassende End-to-End-Tests durchführen, allerdings kann das Laden des gesamten Kontexts zu längeren Testzeiten führen.

Quarkus hat hier einige interessante Ansätze. Mit @QuarkusTest wird der gesamte Quarkus-Kontext geladen, ähnlich wie bei Spring Boot. Zusätzlich gibt es @QuarkusIntegrationTest, das eine Quarkus-Instanz in einer separaten JVM startet, was für Integrationstests nützlich ist. Ein besonderes Highlight von Quarkus ist die automatische Unterstützung von Testcontainern. Ohne zusätzliche Konfiguration werden beispielsweise Datenbankcontainer automatisch gestartet, was den Testaufbau erheblich vereinfacht.

Mein Fazit zum Testing: Quarkus bietet in diesem Bereich durch die automatische Testcontainer-Integration und flexible Testmöglichkeiten mehr Komfort als Spring Boot.

Fazit: Es kommt darauf an

Mein abschließendes Fazit lautet, dass die Frameworks sehr ähnlich sind, sodass sich ohne fachlichen Use Case eine Migration in die eine oder andere Richtung nicht lohnt.

Welche soll man nun wählen? Die beste Technologie ist die, die euer Team beherrscht. Der Erfolg eines Projekts hängt auch hier nicht hauptsächlich von der Wahl des Frameworks ab, sondern von den Menschen, die damit arbeiten. Wenn das Team in Spring Boot fit ist, dann hat es ohne fachliche Anforderungen keinen Sinn, Quarkus einzusetzen. Es sei denn, das Team ist neugierig und möchte Quarkus lernen. Wenn das Team die Jakarta-EE-Standards im Schlaf beherrscht und mit Spring nicht zurechtkommt, gibt es keinen Grund Spring Boot einsetzen. Also: Hört auf eure Teams.

Links & Literatur

[1] https://github.com/gruemme/simple-blog-sb-fetch

[2] https://github.com/gruemme/simple-blog-quarkus-fetch

[3] https://spring.io/projects/spring-boot

[4] https://quarkus.io/guides/

[5] Deandrea, Eric: „Quarkus for Spring Developers“: https://developers.redhat.com/e-books/quarkus-spring-developers

[6] https://microprofile.io

[7] https://www.youtube.com/watch?v=H7O7mIJCLFY

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

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

[10] https://quarkus.io/guides/hibernate-orm#setting-up-and-configuring-hibernate-orm

[11] https://www.graalvm.org/latest/reference-manual/native-image/metadata/Compatibility

The post Spring Boot vs. Quarkus: Ein direkter Vergleich aus der Praxis für Entwickler appeared first on JAX.

]]>
Softwarearchitektur: Muss das sein? https://jax.de/blog/softwarearchitektur-muss-das-sein/ Wed, 05 Mar 2025 13:14:16 +0000 https://jax.de/?p=107227 Oft scheint Softwarearchitektur nur im Weg zu stehen und zu praxisfernen Diskussionen zu führen. Kann man also Software ohne Architektur entwickeln? Vielleicht wird dann ja alles einfacher?

The post Softwarearchitektur: Muss das sein? appeared first on JAX.

]]>
Eigentlich sollte man sich  einfach an den Rechner setzen und Software schreiben können. Nehmen wir als Beispiel einen Importer, der Daten aus einer Datei in eine Datenbank schreiben soll und den man neu schreiben will. Das scheint eine hinreichend einfache Aufgabe zu sein, die keine Architektur erfordert. Man kann einfach loslegen und ist nach einigen Stunden oder Tagen fertig. Eine Architektur scheint überflüssig.

Aber was ist  Softwarearchitektur überhaupt? Man kann über dieses Thema lange und ausführlich diskutieren [1]. Tatsächlich gibt es zahlreiche unterschiedliche Definitionen von Softwarearchitektur. Für diesen Artikel soll die Definition gelten: „Architektur umfasst die wichtigen Dinge, was auch immer das sein mag.“ [2]. Damit ist klar, dass es Architektur geben muss. Schließlich gibt es immer irgendwelche wichtigen Dinge. In gewisser Weise ist diese Definition chauvinistisch, weil sie einfach definiert, dass Architektur wichtig ist und alles andere unwichtig.

Stay tuned

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

 

Qualitäten

Aber was ist wichtig? Bei Software gibt es eine Vielzahl von möglichen, nichtfunktionalen Anforderungen bzw. Qualitäten. Bei einem Importer könnte beispielsweise die Performance wichtig sein, damit die Daten schnell genug in das neue System gelangen. Ein weiterer wichtiger Aspekt könnte die funktionale Korrektheit sein: Arbeitet der Importer tatsächlich richtig und importiert die Daten fachlich korrekt? Oft spielt die Benutzerfreundlichkeit eine Rolle: Wenn eine Anwendung besonders gut nutzbar ist, kann man besonders produktiv mit ihr sein. Produktivität ist oft ein Grund für das Schreiben von Software und Benutzerfreundlichkeit ein wichtiges Qualitätsziel. Software kann gegebenenfalls dank Benutzerfreundlichkeit einen hohen Marktanteil erreichen, weil Benutzer:innen sie gerne verwenden und anderen Lösungen vorziehen. Auch dann ist es ein wichtiges Qualitätsziel, den Marktanteil auszubauen.

Beim Neuschreiben des Importers liegt der Verdacht nahe, dass die alte Software irgendwelche Qualitäten so schlecht erfüllt, dass die Software neu geschrieben werden muss und daher sozusagen ein Totalverlust ist. Dabei können unterschiedliche Qualitäten ausschlaggebend sein: Der Importer kann beispielsweise viel zu langsam sein und mit den aktuellen Datenmengen nicht mehr mitkommen. Dann haben sich die Qualitätsanforderungen geändert und die Software muss diesen Anforderungen genügen. Ein Neuschreiben ist dann sinnvoll, wenn dieses Qualitätsziel mit einer Änderung des vorhandenen Codes nicht mehr möglich ist.

Oft ist der Grund für das Neuschreiben eine spezifische Qualität, nämlich die Wartbarkeit. Wenn der Code sehr schlecht verständlich ist und eine Anpassung kaum noch möglich erscheint, ist ein Neuschreiben vielleicht die einzige Möglichkeit, in Zukunft wieder Änderungen am System vorzunehmen. Auch die Performance kann möglicherweise nur durch Neuschreiben verbessert werden, weil die dafür notwendigen Änderungen sonst zu schwer umsetzbar sind.

Im konkreten Fall ist es also wahrscheinlich, dass der Import deswegen neu geschrieben wird, weil die Qualität des vorhandenen Importers nicht ausreicht. Dann ist aber die Architekturarbeit an der Qualität sehr wichtig: Es wäre mehr als peinlich, wenn man den Importer neu schreibt und danach die Probleme, die Auslöser für das Neuschreiben waren, gar nicht gelöst sind. Und ein Kernpunkt von Architektur sind eben die Antworten auf Fragen wie: Was ist das Ziel des Projekts, welche Qualitäten sind dafür notwendig und welche Maßnahmen sind erforderlich, um das Ziel und die notwendigen Qualitäten zu erreichen?

Diese Betrachtungen müssen auch bei diesem recht einfachen Projekt stattfinden. Vielleicht ist diese Abwägung nicht explizit, sondern implizit: Die neue Version wird mit einer anderen Programmiersprache oder einem anderen Algorithmus umgesetzt, damit die Performance gut genug ist. Oder die Struktur des Codes wird gemanagt und Metriken wie die Länge von Methoden oder Klassen werden erhoben, damit die neue Codebasis wartbar ist. Solche Punkte explizit aufzuschreiben, hilft dabei, die Probleme und die Lösungsstrategien zu durchdenken und Alternativen gegeneinander abzuwägen. So oder so sind die Qualitäten der Treiber für die Architektur (Abb. 1). Ein solches Vorgehen, bei dem die Qualitäten der Software die Basis für die Architektur bilden, kann man sich anhand von zwei Beispielen in Videos anschauen [3], [4].

 

Abb. 1: Qualitäten treiben die Architektur

 

Manchmal sind die Qualitäten nicht nur durch die reine Entwicklungsarbeit erreichbar: Eine gute Performance kann man auch durch bessere Hardware erreichen. Auch Ausfallsicherheit hängt neben der Software von der Hardware und Infrastruktur ab. Benutzerfreundlichkeit kann man typischerweise sogar gar nicht durch technische Maßnahmen, sondern nur durch Techniken aus dem Bereich User Experience (UX) erreichen. Dennoch muss jemand diese Aspekte betrachten, weil das Projekt sonst ein Fehlschlag werden kann.

Qualitätsszenarien

Um in komplexen Systemen solche Qualitätsanforderungen zu konkretisieren, haben sich Qualitätsszenarien bewährt [5]. Sie erlauben es, konkrete Anforderungen so aufzuschreiben, dass man später verifizieren kann, ob sie tatsächlich erfüllt sind. Zu oft sind die notwendigen Qualitäten nicht konkret genug erfasst: Wann ist ein System „schnell“ oder „benutzerfreundlich“? Auch eine Angabe wie „99,999 Prozent Verfügbarkeit“ reicht nicht. Was, wenn das System nur einmal pro Jahr für 50 Minuten ausfällt, aber dann in den entscheidenden 50 Minuten, in denen der höchste Umsatz realisiert wird? Das System erreicht zwar die geforderten 99,999 Prozent Verfügbarkeit aufs Jahr, aber das kann dennoch nicht ausreichend sein. Helfen kann ein Qualitätsszenario wie „Wenn das System zwischen 9 und 18 Uhr ausfällt, muss es nach zehn Minuten wieder zur Verfügung stehen.“ Diese Aussage ist viel konkreter als eine abstrakte Verfügbarkeit und trifft die technischen Anforderungen besser. Nun ist nämlich klar, dass das System zu bestimmten Zeiten nicht lange ausfallen darf, was bei einer allgemeinen Betrachtung der Verfügbarkeit vielleicht nicht offensichtlich geworden wäre und  zu einer unzureichenden Lösung geführt hätte.

Muss das sein?

Mindestens das Thema Qualitäten lässt sich nicht umgehen: Die entwickelte Software wird bestimmte technische Qualitäten haben wie Wartbarkeit, Performance oder Korrektheit. Man hat also nur die Wahl, sich aktiv um diese Qualitäten zu kümmern, sie genau zu verstehen, Prioritäten zu setzen und Lösungsmöglichkeiten zu identifizieren, oder das System wird zufällig irgendwelche Qualitäten haben. Es ist vermutlich deutlich besser, die Qualitäten zu steuern.

Den Prioritäten kommt dabei eine besondere Bedeutung zu: Man muss verstehen, wo welche Qualitäten gefordert sind und dafür eine passende Lösung entwickeln. Erfüllt man die notwendigen Qualitäten nicht, ist das Projekt offensichtlich ein Fehlschlag. Übererfüllt man die Qualitäten, erreicht man ein weiteres wichtiges Ziel nicht, nämlich eine möglichst wirtschaftliche Lösung. Es geht also gerade nicht darum, die in jeder Hinsicht perfekte Lösung zu bauen, sondern vielmehr die Lösung zu bauen, die das Problem löst und dabei auch möglichst kostengünstig ist.

Natürlich steht es jedem frei, diese Betrachtungen nicht anzustellen. Das ist aber kaum empfehlenswert – und meistens wird es zumindest eine oberflächliche Betrachtung geben. Und selbst wenn man diese Betrachtung nicht anstellt: Dann trifft man auch Entscheidungen, die Qualitäten beeinflussen – nur mit einer weniger guten Planung. Software hat immer Eigenschaften wie Performance, Korrektheit oder Wartbarkeit, auch wenn man sie nicht aktiv gestaltet, und diese Eigenschaften sind ein Ergebnis von technischen Entscheidungen.

Also kann man sich nicht aus der Architekturarbeit verabschieden: Das System wird irgendeine Architektur haben. Dafür ist es egal, ob man an der Architektur arbeitet oder nicht.

Stay tuned

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

 

Architektur = Struktur?

Damit ist die Architektur also in erster Linie ein Ansatz, mit dem Qualitäten umgesetzt werden. Performance kann man durch die Auswahl geeigneter Technologien beeinflussen. Bei Korrektheit ist die Lösung eher im Bereich Tests zu suchen.

Viele verstehen unter Architektur aber nur die Struktur der Software, wie also der Source Code organisiert wird. Dazu zählt die Aufteilung in Klassen, Packages, Microservices, Source-Code-Projekte usw. – also die gesamte logische Organisation des Codes. Er wird typischerweise nach fachlichen Aspekten wie Bounded Contexts oder technischen Aspekten wie Schichten organisiert.

Die Struktur der Software hat aber nur begrenzten Einfluss auf die Qualitäten: Lediglich die Qualität „Wartbarkeit“ wird durch die Struktur der Software beeinflusst. Aber auch Wartbarkeit hängt von anderen Faktoren ab. Beispielsweise kann man Software mit vielen Tests besonders einfach warten. Bei dieser Auffassung von Architekturarbeit kommt der Struktur der Software also  keine so große Bedeutung zu, denn sie beeinflusst nur wenige Qualitätskriterien.

Das steht im Widerspruch zu der weitverbreiteten Annahme, dass die Struktur der Software im Wesentlichen die Architektur darstellt. Vielleicht kommt diese Wahrnehmung dadurch zustande, dass die Struktur der Software die Wartbarkeit und damit auch die Wirtschaftlichkeit entscheidend beeinflusst. Sie kann sogar den Aufwand beim initialen Erstellen der Software beeinflussen, nicht erst den Aufwand bei der Wartung. Wirtschaftlichkeit ist typischerweise eines der wichtigsten Ziele in der Entwicklung. Also treiben Wartbarkeit und Wirtschaftlichkeit den Fokus auf die Struktur der Software (Abb. 2).

Abb. 2: Wartbarkeit und Wirtschaftlichkeit bei der Entwicklung sind Gründe, sich auf die Struktur der Software zu fokussieren

 

Ein weiterer Grund für diese Gleichsetzung von Architektur mit der Struktur der Software ist, dass Architektur im Bauwesen eben auch für die Strukturen steht. In der Realität würde ein Fokus auf die Struktur als alleinige Eigenschaft der Architektur dazu führen, dass die Qualitäten jenseits der Wartbarkeit vernachlässigt werden. Das kann recht leicht zu einem Architekturfehlschlag führen.

Architektur skalieren

Bisher haben wir einen sehr einfachen Fall betrachtet: Den Importer schreibt nur eine Person und die Entwicklung dauert nicht sonderlich lange. Typischerweise arbeitet aber ein Team an einer Software. Damit ist eine Kommunikation der Architektur und eine gemeinsame Arbeit an der Architektur unumgänglich. Dabei geht es um die Koordination der Techniker:innen, deren Ideen in die Architektur einfließen sollen, und um die verschiedenen Stakeholder, die Erwartungen an die Software haben und damit eine Quelle von Qualitätsanforderungen sind.

Dementsprechend muss die Architektur dokumentiert und kommuniziert werden. Wenn das Projekt groß und komplex genug ist, kann das eine Vollzeitaufgabe sein. Dann gibt es die Möglichkeit, dass sich eine Person Vollzeit um das Thema Architektur kümmert und nicht an der Codeentwicklung selbst teilnimmt. Eine andere Möglichkeit ist, dass die Architekt:innen als „Coding Architects“ an der Entwicklung teilnehmen. Dann müssen sich aber mehrere Architekt:innen die Architekturarbeit aufteilen, wenn die reine Architekturarbeit tatsächlich ein Vollzeitjob ist und sie noch Zeit für Coding haben wollen. Die Architekt:innen müssen untereinander kommunizieren und sich koordinieren, was zu mehr Kommunikation und Koordination führt.

Coding Architects haben oft einen besseren Ruf, weil sie durch die Arbeit am Code wissen, was im Projekt geschieht und daher nicht von der Realität entkoppelt im Architekturelfenbeinturm enden können. Allerdings sollten auch Vollzeitarchitekt:innen, wenn sie kommunikationsstark sind, genügend Informationen über die Situation im Projekt haben. Sie sollten dann eine moderierende Rolle einnehmen und die anderen am Projekt Beteiligten als Expert:innen für die jeweiligen Bereiche wahrnehmen. Eine Person, die sich beispielsweise mit dem Frontend eines Systems beschäftigt, ist vermutlich qualifizierter, Entscheidungen über diesen Bereich zu treffen, als eine Person, die sich allgemein um die Architektur kümmert und in diesem Bereich keine Tiefe hat. Die Meinung der Frontend-Expert:in muss mindestens in die Entscheidung einfließen und wird sie vermutlich entscheidend beeinflussen. Dementsprechend müssen Vollzeitarchitekt:innen meistens Abstand davon nehmen, eine Entscheidungen alleine zu treffen. Es geht in dieser Rolle um Kommunikation und Koordination. Deswegen ist es nicht so einfach, die Rolle „Softwarearchitekt:in“ zu verstehen und gut auszufüllen [6], [7].

Manche Architekturentscheidungen werden auch gar nicht explizit getroffen. Entwickler:innen schreiben Code und treffen dabei immer wieder Entscheidungen, die die Struktur des Systems beeinflussen. Schließlich wird erst dann entschieden, wie der Code in Packages oder Microservices strukturiert ist, wenn er geschrieben wird. Ebenso wählen Entwickler:innen gegebenenfalls bestimmte Technologien aus und nehmen so Einfluss auf die technischen Eigenschaften des Systems, die wiederum die Qualitäten beeinflussen. Also treffen auch sie Architekturentscheidungen. All diese Entscheidungen zu kontrollieren, wäre zu zeitaufwendig, sodass auch Entwickler:innen manchmal die Rolle „Softwarearchitekt:in“ spielen. Damit diese Entscheidungen nicht konträr zu anderen Entscheidungen stehen, ist wieder Kommunikation und Koordination notwendig.

Zu viel oder zu wenig?

Man kann noch die Frage stellen, ob es zu wenig oder zu viel Architektur geben kann. Wenn man zu wenig Aufwand in die Architektur steckt, besteht die Gefahr, dass man die technischen Ziele nicht erreicht, weil man sie entweder nicht versteht oder sich keine Lösungen für die Erreichung überlegt hat. Ebenso kann es gut sein, dass die Struktur und der Rest der Architektur nicht ausreichend kommuniziert wird. Das kann dann dazu führen, dass die Architekturkonzepte nicht durchgehalten werden und das Softwaresystem im Chaos endet, also technische Konzepte nicht umgesetzt werden oder die Strukturierung des Systems von dem eigentlich geplanten Design signifikant abweicht.

Zu viel Architektur erscheint zunächst kaum möglich, aber man kann zu viel Aufwand für Architektur treiben, ohne dabei ausreichende Ergebnisse zu erzielen. Beispiel ist eine übermäßige Bürokratie für Entscheidungen. Ebenso können frühzeitige Entscheidungen eingefordert werden, statt Entscheidungen so spät wie möglich zu treffen. Das führt zu theoretischen Diskussionen, weil über Probleme in der ferneren Zukunft gesprochen wird. Zu einem späteren Zeitpunkt stehen mehr und bessere Informationen zur Verfügung, weil man mit der Zeit mehr über die jeweiligen Probleme lernt und sie dann besser lösen kann.

In der Praxis kommt beides vor, zu wenig und zu viel Architektur, sodass man hier keine generellen Hinweise geben kann, wie man typischerweise vorgehen soll (Abb. 3).

Abb. 3: Es kann zu wenig oder zu viel Softwarearchitektur geben – oder gar ein Architekturtheater

Architekturtheater?

Kevlin Henney hat den Begriff Architekturtheater geprägt [8]: Ein schwergewichtiger, komplizierter Prozess mit viel Bürokratie und Hierarchie, bei dem aber am Ende keine sinnvollen Entscheidungen getroffen werden. Hier ist  ein Zuviel an Prozess mit einem Zuwenig an Ergebnis kombiniert – es ist  gleichzeitig zu viel und zu wenig Architektur. „Architekturtheater“ beschreibt gut, was vor sich geht: Es wird vorgespielt, dass man sich intensiv um Architektur kümmert, aber weil der Prozess so schwerfällig ist und auch im Sinne des Elfenbeinturms keine echte Beziehung zur Realität hat, findet in Wirklichkeit keine effektive Architekturarbeit statt.

Fazit

Es gibt also in einem Softwaresystem immer eine Architektur. Sie umfasst neben der Struktur der Software auch die technischen Lösungen für die spezifischen Herausforderungen. Man kann die Architektur nur aktiv gestalten oder die Gestaltung dem Zufall überlassen. Dementsprechend gibt es irgendwelche Personen, die an der Architektur arbeiten. Die können sich entweder Vollzeit mit der Architektur beschäftigen oder es können mehr Menschen sein, die dann nur einen Teil ihrer Zeit mit Softwarearchitektur verbringen.Mit dem Thema des Artikels hat sich auch eine Episode von „Software Architektur im Stream“ beschäftigt [9].

Links & Literatur

[1] Software Architektur im Stream: „Was ist Softwarearchitektur überhaupt?“: https://software-architektur.tv/2022/02/11/folge109.html

[2] https://martinfowler.com/architecture

[3] Software Architektur im Stream, Folgen zu „Wir bauen eine Software-Architektur“: https://software-architektur.tv/tags.html#Wir%20bauen%20eine%20Software-Architektur

[4] Software Architektur im Stream, Folgen zur iSAQB-Beispiel-Aufgabe: https://software-architektur.tv/tags.html#iSAQB%20Advanced%20Beispielaufgabe

[5] Software Architektur im Stream: „Qualitätsszenarien“: https://software-architektur.tv/2021/07/16/folge67.html

[6] „Die Rolle ‚Software-Architekt:in‘ – Folge 1“: https://software-architektur.tv/2022/07/07/folge126.html

[7] „Die Rolle ‚Software-Architekt:in‘ – Folge 2“: https://software-architektur.tv/2022/07/15/folge127.html

[8] https://mastodon.social/@kevlin/112003129757159797

[9] „Softwarearchitektur – Muss das sein?“: https://software-architektur.tv/2024/03/08/folge206.html

The post Softwarearchitektur: Muss das sein? appeared first on JAX.

]]>
Moderne Softwarearchitektur verstehen https://jax.de/blog/moderne-softwarearchitektur-verstehen/ Mon, 10 Feb 2025 15:48:42 +0000 https://jax.de/?p=107098 Softwarearchitektur beschreibt die Struktur eines Softwaresystems und die Entscheidungen, die zu dieser Struktur führen. Entwicklungsteams großer oder komplexer Systeme sollten sich intensiv mit Softwarearchitektur beschäftigen, um stabile und effektive Lösungen zu schaffen. Doch was genau bedeutet der Begriff „Softwarearchitektur“ und wer trägt die Verantwortung dafür?

The post Moderne Softwarearchitektur verstehen appeared first on JAX.

]]>
Was gehört zu Softwarearchitektur?

Sicher kennen Sie die typischen Grundrisspläne von Gebäuden oder ihrer eigenen Wohnung. Da finden Sie Mauern, Türen, Fenster schematisch dargestellt (Abb. 1). Niemand in der Baubranche käme auf die Idee, ein Gebäude ohne derartige Pläne zu beginnen.

Abb. 1: Typischer Grundriss eines Gebäudes [1]

In der Informatik zeigen solche Bilder dann Komponenten („Kästchen“) und deren gegenseitige Abhängigkeiten („Pfeile“). Damit können wir die statische Struktur von IT-Systemen zeigen, also den Aufbau des Quellcodes im Großen. Solche strukturellen Pläne („Grundrisse“) stellen allerdings nur einen Teil der Architektur dar – denn darin fehlen noch die „Baumaterialien“.

Stay tuned

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

 

Für Software gehört dazu die Auswahl von Programmiersprachen, Frameworks, Middleware sowie der geeigneten technischen Infrastruktur für Test und Betrieb von Systemen. Das nennen wir querschnittliche Konzepte und Technologieentscheidungen. Für unser Gebäude aus Abbildung 1 müssten wir entsprechend festlegen, ob wir mit Holz, Beton oder Ziegelstein bauen, wie wir
Wasser-, Strom- und Netzwerkleitungen verlegen und so weiter.

Fassen wir zusammen: Architektur (ob Gebäude oder IT-Systeme) definiert einerseits die Struktur („Kästchen und Pfeile“) von Systemen, andererseits grundlegende, querschnittliche Themen wie Implementierungs- und Infrastrukturtechnologien. Unter [2] führe ich die Analogie zwischen Gebäude- und Softwarearchitektur etwas weiter aus. Ein kleines Beispiel finden Sie im Kasten „Strukturen und Konzepte“. Für den Überblick an dieser Stelle soll uns das genügen.

Jetzt stellt sich die Frage, wie wir zu diesen Entscheidungen kommen beziehungsweise welche Aufgaben noch rund um diese strukturellen und querschnittlichen Entscheidungen zu erledigen sind.

Strukturen und Konzepte

Am Beispiel eines fiktiven Onlineshops möchte ich die Bedeutung und Unterschiede von Strukturen („Bausteine des Systems“) und Konzepten („Lösungsansätze, Technologien“) erläutern. Wir strukturieren unseren fiktiven (und unvollständigen) Onlineshop etwa nach Domain-Driven Design oder anderen Strukturansätzen und erhalten dabei die Bausteine, Module (oder in DDD-Sprechweise, Bounded Contexts) aus Abbildung 2.

Abb. 2: Bausteine des Onlineshops

Diese Struktur enthält allerdings noch keinerlei Informationen über die gewählte Implementierungs- oder Deploymenttechnologie oder die verwendeten Frameworks. Genau das wären unsere Konzepte – ein paar Beispiele (ebenfalls fiktiv) in der folgenden Aufzählung:

  • Java/Kotlin mit Spring Boot als Backend-Technologie
  • Verwendung von Angular für grafische Frontends
  • Einige Bausteine werden als Self-Contained Systems (aka Microservices) eigenständig deployt und betrieben
  • Apache Kafka als Messaging-System zur (zeitlichen) Entkopplung der Self-Contained Systems
  • Verwendung von Jasper-Reports für sämtliche Reporting-Aufgaben
  • Verwendung von PostgreSQL als Datenspeicher
  • Caching lokal benötigter Daten über SQLite

    Sie sehen, diese Lösungskonzepte beziehen sich teilweise (z. B. Jasper-Reports, PostgreSQL, Spring Boot) auf mehrere der Bausteine, daher rührt die Bezeichnung „querschnittliche Konzepte“. Solche Konzepte können Sie in der Architektur oftmals (fast) unabhängig von der Struktur Ihrer Bausteine oder Komponenten festlegen.

    Wie geht Softwarearchitektur?

    Bevor wir in der Architektur mit diesen Entscheidungen loslegen, sollten wir die grundlegenden Anforderungen an das System verstanden haben: Was soll das System leisten, welche Aufgaben oder Prozesse soll es unterstützen (aka funktionale Anforderungen)? Dazu kommt das schwierige Thema der Qualitätsanforderungen, wie Performanz, Durchsatz, Sicherheit, Änderbarkeit und so weiter (mehr dazu in [3]). Schließlich müssen Sie auch die Rand- oder Rahmenbedingungen kennen (constraints), die die Entscheidungsmöglichkeiten von Architekt:innen einschränken.

    DIE KUNST DER SOTWARE-ARCHITEKTUR

    Architecture & Design-Track entdecken

     

    Es geht hier nicht um alle Anforderungen – denn dann wären wir ja bei einem Waterfallish-upfront-Ansatz – sondern um die aktuell bekannten und relevanten Themen einer Iteration. Primär sollten Sie sich um architekturrelevante Anforderungen kümmern. Etwa solche, die von besonders wichtigen Stakeholdern stammen (z. B. oberes Management, Auftraggeber etc.), besonders kritisch bzw. riskant sind, besonders kritische Qualitätseigenschaften betreffen oder einen ausgeprägt innovativen Charakter besitzen.

    Sollten diese Anforderungen zu schwammig, unklar und widersprüchlich sein oder gar komplett fehlen, müssen Sie in der Architektur handeln statt zu jammern, also gemeinsam mit Stakeholdern nachbessern oder zumindest über die für Requirements verantwortlichen Personen nachfordern. Daher sehen Sie in Abbildung 3 auch verschiedene solcher Stakeholder symbolisch mit der Aufgabe „Anforderungen klären“ verbunden.

    Abb. 3: Aufgaben in der Softwarearchitektur

    Den Kern der Architekturaufgaben bildet das Duo „entwerfen“ – das sehen Sie in Abbildung 4 nochmals hervorgehoben:

    • Durch „Strukturen entwerfen“ legt die Architektur die Zerlegung (auch: Schnitt) Ihres Systems fest. Sie bestimmt dabei die Bestandteile (Komponenten, Module, Services, Pakete oder wie auch immer in Ihrer gewählten Technologie die einzelnen Bestandteile eines Gesamtsystems heißen). Ganz wesentlich hierbei sind die Schnittstellen zwischen den einzelnen Bestandteilen sowie zur Umwelt.
    • Durch „Konzepte entwerfen“ legt die Architektur beispielsweise die genutzten Technologien und Frameworks fest. Sie bestimmt die Art und Weise, wie die Technologien eingesetzt werden, und gibt Patterns (Muster) und Regeln für architekturrelevante Themen vor.

    Stay tuned

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

     

    Sie finden in Abbildung 4 einen überschneidenden Bereich, den ich anhand einiger Beispiele erklären möchte: Manche Entscheidungen betreffen sowohl Strukturen als auch querschnittliche Konzepte, beispielsweise:

    • Die (querschnittliche) Entscheidung, sämtliche externen REST-Schnittstellen durch einen Penetration-Test auf Sicherheitsrisiken zu prüfen. Externe Schnittstellen gehören zu den Bausteinen, den Strukturelementen des Systems, Penetration-Tests stellen ein methodisches (querschnittliches) Konzept dar.
    • Die (querschnittliche) Entscheidung, gemäß Domain-Driven Design zu arbeiten und dabei jeden fachlichen Baustein (bounded context) gemäß dem Clean-Architecture-Muster zu implementieren.
    • Die (querschnittliche) Entscheidung, Apache Kafka als Produkt für Messaging zu verwenden, hat Konsequenzen für alle betroffenen Bausteine (Strukturelemente).

    Abb. 4: Entwurfsaufgaben: Strukturen und Konzepte

    Wir arbeiten in der Architektur mit den sogenannten Stakeholdern zusammen. Neben dem Entwicklungsteam gehören dazu Fachbereiche, Auftraggebende, eventuell Behörden und Normungsgremien, Test- und QS-Abteilungen, Management, Product Owner, Nachbarprojekte und so weiter. Deswegen zählt kommunizieren zu den wesentlichen Aufgaben – mündlich wie auch schriftlich (dann nennen wir es dokumentieren). Im gleichnamigen Textkasten erkläre ich, was arc42 mit dieser Aufgabe zu tun hat.

    arc42

    Sie hören in Ihrer Firma immer wieder von arc42 [4], können es aber nicht einordnen? Hier eine kurze Fassung: arc42 ist ein Open-Source-Rahmenwerk zur Kommunikation (sprich: Erklärung und/oder Dokumentation) von Software- und IT-Architekturen. Sie können damit alle für Architektur und Entwicklung relevanten Aspekte Ihres Systems in einer einheitlichen Form beschreiben. Vergleichen Sie arc42 mit einem Schrank, bei dem jedes Fach bestimmte Dinge („Informationen“) über die Architektur enthält. Die Tabelle gibt eine kurze Übersicht der wesentlichen Elemente von arc42, und wie sie mit den anderen Themen dieses Artikels zusammenhängen. In [6] finden Sie viele Beispiele für die einzelnen Sektionen aus konkreten Systemen.

    Sektion Name Bedeutung
    1 Aufgabenstellung Eine Kurzfassung der wesentlichen Aufgaben („funktionale Anforderungen“) des Systems, der wichtigsten drei bis fünf Qualitätsanforderungen sowie eine Übersicht der beteiligten Stakeholder
    2 Randbedingungen Welche technischen oder organisatorischen Einschränkungen gibt es?
    3 Kontextabgrenzung Übersicht der externen Schnittstellen. Einbettung des Systems in dessen (fachliches + technisches) Umfeld
    4 Lösungsstrategie Wesentliche Elemente oder Entscheidungen der Lösung, etwa: zentrale Technologien
    5 Bausteinsicht Statische Struktur des Systems, Subsysteme, Komponenten, Module oder (Micro-)Services. Zeigt den Aufbau des Quellcodes aus einer Vogelperspektive. Wichtig: zeigt auch (interne) Schnittstellen
    6 Laufzeitsicht Wie bearbeiten die Bausteine (siehe Teil 5) wesentliche Abläufe im System?
    7 Verteilungssicht Technische Infrastruktur (Hardware, Netzwerke) und wie die Software darauf verteilt ist (Deployment)
    8 Querschnittliche Konzepte Welche Technologien werden wie eingesetzt? Beispiele: Wie speichert das System Daten, wie findet User-Interaktion statt, wie wird das System getestet, welche wesentlichen Patterns finden Anwendung?
    9 Architekturentscheidungen Alles, was an Entscheidungen sonst nirgendwo Platz findet. Viele Teams bringen hier ihre ADRs unter
    10 Qualitätsanforderungen Die Qualitätsanforderungen, die es nicht in die „Hitparade“ in Teil 1 geschafft haben
    11 Risiken und offene Punkte Technische Schulden, bekannte Probleme oder Risiken
    12 Glossar Erklärt die wichtigsten Fachbegriffe, die speziell und wichtig für dieses System sind. Bitte nicht REST oder HTTP erklären, das steht schon bei Wikipedia

    Jetzt bleibt noch die Aufgabe „Umsetzung begleiten“: Es besteht beim Arbeiten im Team immer das Risiko, dass Menschen sich missverstehen. Das ist ein menschliches Grundproblem, daher können Sie das nicht grundlegend ändern. Wenn Sie dem Team etwas erklären, könnten manche Personen diese Worte und Bilder anders interpretieren, als Sie das gemeint haben.

    Solche Missverständnisse haben wir alle in der Realität schon erlebt. Sie sollten in Ihrer Architekturarbeit aktiv etwas gegen diese Missverständnisse unternehmen: Begleiten Sie die Umsetzung! Prüfen Sie beispielsweise, ob der implementierte Code so beschaffen ist, wie Sie das in der Architektur vorgesehen haben. Code-Reviews, Pull/Merge Requests oder statische Codeanalyse sind nur einige der methodischen Mittel, die Sie hierfür einsetzen können. Design-Reviews, Pair oder Mob Programming, Coding Styleguides, Referenzimplementierungen, Checklisten und noch viele andere.

     

    Auf eine solche konstruktive Weise die Umsetzung zu begleiten, hat allerdings noch einen weiteren positiven Effekt: Ihre Teamkolleg:innen werden an manchen Stellen schlichtweg auf bessere Ideen kommen als die ursprünglichen Architekturentscheidungen. Solche strukturellen oder technischen Verbesserungen, Vereinfachungen, geschickteren Ansätze oder Ähnliches bezeichne ich als „Goldstücke, und die sollten Einzug in die Architektur halten. Insbesondere weil Sie in Ihrer Rolle als Architekt:in eben nicht alles wissen (können). Dazu kommen wir gleich, wenn wir klären, welche Person(en) überhaupt diese Architekturaufgaben erledigen können.

    So viel zu den sechs Kernaufgaben der Softwarearchitektur.

    Wer macht Softwarearchitektur?

    Welche Optionen gibt es denn? Einerseits könnten wir monarchisch diktieren, also die Entscheidungsgewalt (im wahrsten Sinne des Wortes) auf eine einzelne Person maximal zentralisieren. Andererseits könnten wir die Architekturaufgaben einfach an das gesamte Entwicklungsteam delegieren – und komplett dezentralisieren. Dazwischen gibt es eine Vielzahl möglicher Varianten, von denen Sie in Abbildung 5 einige Vertreter finden (nach [5] und [6])

    Zur Vereinfachung beziehen sich die hier skizzierten Situationen auf Teams überschaubarer Größe, circa acht bis zwölf Personen. Für größere Teams oder Gruppen aus mehreren Teams müssen zusätzliche oder andere Regeln gelten, auf die wir in dieser Übersicht nicht eingehen.

    Abb. 5: Rolle und Personen: zentrale bis dezentrale Architekturarbeit

    In [7] erkläre ich Vor- und Nachteile dieser fünf Modelle, daher hier nur in Kurzform: Alle diese Arbeitsweisen haben sinnvolle Anwendungsbereiche. In Off- oder Nearshore-Situationen kann eine zentralisiert-monarchische Organisation sinnvoll sein, auch wenn sie für viele Entwicklungsteams eher nach Anti-Pattern aussieht. Eine einzelne Person trifft sicher konsistente Entscheidungen, ihr mangelt es aber möglicherweise an „Schwarmintelligenz“ und ehrlichem Feedback. Andererseits kann eine rein demokratische (dezentrale) Teamarchitektur zu beliebig viel Chaos führen, obwohl sie auf den ersten Blick für viele Teams attraktiv erscheint.

    Wie so oft in der IT gilt hier die „Kommt drauf an“-Regel: Jedes Team muss situativ die passende Arbeitsweise finden und für sich selbst die Frage beantworten: „Wie sollten wir Architekturentscheidungen treffen?“. Meine Vermutung (aus einigen Jahren Erfahrung): Das Modell der Agenten (also zwei bis drei Personen teilen sich die Architekturaufgaben) skaliert gut, liefert inhaltlich oftmals hervorragende Ergebnisse und trifft diese Entscheidungen recht schnell, d. h. eignet sich auch für zeitkritische Projekte.

    Weder SOLID noch Clean Code sind Architektur

    Die Einhaltung von Programmierregeln (wie Clean Code oder die SOLID-Prinzipien) allein machen keine solide Architekturarbeit aus. Sie können mit Clean Code unglaublich inperformanten Code schreiben oder gravierende Sicherheitslücken produzieren. Verständlich geschriebener Code gehört zu den wünschenswerten Eigenschaften von IT-Systemen, aber wenn höchste Performance gefragt ist, stehen Aspekte der Lesbarkeit und Verständlichkeit hinten an! Diese Regeln lassen die Gesamtstruktur von IT-Systemen komplett außer Acht. Sie sagen nichts über Deployment, technische Infrastruktur oder die systematische Anwendung übergreifender Konzepte. Insofern besitzen sie für Architektur wenig (!) Bedeutung, und ihre Anwendung stellt keineswegs gute oder solide Architekturarbeit sicher!

    Nehmen Sie sich etwas Zeit und genießen Sie Golo Rodens ausführliche Ausführungen zu diesen Themen [8].

    Fazit

    Jedes System besitzt interne Strukturen („Bausteine und deren Abhängigkeiten“) und verwendet bestimmte Technologien auf eine jeweils bestimmte Art und Weise („Querschnittliche Konzepte“). Die Entscheidungen über diese beiden Themen (Strukturen und Konzepte) können Teams gezielt treffen (sprich: die Architektur aktiv gestalten) oder dem Zufall überlassen (was langfristig viele Probleme verursachen wird).

    Mit aktiver Gestaltung steigt die Wahrscheinlichkeit, die notwendigen Anforderungen und insbesondere Qualitäten zu erreichen. Insofern haben Sie keine wirkliche Wahl – Architekturarbeit muss sein!

    Ob Sie diese Gestaltungsarbeit einer einzelnen Person überlassen, sie auf mehrere Schultern verteilen oder im Team abstimmen, sollten Sie situativ entscheiden. In jedem Fall wünsche ich Ihnen für Ihre Architekturarbeit viel Erfolg.

    Stay tuned

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

     


    Links & Literatur

    [1] Amsterdam City Archive, Grundriss, Foto von unsplash: https://unsplash.com/de/fotos/wD9uk9fNcQU

    [2]: Starke, Gernot: „Grundlagen der Softwarearchitektur – Teil 1“. INNOQ-Blog: https://www.innoq.com/de/articles/2023/07/architektur-teil-1/

    [3] Q42 – das arc42 Qualitätsmodell: https://quality.arc42.org. Definiert Qualitätseigenschaften und zeigt viele konkrete Beispiele, wie entsprechende Anforderungen formuliert werden können („Qualitätsszenarien“).

    [4] arc42: https://arc42.org. Dokumentation mit vielen Beispielen unter https://docs.arc42.org. Kompaktfassung als „one pager“ unter https://canvas.arc42.org

    [5] Toth, Stefan: „Vorgehensmuster für Softwarearchitektur“, Carl Hanser Verlag, 2019

    [6] Hohpe, Gregor: „Organizing Architecture“: https://architectelevator.com/architecture/organizing-architecture/

    [7] Starke, Gernot: „Grundlagen der Softwarearchitektur, Teil 4: Wer macht das?“: https://www.innoq.com/de/articles/2023/10/grundlagen-der-softwarearchitektur-teil-4

    [8] Roden, Golo: „Architektur ist überbewertet“: https://www.youtube.com/watch?v=C7TMa_kYANA und als Artikel unter https://www.heise.de/blog/Architektur-ist-ueberbewertet-und-was-wir-daraus-lernen-koennen-10191624.html

    The post Moderne Softwarearchitektur verstehen appeared first on JAX.

    ]]>
    Architektur ist nicht Kubernetes: Diana Montalions Vision für Systemarchitektur auf der W-JAX https://jax.de/blog/moderne-systemarchitektur-systems-thinking-w-jax/ Tue, 19 Nov 2024 15:04:34 +0000 https://jax.de/?p=106834 In ihrer mitreißenden Keynote auf der W-JAX in München stellt Diana Montalion eine neue Sichtweise auf moderne Systemarchitektur vor. Sie zeigt, dass echte Innovation weit über Tools wie Kubernetes hinausgeht. Stattdessen rückt sie Systems Thinking und die kunstvolle, flexible Gestaltung von Beziehungen zwischen Systemkomponenten in den Fokus. Mit ihrer Vision macht sie deutlich, dass moderne Software-Architektur mehr ist als Technik – sie ist eine soziotechnische Disziplin, die technisches Know-how und menschliche Zusammenarbeit nahtlos miteinander vereint.

    The post Architektur ist nicht Kubernetes: Diana Montalions Vision für Systemarchitektur auf der W-JAX appeared first on JAX.

    ]]>

    Architektur neu denken!

    Eines der kontroversesten Wörter in der heutigen Technologiekultur ist „Architekt“. Was bedeutet Architektur wirklich? Diese Frage sorgt immer wieder für hitzige Diskussionen und Missverständnisse. In ihrer Keynote auf der W-JAX in München wirft die renommierte Expertin Diana Montalion einen frischen Blick auf das Thema. Sie verdeutlicht, dass Architektur weit mehr ist als die Implementierung von Tools wie Kubernetes. Stattdessen geht es um das Entwerfen von Beziehungen zwischen Systemkomponenten und die Fähigkeit, diese Muster flexibel an wechselnde Bedingungen anzupassen. Dies erfordert fundierte mentale Modelle und kollaboratives Arbeiten, um diese weiterzuentwickeln.

    Diana Montalion, Autorin des O’Reilly-Buchs Learning Systems Thinking: Essential Nonlinear Skills & Practices for Software Professionals, blickt auf über 18 Jahre Erfahrung in der Software-Entwicklung und -Architektur zurück. Sie hat unter anderem für Organisationen wie Stanford, die Gates Foundation und The Economist gearbeitet und war Principal Systems Architect für die Wikimedia Foundation.

    In ihrer Keynote betont Diana, dass moderne Architektur eine soziotechnische Disziplin ist – eine Mischung aus sozialen und technischen Fähigkeiten. Sie beschreibt, wie gutes Architektendenken effektives Systemdenken strukturiert und erklärt die fünf wesentlichen Qualitäten, die einen guten Architekten von einem großartigen unterscheiden.

    Stay tuned

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

     

    Die wichtigsten Take-aways

    • Architekturdenken: Systeme sollten nicht isoliert betrachtet werden. Es ist entscheidend, das gesamte Ökosystem zu berücksichtigen und zu verstehen, wie verschiedene Komponenten zusammenarbeiten, um echte Effizienz und Effektivität zu erreichen.
    • Silos aufbrechen: Abteilungsübergreifende Zusammenarbeit ist notwendig, um die Integration verschiedener Technologien und Teams zu verbessern. Teams müssen effektiv kommunizieren und kooperieren können.
    • Kultureller Wandel: Ein grundlegender Kulturwandel in Unternehmen ist erforderlich, um moderne Architekturen erfolgreich zu implementieren. Vertrauen und Autonomie der Teams sowie die Bereitschaft, neue Wege zu gehen, sind hierbei essenziell.
    • Einsatz geeigneter Werkzeuge: Technologien wie Kubernetes können hilfreich sein, dürfen jedoch nicht das zentrale Element des Architekturdenkens werden. Sinnvolle und ganzheitliche Designansätze, die weit über spezifische Tools hinausgehen, sind entscheidend.

    The post Architektur ist nicht Kubernetes: Diana Montalions Vision für Systemarchitektur auf der W-JAX appeared first on JAX.

    ]]>
    JavaFX-Animationen auf dem Prüfstand: Node oder Canvas? https://jax.de/blog/javafx-rendering-effizienz-node-canvas-vergleich/ Mon, 07 Oct 2024 08:22:14 +0000 https://jax.de/?p=90204 JavaFX bietet vielseitige Möglichkeiten für Benutzeroberflächen, doch die Wahl zwischen Nodes und Canvas ist entscheidend für die Performance. Dieser Artikel vergleicht beide Methoden und zeigt, wie sich Animationen auf Geräten wie dem Raspberry Pi auswirken. Mit einer Demoanwendung wird demonstriert, wie Canvas die Effizienz steigern kann, um die richtige Komponente für leistungsstarke JavaFX-Anwendungen zu wählen.

    The post JavaFX-Animationen auf dem Prüfstand: Node oder Canvas? appeared first on JAX.

    ]]>
    Kürzlich habe ich mit der Installation eines JDK experimentiert, das JavaFX enthält. Das vereinfacht die Ausführung von JavaFX-Anwendungen, da man die JavaFX-Laufzeitumgebung nicht separat herunterladen muss, beispielsweise von der Gluon-Website [1]. Ich habe diese Experimente auf einem Raspberry Pi durchgeführt und eine kleine Testanwendung verwendet, die eine Menge sich bewegender Punkte auf den Bildschirm bringt. Dabei habe ich bemerkt, dass die Leistung bei vielen dieser Kreisobjekte langsamer wird.

    Ich habe schon mehrmals gelesen, dass ein Canvas für diese Art von Anwendungsfall viel effizienter sein kann, und das hat mich dazu veranlasst, eine Testanwendung mit „Bouncing Balls“ (Abb. 1) zu erstellen, die es einfach macht, Nodes und Canvas zu vergleichen.

    Abb. 1: Meine Testanwendung

    Node versus Canvas

    In JavaFX sind sowohl Nodes als auch Canvas Teil des Scene Graphs, aber sie haben unterschiedliche Use Cases. Die Wahl zwischen den beiden hängt oft von den spezifischen Anforderungen Ihrer Anwendung ab. Sie verwenden Nodes für statische Inhalte wie Eingabeformulare, Datentabellen, Dashboards mit Diagrammen … Das ist in der Regel bequemer und effizienter. Das Canvas bietet Ihnen mehr Flexibilität, wenn Sie dynamische oder benutzerdefinierte Inhalte erstellen müssen.

    JavaFX Node

    javafx.scene.Node ist die Basisklasse und alle visuellen JavaFX-Komponenten erweitern sie. Das geht mehrere „Schichten“ tief. Zum Beispiel Button > ButtonBase > Labeled > Control > Region > Parent > Node.

    Zusammengefasst:

    • Ein Node in JavaFX repräsentiert ein Element des Scene Graph.
    • Dazu gehören UI-Steuerelemente wie Buttons, Labels, Text Fields, Shapes, Images, Media, Embedded Web Browser usw.
    • Jeder Node kann im 3D-Raum positioniert und transformiert werden, er kann Events handlen und es können Effekte auf ihn angewendet werden.
    • Node ist eine Basisklasse für alle visuellen Elemente.
    • Die Verwendung von Nodes wird als „Retained Mode Rendering“ bezeichnet.

    SIE LIEBEN JAVA?

    Den Core-Java-Track entdecken

     

    Das sind einige typische Komponenten, die von Node abgeleitet sind:

    Label label = new Label("Hello World!");
    Button button = new Button("Click Me!");

    JavaFX Canvas

    javafx.scene.canvas erweitert ebenfalls Node, fügt aber spezielle Funktionen hinzu. Sie können Ihren eigenen Inhalt auf dem Canvas zeichnen, indem Sie eine Reihe von Grafikbefehlen verwenden, die von einem GraphicsContext bereitgestellt werden.

    Zusammengefasst:

    • Sie zeichnen auf einem Canvas mit einem GraphicsContext.
    • Das direkte Zeichnen auf einem Canvas wird als „Immediate Mode Rendering“ bezeichnet.
    • Das gibt Ihnen mehr Flexibilität, ist aber weniger effizient, wenn sich der Inhalt nicht oft ändert.

    In diesem Beispiel wird ein Rechteck gezeichnet:

    Canvas canvas = new Canvas(400, 300);
    GraphicsContext gc = canvas.getGraphicsContext2D();
    gc.setFill(Color.BLUE);
    gc.fillRect(50, 50, 100, 70);

    Demoanwendung

    Die Demoanwendung kann im GitHub Gist unter [2] gefunden werden. Sie enthält Code, um eine Menge sich bewegender Kreise zu erzeugen – sowohl als Nodes als auch gezeichnet auf einem Canvas. Der Wert am Anfang des Codes definiert, welcher Ansatz verwendet wird:

    private static int TYPE_OF_TEST = 1; // 1 = Nodes, 2 = Canvas

    Nodes verwenden

    Wenn Sie Nodes verwenden, wird dem Bildschirm ein Bereich hinzugefügt, in dem Bälle eingefügt werden. Bei jedem Ball handelt es sich um einen Circle Node mit einer Bewegungsmethode (Listing 1).

    class BallNode extends Circle {
      private final Color randomColor = Color.color(Math.random(), 
        Math.random(), Math.random());
      private final int size = r.nextInt(1, 10);
      private double dx = r.nextInt(1, 5);
      private double dy = r.nextInt(1, 5);
    
      public BallNode() {
        this.setRadius(size / 2);
        this.setFill(randomColor);
        relocate(r.nextInt(380), r.nextInt(620));
      }
    
      public void move() {
         if (hitRightOrLeftEdge()) {
          dx *= -1; // Ball hit right or left 
        }
        if (hitTopOrBottom()) {
          dy *= -1; // Ball hit top or bottom
        }
        setLayoutX(getLayoutX() + dx);
        setLayoutY(getLayoutY() + dy);
      }
    
      ...
    }
    

    Canvas verwenden

    Wenn Sie das Canvas verwenden, ist jeder Ball ein Datenobjekt, und alle Bälle werden bei jedem Tick auf das Canvas gezeichnet (Listing 2).

    class BallDrawing {
      private final Color fill = Color.color(Math.random(), 
        Math.random(), Math.random());
      private final int size = r.nextInt(1, 10);
      private double x = r.nextInt(APP_WIDTH);
      private double y = r.nextInt(APP_HEIGHT - TOP_OFFSET);
      private double dx = r.nextInt(1, 5);
      private double dy = r.nextInt(1, 5);
    
      public void move() {
        if (hitRightOrLeftEdge()) {
          dx *= -1; // Ball hit right or left
        }
        if (hitTopOrBottom()) {
          dy *= -1; // Ball hit top or bottom
        }
        x += dx;
        y += dy;
      }
    
      ...
    }
    

    Verschieben der Objekte

    Die Anwendung verwendet eine Timeline, um alle fünf Millisekunden weitere Objekte hinzuzufügen und sie zu verschieben (Listing 3).

    Timeline timeline = new Timeline(new KeyFrame(Duration.millis(5), t -&amp;gt; onTick()));
    timeline.setCycleCount(Timeline.INDEFINITE);
    timeline.play();
    
    private void onTick() {
      if (TYPE_OF_TEST == 1) {
        // Add ball nodes to the pane
        for (var i = 0; i &amp;lt; ADD_BALLS_PER_TICK; i++) {
          paneBalls.getChildren().add(new BallNode());
        }
    
        // Move all the balls in the pane
        for (Node ballNode : paneBalls.getChildren()) {
          ((BallNode) ballNode).move();
        }
      } else if (TYPE_OF_TEST == 2) {
        // Add balls to the list of balls to be drawn
        for (var i = 0; i &amp;lt; ADD_BALLS_PER_TICK; i++) {
          ballDrawings.add(new BallDrawing());
        }
        
        // Clear the canvas (remove all the previously balls that were drawn)
        context.clearRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight());
    
        // Move all the balls in the list, and draw them on the Canvas
        for (BallDrawing ballDrawing : ballDrawings) {
          ballDrawing.move();
          context.setFill(ballDrawing.getFill());
          context.fillOval(ballDrawing.getX(), ballDrawing.getY(),
            ballDrawing.getSize(),  ballDrawing.getSize());
        }
      }
    }
    

    Ausführen der Anwendung

    Zum Ausführen der Anwendung habe ich folgenden Ansatz gewählt:

    • den Code in einer Datei FxNodesVersusCanvas.java speichern
    • eine Java-Laufzeitumgebung mit JavaFX installieren, z. B. von Azul Zulu [3] oder mit SDKMAN [4]: sdk install java 22.0.1.fx-zulu
    • JBang installieren, entweder von [5] oder mit SDKMAN: sdk install jbang
    • die Anwendung starten mit: jbang FxNodesVersusCanvas.java

    Leistung im Vergleich

    Natürlich hängt die Leistung vom System ab, auf dem Sie die Anwendung ausführen. Wie Sie im Video unter [6] und in Abbildung 2 sehen können, habe ich es sowohl auf einem Apple Mac Studio als auch auf einem Raspberry Pi 5 ausgeführt. Das Ergebnis ist konsistent, da man ungefähr zehnmal mehr Objekte zum Canvas verglichen mit der Anzahl der Nodes hinzufügen kann, bevor die Framerate einbricht. Das ist kein „wissenschaftliches Ergebnis“, aber es vermittelt einen guten Eindruck davon, was mit Canvas erreicht werden kann.

    • Raspberry Pi wird bei 3k Nodes deutlich langsamer als bei 30k Nodes auf Canvas
    • Mac wird bei 15k Nodes langsamer als bei 150k auf Canvas

    Das Bild zeigt zwei nebeneinander angeordnete JavaFX-Demos mit der Überschrift „JavaFX Demo, Nodes versus Canvas“. Beide Fenster zeigen eine große Menge bunter Punkte, die zufällig auf weißem Hintergrund verteilt sind. In der oberen Zeile wird Java-Version 17.0.1 und JavaFX-Version 22.0.1 verwendet. Das linke Fenster zeigt eine Anzahl von 7.210 Punkten und eine Bildrate von 5 FPS (Frames per Second), während das rechte Fenster 7.232 Punkte mit einer Bildrate von 16 FPS anzeigt. Es handelt sich offenbar um einen Vergleich der Leistung von JavaFX-Nodes versus Canvas.

    Abb. 2: Das laufende Experiment

    Fazit

    Eine große Anzahl visueller Komponenten in einer typischen JavaFX-Benutzeroberfläche würde eine schlecht gestaltete Anwendung darstellen. Stellen Sie sich ein langes Registrierungsformular mit Hunderten von Eingabefeldern und Beschriftungen vor … Das würde Ihre Benutzer in den Wahnsinn treiben. Aber in anderen Fällen, in denen Sie eine komplexe Animation oder eine fortgeschrittene Benutzerschnittstellenkomponente erzeugen wollen, ist die Möglichkeit, auf dem Canvas zu zeichnen, ein idealer Ansatz.


    Links & Literatur

    [1] https://gluonhq.com/products/javafx/

    [2] https://gist.github.com/FDelporte/c74cdf59ecd9ef1b14df86e08faa0c56

    [3] https://www.azul.com/downloads/?package=jdk-fx#zulu

    [4] https://sdkman.io

    [5] https://www.jbang.dev

    [6] https://www.youtube.com/watch?v=nJGRW5xP_AE

    [7] https://leanpub.com/gettingstartedwithjavaontheraspberrypi/

    [8] https://www.elektor.com/getting-started-with-java-on-the-raspberry-pi

    The post JavaFX-Animationen auf dem Prüfstand: Node oder Canvas? appeared first on JAX.

    ]]>
    Erweiterte Streams in Java 23: Was JEP 473 Entwicklern bietet https://jax.de/blog/java-23-stream-gatherers-jep-473-neuerungen/ Mon, 02 Sep 2024 12:30:52 +0000 https://jax.de/?p=90169 Java 23 bringt eine Vielzahl von Verbesserungen in der Syntax und bei den APIs. Dieser Artikel fokussiert sich auf „JEP 473 – Stream Gatherers (Second Preview)“ als interessante Neuerung bei Streams. JEP 473 folgt auf JEP 461 aus Java 22, in dem Stream Gatherers bereits als Erweiterung der Stream-Verarbeitung vorgestellt wurden, die die Definition eigener Intermediate Operations erlauben. Schauen wir uns an, was uns Neues erwartet.

    The post Erweiterte Streams in Java 23: Was JEP 473 Entwicklern bietet appeared first on JAX.

    ]]>
    Die in Java 8 LTS eingeführten Streams waren von Anfang an recht mächtig. In den folgenden Java-Versionen wurden verschiedene Erweiterungen im Bereich der Terminal Operations hinzugefügt. Erinnern wir uns: Terminal Operations dienen dazu, die Berechnungen eines Streams abzuschließen und den Stream beispielsweise in eine Collection oder einen Ergebniswert zu überführen.

    Stay tuned

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

     

    Mit JEP 473 wird eine Erweiterung des Stream API zur Unterstützung benutzerdefinierter Intermediate Operations umgesetzt. Darunter versteht man Verarbeitungsschritte wie das Filtern und Transformieren, die sich zu komplexeren Aktionen verbinden lassen. Bisher gab es zwar diverse vordefinierte Intermediate Operations, eine Erweiterungsmöglichkeit war allerdings nicht vorgesehen. Eine solche ist jedoch wünschenswert, um Aufgaben realisieren zu können, die zuvor nicht ohne Weiteres oder nur mit Tricks und eher umständlich umzusetzen waren.

    Einführung

    Nehmen wir an, wir wollten alle Duplikate aus einem Stream herausfiltern und für ein Kriterium angeben. Um es einfach nachvollziehbar zu halten, betrachten wir einen Stream von Strings und als Kriterium deren Länge.

    Hypothetisch wäre das wie folgt mit einer Intermediate Operation in Form einer fiktiven Methode distinctBy() bezüglich der Länge umsetzbar, indem man String::length als Kriterium definiert:

    var result = Stream.of("Tim", "Tom", "Jim", "Mike").
      distinctBy(String::length).   // hypothetisch
      toList();
    
    // result ==> [Tim, Mike]

    Bitte beachten Sie, dass ich mich bei einigen Beispielen von jenen aus dem Original-JEP [1] inspirieren lassen und diese angepasst oder erweitert habe.

    Abhilfe mit den bisherigen Möglichkeiten

    Schauen wir uns einmal an, wie sich Duplikate bezüglich der Stringlänge mit den bisherigen Möglichkeiten des Stream API vermeiden lassen (Listing 1) – den dazu ebenfalls benötigten Record DistinctByLength stelle ich weiter unter vor.

    jshell> var result = Stream.of("Tim", "Tom", "Jim", "Mike").
      ...>                      map(DistinctByLength::new). // #1
      ...>                      distinct().                 // #2
      ...>                      map(DistinctByLength::str). // #3
      ...>                      toList();
    result ==> [Tim, Mike]
    

    Schauen wir uns einmal an, wie sich Duplikate bezüglich der Stringlänge mit den bisherigen Möglichkeiten des Stream API vermeiden lassen (Listing 1) – den dazu ebenfalls benötigten Record DistinctByLength stelle ich weiter unter vor.

    record DistinctByLength(String str)
    {
      @Override
      public boolean equals(Object obj)
      {
        return obj instanceof DistinctByLength(String other) &&
          str.length() == other.length();
      }
    
      @Override
      public int hashCode()
      {
        return str == null ? 0 : Integer.hashCode(str.length());
      }
    }
    

    Dieser Record ist lediglich ein Wrapper um einen String und besitzt dazu ein Attribut str sowie die korrespondierende Zugriffsmethode. Damit wir den Record für unseren Zweck verwenden können, müssen wir die Methoden equals() und hashCode() auf die Stringlänge ausgerichtet überschreiben. In der Implementierung von equals() verwenden wir das Pattern Matching bei instanceof in Kombination mit Record Patterns, wodurch sich der Sourcecode sehr kompakt halten lässt.

    SIE LIEBEN JAVA?

    Den Core-Java-Track entdecken

     

    Beispiel Gruppierung

    Ein weiteres Beispiel für den Bedarf an selbst definierten Intermediate Operations ist die Gruppierung der Daten eines Streams in Abschnitte fixer Größe. Zur Demonstration sollen jeweils vier Zahlen zu einer Einheit zusammengefasst, also gruppiert werden. Für unser Beispiel sollen nur die ersten drei Gruppen ins Ergebnis aufgenommen werden. Auch hier wird wieder der an den JEP angelehnte Sourcecode mit einer fiktiven Methode windowFixed() gezeigt (Listing 3).

    record DistinctByLength(String str)
    {
      @Override
      public boolean equals(Object obj)
      {
        return obj instanceof DistinctByLength(String other) &&
          str.length() == other.length();
      }
    
      @Override
      public int hashCode()
      {
        return str == null ? 0 : Integer.hashCode(str.length());
      }
    }
    

    Neu: Interface Gatherer und die Methode gather()

    Im Lauf der Jahre ist aus der Java-Community einiges an Vorschlägen und Wünschen für Intermediate Operations als Ergänzung für das Stream API eingebracht worden. Oftmals sind diese in ganz spezifischen Kontexten sinnvoll. Hätte man sie alle ins JDK integriert, hätte dies das API allerdings ziemlich aufgebläht und den Einstieg in das (ohnehin schon umfangreiche) Stream API (weiter) erschwert. Um aber dennoch die Flexibilität benutzerdefinierter Intermediate Operations zu ermöglichen, wird ein ähnlicher Ansatz wie bei den Terminal Operations und dem Extension Point in Form der Methode collect(Collector) und des Interface java.util.stream.Collector verfolgt. Durch diese Kombination lassen sich Terminal Operations bei Bedarf individuell ergänzen.

    Um flexibel neue Intermediate Operations bereitstellen zu können, offeriert das Stream API nun eine Methode gather(Gatherer) in Kombination mit dem Interface

    java.util.stream.Gatherer. Wollten wir die zuvor besprochene distinctBy()-Funktionalität selbst realisieren, so könnten wir dazu einen eigenen Gatherer implementieren – das würde jedoch den Rahmen dieser Einführung sprengen.

    Ausgewählte Gatherer

    Praktischerweise sind zur Umsetzung einiger Vorschläge und Wünsche aus der Java-Community nach spezifischen Intermediate Operations bereits ein paar Gatherer in das JDK aufgenommen worden. Sie sind in der Utility-Klasse java.util.stream.Gatherers definiert. Zum Nachvollziehen der Beispiele ist folgender Import nötig:

    jshell> import java.util.stream.*

    windowFixed

    Um einen Stream in kleinere Bestandteile fixer Größe ohne Überlappung zu unterteilen, dient windowFixed() aus dem JDK. Greifen wir das zweite Beispiel aus der Einführung auf und schauen uns an, wie einfach es sich jetzt mit JDK-Basisfunktionalität realisieren lässt.

     

    Nachfolgend wird per iterate() ein unendlicher Stream von Zahlen erzeugt und durch Aufruf von windowFixed(4) jeweils in Teilbereiche der Größe vier untergliedert. Mit limit(3) wird die Anzahl an Teilbereichen auf drei begrenzt und diese werden durch Aufruf von toList() in Form einer Liste als Ergebnis bereitgestellt (Listing 4).

    jshell> var result = Stream.iterate(0, i -> i + 1).
      ...>                      gather(Gatherers.windowFixed(4)).
      ...>                      limit(3).
      ...>                      toList()
    resultNew ==> [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]
    

    Beim Unterteilen in Bereiche fester Größe gibt es einen Spezialfall zu beachten: Enthält ein Datenbestand nicht genügend Elemente, um die gewünschte Teilbereichsgröße zu füllen, enthält der letzte Teilbereich weniger Elemente. Als Beispiel dient ein Stream mit einem durch Aufruf von of() erzeugten fixen Datenbestand der Werte 0 bis einschließlich 6. Dieser wird mit windowFixed(3) in Teilbereiche der Größe drei unterteilt, wodurch der letzte Teilbereich nur ein Element enthält, nämlich die Zahl 6 (Listing 5).

    jshell> var result = Stream.of(0, 1, 2, 3, 4, 5, 6).
      ...>                      gather(Gatherers.windowFixed(3)).
      ...>                      toList()
    result ==> [[0, 1, 2], [3, 4, 5], [6]]
    
    

    windowSliding

    Neben dem Unterteilen in jeweils unabhängige Teilbereiche kann auch eine Untergliederung mit Überlagerungen von Interesse sein. Um einen Stream in kleinere Bestandteile fixer Größe mit Überlappung zu unterteilen, dient die Methode windowSliding() aus dem JDK.

    Wieder wird per iterate() ein unendlicher Stream von Zahlen erzeugt und durch Aufruf von windowSliding(4) jeweils in Teilbereiche der Größe vier untergliedert, allerdings mit einer Überlappung bzw. Verschiebung um ein Element. Mit limit(3) wird die Anzahl an Teilbereichen auf drei begrenzt. Wie zuvor werden diese durch Aufruf von toList() in Form einer Liste als Ergebnis bereitgestellt (Listing 6).

    jshell> var result = Stream.iterate(0, i -> i + 1).
      ...>                      gather(Gatherers.windowSliding(4)).
      ...>                      limit(3).
      ...>                      toList()
    result ==> [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]]
    

    Betrachten wir die Auswirkungen auf das zuvor als Spezialfall aufgeführte Beispiel eines Streams wieder mit den Werten 0 bis inklusive 6. Statt mit windowFixed() wird hier windowSliding() genutzt. Dadurch wird der Datenbestand in sich überlappende Teilbereiche untergliedert. Dementsprechend tritt hier die Situation eines unvollständigen letzten Teilbereichs nicht auf, sondern es werden fünf Teilbereiche mit je drei Elementen erzeugt (Listing 7).

    jshell> var result = Stream.of(0, 1, 2, 3, 4, 5, 6).
      ...>                      gather(Gatherers.windowSliding(3)).
      ...>                      toList()
    result ==> [[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]]
    

    Bei dieser Art von Operation kann der zuvor behandelte Spezialfall eines Datenbestands mit einer nicht ausreichenden Menge an Elementen normalerweise nicht auftreten. Das ist lediglich dann möglich, wenn die Länge der Eingabe kleiner als die Window-Größe ist – in dem Fall besteht das Ergebnis aus der gesamten Eingabe, wie es nachfolgend für einen Datenbestand von drei Werten und eine Window-Größe von fünf zu sehen ist. Das Ergebnis ist eine Liste, die wiederum eine Liste mit drei Elementen enthält (Listing 8).

    jshell> var resultNew = Stream.of(1, 2, 3).
      ...>                         gather(Gatherers.windowSliding(5)).
      ...>                         toList()
    resultNew ==> [[1, 2, 3]]
    

    fold

    Dazu, nämlich die Werte eines Streams miteinander zu verknüpfen, dient die Methode fold(). Sie arbeitet ähnlich wie die Terminal Operation reduce(), die ein Ergebnis aus einer Folge von Elementen erzeugt, indem wiederholt eine Operation zur Kombination, beispielsweise + oder * für Zahlen, auf die Elemente angewendet wird. Dazu gibt man einen Startwert und eine Berechnungsvorschrift an. Diese Letztere fest, wie das bisherige Ergebnis mit dem aktuellen Element verknüpft wird.

    Stay tuned

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

     

    Nutzen wir dieses Wissen als Ausgangsbasis für ein Beispiel mit fold(). Damit lässt sich die Summe der Werte mit 0 als Startwert und einer Addition als Berechnungsvorschrift wie in Listing 9 berechnen.

    jshell> var crossSum = Stream.of(1, 2, 3, 4, 5, 6, 7).
      ...>                        gather(Gatherers.fold(() -> 0L,
      ...>                          (result, number) -> result + number)).
      ...>                        findFirst()
    crossSum ==> Optional[28]
    

    Bedenken Sie, dass gather() einen Stream als Ergebnis zurückgibt. Hier ist das ein einelementiger Stream. Um daraus einen Wert auszulesen, dient der Aufruf von findFirst(), der ein Optional<T> liefert, weil theoretisch der Stream auch leer sein könnte.

    Als Berechnungsvorschrift können wir alternativ etwa eine Multiplikation mit einem Startwert von 1 nutzen und so für die spezielle Wertefolge von 1 bis 7 die Fakultät berechnen (Listing 10). Ganz allgemein handelt es sich um eine Multiplikation der gegebenen Zahlen (Listing 11).

    jshell> var result = Stream.of(1, 2, 3, 4, 5, 6, 7).
      ...>                      gather(Gatherers.fold(() -> 1L,
      ...>                        (result, number) -> result * number)).
      ...>                      findFirst()
    result ==> Optional[5040]
    
    jshell> var result = Stream.of(10, 20, 30, 40, 50).
      ...>                      gather(Gatherers.fold(() -> 1L,
      ...>                        (result, number) -> result * number)).
      ...>                      findFirst()
    result ==> Optional[12000000]
    

    Aktionen für abweichende Typen

    Was passiert, wenn wir zur Kombination der Werte auch solche Aktionen ausführen wollen, die nicht für die Typen der Werte, hier int, definiert sind? Als Beispiel wird ein Zahlenwert in einen String umgewandelt und dieser gemäß dem Zahlenwert durch Aufruf der Methode repeat() der Klasse String wiederholt (Listing 12).

    jshell> var repeatedNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7).
      ...>                               gather(Gatherers.fold(() -> "", 
      ...>                                 (result, number) -> result + 
      ...>                                 ("" +   number).repeat(number))).  
      ...>                               toList()
    repeatedNumbers ==> [1223334444555556666667777777]
    

    Varianten mir reduce()

    Nur der Vollständigkeit halber seien hier die vorherigen Berechnungen als Varianten mit reduce() gezeigt. Weil reduce() eine Terminal Operation ist, lässt sie keine weitere Verarbeitung im Stream mehr zu – zudem funktionieren die Aktionen nur auf den Typen der Werte, womit sich das zuletzt gezeigte Beispiel nicht umsetzen lässt (Listing 13). Genau wie bei fold() werden bei reduce() ein Startwert und eine Berechnungsvorschrift angegeben. Daraus entsteht dann ein Ergebniswert.

    jshell> var sum = Stream.of(1, 2, 3, 4, 5, 6, 7).
      ...>                   reduce(0, (result, number) -> result + number)
    sum ==> 28
    
    jshell> var result = Stream.of(1, 2, 3, 4, 5, 6, 7).
      ...>                      reduce(1, (result, number) -> result * number)
    result ==> 5040
    
    jshell> var result = Stream.of(10, 20, 30, 40, 50).
      ...>                      reduce(1, (result, number) -> result * number)
    result ==> 12000000
    

    scan

    Sollen alle Elemente eines Streams zu neuen Kombinationen zusammengeführt werden, sodass jeweils immer ein Element dazukommt, kommt scan() zum Einsatz. Die Methode arbeitet ähnlich wie fold(), das die Werte zu einem Ergebnis kombiniert. Bei scan() wird dagegen für jedes weitere Element ein neues Ergebnis produziert.

     

    Zunächst nutzen wir dies für die Ermittlung von Summen (Listing 14). Danach kombinieren wir Texte statt Ziffern nur durch Abwandlung des Startwerts, für den wir hier einen Leerstring nutzen, wodurch das + zu einer Stringkonkatenation wird (Listing 15).

    jshell> var crossSums = Stream.of(1, 2, 3, 4, 5, 6, 7).
      ...>                         gather(Gatherers.scan(() -> 0, 
      ...>                           (result, number) -> result + number)).
      ...>                         toList()
    crossSums ==> [1, 3, 6, 10, 15, 21, 28]
    
    jshell> var crossSums = Stream.of(1, 2, 3, 4, 5, 6, 7).
      ...>                         gather(Gatherers.scan(() -> 0, 
      ...>                           (result, number) -> result + number)).
      ...>                         toList()
    crossSums ==> [1, 3, 6, 10, 15, 21, 28]
    

    Man könnte auch eine n-malige Wiederholung realisieren – dabei wird schön der Unterschied zu fold() deutlich (Listing 16).

    jshell> var repeatedNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7).
      ...>                               gather(Gatherers.scan(() -> "",
      ...>                                 (result, number) -> result +   
      ...>                                 ("" + number).repeat(number))). 
      ...>                               toList()
    repeatedNumbers ==> [1, 122, 122333, 1223334444, 122333444455555, 122 ... 3334444555556666667777777]
    

    Fazit

    In diesem Artikel haben wir uns mit den Stream Gatherers als Preview-Feature beschäftigt. Zunächst habe ich erläutert, warum diese Neuerung für uns Entwickler nützlich und hilfreich ist. Danach wurden diverse bereits im JDK vordefinierte Stream Gatherers anhand von kleinen Anwendungsbeispielen vorgestellt. Insbesondere wurden auch Randfälle und Besonderheiten beleuchtet. Dadurch sollten Sie einen guten ersten Überblick gewonnen haben und fit genug sein, um eigene Experimente zu starten.

    Neben den Stream Gatherers enthält Java 23 viele weitere Neuerungen, die die Programmiersprache voranbringen und attraktiver machen. Diese Modernisierung sollte dazu beitragen, dass Java weiterhin konkurrenzfähig bleibt und sich in modernen Anwendungsbereichen behauptet und insbesondere auch zu anderen derzeit populären Sprachen wie Python oder Kotlin aufschließt. Auch für komplette Newbies wird die Einstiegshürde gesenkt: Dank JEP 477 (Implicitly Declared Classes and Instance Main Methods (Third Preview)) lassen sich kleinere Java-Programme viel schneller und mit deutlich weniger Zeilen sowie für Anfänger schwierigen Begrifflichkeiten erstellen.

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

    Stay tuned

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

     

    The post Erweiterte Streams in Java 23: Was JEP 473 Entwicklern bietet appeared first on JAX.

    ]]>
    Java Core 2024: Ein umfassender Überblick https://jax.de/blog/java-core-2024-ueberblick/ Thu, 04 Jul 2024 12:56:46 +0000 https://jax.de/?p=89923 Wer populäre Technologien einsetzt, bekommt im Problemfall schneller Hilfe und hat auch bei der Entwicklerrekrutierung weniger Probleme. Die aktuelle Ausgabe des von dem SaaS-Anbieter New Relic veröffentlichten Status-quo-Reports zeigt, wie es um das Java-Ökosystem steht.

    The post Java Core 2024: Ein umfassender Überblick appeared first on JAX.

    ]]>
    Der New-Relic-Report [1] speist sich überwiegend aus Daten, die das Unternehmen aus den hauseigenen Observability-Systemen erhält. Wir stellen die Ergebnisse vor und gehen kurz auf die Besonderheiten verschiedener GC-Algorithmen ein. Bei der Bewertung der Daten ist zu beachten, dass die Informationen auf im Einsatz befindlichen Systemen beruhen – Prototypen und nur auf der Workstation eines Entwicklers lebende Programme werden nur selten mit New Relic verbunden.

    Stay tuned

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

     

    Schnellere Releases

    Oracles Entscheidung, die Auslieferung neuer Java-Releases zu beschleunigen, wirkt sich auf das gesamte Ökosystem aus. Java 21 war aufgrund der Inklusion diverser Preview-Technologien ein besonders interessantes Release, das rasch angenommen wurde. Genauer: Nach der Auslieferung wurden binnen sechs Monaten 1,4 Prozent der überwachten Applikationen umgestellt – die Vorgängervariante Java 17 erreichte in der entsprechenden Zeitspanne nur eine Umstellung von 0,37 Prozent.

    Relevant ist auch, dass Nicht-LTS-Versionen in den Ergebnissen von New Relic eine untergeordnete Rolle spielen: Weniger als 2 Prozent der untersuchten Programme nutzen sie produktiv.

    Mindestens ebenso relevant ist die Frage, welches JDK beziehungsweise welche Runtime zur Ausführung verwendet wird – Oracle hat in der Vergangenheit durch eigenwillige lizenzpolitische Entscheidungen einiges an Goodwill verspielt, was sich in den Zahlen widerspiegelt (Abb. 1).

    Abb. 1: Das Wachstum anderer Anbieter erfolgt fast ausschließlich auf Oracles Kosten (Bildquelle: [1])

    Der Höhenflug von Amazon war dabei von kurzer Dauer, mittlerweile liegt der Großteil des Wachstums im Bereich des von der Eclipse Foundation verwalteten Adoptium. Azul Systems ist mehr oder weniger konstant, auch Red Hat und BellSoft erfreuen sich einer loyalen Nutzerschaft. SAP und Ubuntu konnten ihre (minimalen) Mindshare-Anteile indes nicht wirklich halten.

    Ressourcen und ihr Management

    Neue Designparadigmen wie Serverless und Microservices wurden von der Java-Community schnellstmöglich umgesetzt bzw. angenommen. Das geht mit interessanten Veränderungen hinsichtlich des Ressourcenbedarfs der individuellen Komponenten eines Systems einher.

    Neue Designparadigmen wie Serverless und Microservices wurden von der Java-Community schnellstmöglich umgesetzt bzw. angenommen. Das geht mit interessanten Veränderungen hinsichtlich des Ressourcenbedarfs der individuellen Komponenten eines Systems einher.

    Kennzahl 2023 [%]
    1 bis 4 57,7
    5 bis 8 25
    9 bis 16 8,2
    17 bis 32 4,1
    33 bis 64 3,0
    Mehr als 64 2,0

    Tabelle 1: Verfügbare logische Prozessoren nach Java-Anwendungen im Jahr 2023

    Im Bereich der JVM-Speicherzuweisungen setzt sich dieser Trend dagegen nicht fort. Sehr kleine VMs sind nach wie vor sehr beliebt, während sehr große Speicherbereiche eine (eher geringe) Schrumpfung zeigen. Der Gutteil der Systeme kommt indes mit weniger als 2 GB aus, was die im Markt vorherrschende Meinung von Java als Speicherfresser relativiert.

    SIE LIEBEN JAVA?

    Den Core-Java-Track entdecken

     

    Insbesondere im Embedded-Bereich wird Java wegen der durch den Garbage Collector (GC) systemimmanenten „Denkpausen“ kritisiert. Die diversen JVM-Anbieter begegnen diesem Problem seit einiger Zeit durch neuartige GC-Algorithmen, die insbesondere auf mehrkernigen Systemen die Minderung der Probleme ermöglichen (Abb. 2). Zu berücksichtigen ist dabei, dass die verschiedenen VMs unterschiedliche Standardeinstellungen mitbringen – die Umstellung von Java 11 auf G1 und der damit einhergehende Zuwachs an darauf basierenden Systemen belegt, dass viele Installationen nach dem Prinzip „defaults are fine“ agieren.

    Abb. 2: Garbage Collectors, die von Java-LTS-Versionen genutzt werden (Bildquelle: [1])

    Der Rückgang im Bereich des Klassikers Serial ist im Zusammenhang mit der oben besprochenen Änderung an der Konstruktion der Systeme interessant – er hält das Gesamtsystem an, ist aber auf ressourcenbeschränkten Systemen am effizientesten.

    Im Fall von G1 gilt aufgrund des Aufräumens kleiner Speicherbereiche, dass das System insbesondere auf nebenläufigen Maschinen für weniger Ärger sorgt. Der Garbage Collector kann seine Stärken vor allem dann ausspielen, wenn die JVM mehr als 4 GB Speicher zugewiesen hat. Die feinere Unterteilung macht die Aufräumprozesse effizienter.

    Im Fall von G1 gilt aufgrund des Aufräumens kleiner Speicherbereiche, dass das System insbesondere auf nebenläufigen Maschinen für weniger Ärger sorgt. Der Garbage Collector kann seine Stärken vor allem dann ausspielen, wenn die JVM mehr als 4 GB Speicher zugewiesen hat. Die feinere Unterteilung macht die Aufräumprozesse effizienter.

    Frameworks am Puls der Zeit

    Die Verfügbarkeit von Modularisierungssystemen wie Maven oder die in Gradle integrierte Artefaktverwaltung animieren Entwickler dazu, Komponenten aus dem Ökosystem zur Erfüllung der anstehenden Aufgaben heranzuziehen.

     

    Die erste in diesem Zusammenhang wichtige Frage betrifft die Art der Datenspeicherung: Schon aus dem Enterprise-Fokus folgt, dass Java-Applikationen häufig mit Datenbankservern aller Sorten interagieren. Die Herkunft von Java aus dem Sun- bzw. Oracle-Umfeld spiegelt sich in einer klaren Marktdominanz der Oracle Database wider, die in fast 20 Prozent der von New Relic überwachten Java-Applikationen zum Einsatz kommt (Tabelle 2).

    Datenbank Nutzerschaft [%]
    Oracle Database 17,3
    PostgreSQL 14,4
    MySQL 12,5
    MongoDB 7,4
    DynamoDB 4,9
    SQL Server 4,4
    Cassandra 2,7
    Elasticsearch 2,5
    MariaDB 1,4
    Redshift 0,3

    Tabelle 2: Die beliebtesten Datenbankserver [1]

    Dabei dominieren klassische, auf SQL basierende Datenbanken: Werden die Anteile der drei Bestplatzierten addiert, erhält man einen Gesamtwert von 44,2 Prozent. An vierter Stelle folgt MongoDB; Java-Datenbanken wie die Graphdatenbank Neu4J sind überhaupt nicht auf den Rangplätzen anzutreffen.

    Ein weiteres Thema betrifft den Verbreitungsgrad der Kryptographienutzung: Aus den von New Relic erhobenen Zahlen lässt sich ableiten, dass 41 Prozent der überwachten Applikationen auf die ein oder andere Weise auf eine Kryptographiebibliothek zurückgreifen. Das muss aber nicht unbedingt durch eine explizite Willensäußerung des Entwicklerteams bedingt sein, es ist genauso gut vorstellbar, dass die Bibliothek als Dependency einer anderen Bibliothek Eingang in die Build-Artefaktliste findet.

     

    Jedenfalls zeigt die Verteilung der verwendeten Bibliotheken nur wenig Überraschendes (Abb. 3). Der erste Platz geht an den Klassiker Bouncy Castle, während die in diversen Spring-Frameworks inkludierte Spring Security den zweiten Platz einnimmt.

    Abb. 3: Meistgenutzte Verschlüsselungsbibliotheken für Java-Anwendungen (Bildquelle: [1])

    In seinem Report weist New Relic darauf hin, dass man ein baldiges deutliches Wachstum von Amazon Corretto erwartet. Ursache dafür ist demnach erstens die Vereinheitlichung der Software-Supply-Chain und zweitens die im Allgemeinen sehr gute Performance der diversen von Amazon implementierten Algorithmen.

    Eine weitere im Report gestellte Frage betrifft die Art, wie Java-Applikationen Logging-Informationen sammeln. SLF4J wird dabei von 83 Prozent der Entwickler benutzt und damit ein Framework, das wie in Abbildung 4 schematisch dargestellt als Abstraktionsschicht zwischen der Applikation und dem jeweiligen Logging-Framework fungiert und zu einer Steigerung der Flexibilität beiträgt.

    Abb. 4: SLF4J abstrahiert zwischen Applikationscode und dem jeweiligen Logging-Framework

    Neben diesem genutzten Shortcut gilt, dass sich Log4j nach wie vor als absoluter Platzhirsch im Bereich der Logging-Frameworks etabliert hat: In 76,4 Prozent der von New Relic überwachten Applikationen findet sich eine Abhängigkeit auf diesen Universallogger. An zweiter Stelle steht JBoss (Abb. 5).

    Abb. 5: Die beliebtesten Logging-Frameworks (Bildquelle: [1])

    Stack Overflow als Pulsmesser

    Dass die Anzahl der im Entwicklerfragedienst Stack Overflow zu bestimmten Technologien sichtbaren Interaktionen eine gute Benchmark für die Popularität der jeweiligen Technologie darstellt, soll in den folgenden Schritten als gegeben angenommen werden. Im Hause NewRelic bietet man mit GenAI seit einiger Zeit etwas Vergleichbares an, das auf Java-Entwickler fokussiert ist. Abbildung 6 zeigt, wie sich die Anfragen an diese KI über die verschiedenen Kategorien verteilen. Unter Learning versteht man dabei im Hause New Relic dabei nicht Machine Learning. Vielmehr handelt es sich um Fragen, die man auch als „How to“-Questions bezeichnen würde.

    Abb. 6: Entwicklerfragen an die New-Relic-KI nach Themen (Bildquelle: [1])

    Fazit

    Die von New Relic erhobenen Informationen geben Entwicklern und Nutzern einen Überblick über den Zustand der Java-Entwicklung als Ganzes. Die rasche Annahme neuer Technologien zeigt, dass Sorgen um das Ableben der Java-Entwicklung gelinde gesagt vollkommen übertrieben sind.

    Stay tuned

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

     


    Links & Literatur

    [1] https://newrelic.com/sites/default/files/2024-05/new-relic-state-of-the-java-ecosystem-report-2024-05-21.pdf

    [2] https://openjdk.org/jeps/376

    The post Java Core 2024: Ein umfassender Überblick appeared first on JAX.

    ]]>
    Exactly Once in verteilten Systemen: Realität oder Utopie? https://jax.de/blog/exactly-once-idempotenz-java/ Mon, 10 Jun 2024 13:24:45 +0000 https://jax.de/?p=89825 Sind verteilte Systeme im Einsatz, wie es beispielsweise bei Microservices der Fall ist, ist eine verteilte Datenverarbeitung – häufig asynchron über Message Queues – an der Tagesordnung. Werden Nachrichten ausgetauscht, sollen diese häufig genau einmal verarbeitet werden – gar nicht so einfach, wie sich herausstellt.

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

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

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

    Abb.1: Einfacher Nachrichtenaustausch

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

    Stay tuned

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

     

    Es kann so einfach sein

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

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

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

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

    Immer Ärger mit der Kommunikation

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

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

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

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

    ALL ABOUT MICROSERVICES

    Microservices-Track entdecken

     

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

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

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

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

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

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

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

    SIE LIEBEN JAVA?

    Den Core-Java-Track entdecken

     

    Lösungen müssen her

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

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

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

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

    Abb. 6: Eine idempotente Verarbeitung

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

    Stay tuned

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

     

    Fazit

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

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

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

    ]]>
    Java 22 … and beyond | JAX 2024 Keynote https://jax.de/blog/java-release-cycle-beschleunigt-neue-features-java-22/ Tue, 07 May 2024 11:20:44 +0000 https://jax.de/?p=89728 Brian Goetz, Java Language Architect bei Oracle, hielt am 24. April auf der JAX eine fesselnde Keynote. Er hob die jüngsten Fortschritte von Java hervor und ging dabei über die Funktionen von Java 22 hinaus. Was hält die Zukunft für Java-Entwickler:innen bereit?

    The post Java 22 … and beyond | JAX 2024 Keynote appeared first on JAX.

    ]]>
    Das neue Release-Modell für Java-Versionen ist laut Goetz ein klarer Vorteil für Entwickler:innen. Es ermöglicht einen schnelleren Zugang zu den neuesten Features und Sicherheitsupdates und sorgt so für moderne und sichere Anwendungen.

    Die Verbesserungen gehen aber weit über den Release-Zyklus hinaus. Bahnbrechende Innovationen wie Project Loom (leichtgewichtige virtuelle Threads) und ZGC (Low-Pause Garbage Collection) haben die Java-Entwicklung nachhaltig verändert.

    SIE LIEBEN JAVA?

    Den Core-Java-Track entdecken

     

    Das Projekt Panama verbessert die Entwickler:innen-Erfahrung durch eine sicherere und leistungsfähigere Methode zur Interaktion mit nativen Codebibliotheken. Das Projekt Amber führt datenorientierte Programmierfunktionen wie Records und Pattern Matching ein und macht Java ausdrucksstärker.

    Die Zukunft ist vielversprechend, aber Goetz räumt auch ein, dass es Herausforderungen gibt. Library-Upgrades können durch Abhängigkeiten von älteren Funktionen behindert werden. Um diese Lücke zu schließen, schlägt er einen schnelleren Release-Zyklus für das Library-Ökosystem vor, um mit der Innovation von Java Schritt zu halten.

    Diese Keynote ist vollgepackt mit Erkenntnissen für Java-Entwickler:innen. Sehen Sie sich die Aufzeichnung an, um mehr über diese Fortschritte zu erfahren und zu sehen, was Java als Nächstes erwartet!

    Stay tuned

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

     

    The post Java 22 … and beyond | JAX 2024 Keynote appeared first on JAX.

    ]]>