Von der Erfüllung des Slogans „The Network is the Computer“ über den Java EE-Standard bis hin zur Unterstützung von SOAP- und REST-Webdiensten bis hin zur Schaffung eines starken Ökosystems und Communities, die Microservices und Cloud-native Anwendungen unterstützen, die ohne den Schwerpunkt auf APIs nicht möglich sind. Java ist seit seiner Einführung mit dem Internet verflochten und unterstützt bestehende sowie eigene herstellerneutrale Standards und De-facto-Standards, die aktuelle Herausforderungen widerspiegeln, wie etwa MicroProfile oder die Unterstützung der Integration mit KI.
Java wird dieses Jahr 30 Jahre alt und niemand kann zählen, wie viele Anwendungen in den drei Jahrzehnten mit Java erstellt wurden. Aber diese Anwendungen existieren selten isoliert!
Ich versuche immer, Leute, die an Entwicklungsprojekten arbeiten, an die oft übersehene Bedeutung von Remote-APIs und Integrationsaspekten zu erinnern, die für die Verbindung von Softwareteilen untereinander kritisch sind. Ich betone auch oft, dass die gute Kommunikation und Zusammenarbeit der Menschen in einem Projekt wichtiger ist als die Lösung der meisten technischen Probleme. In diesem Artikel möchte ich die große Rolle zeigen, die Java bei der Verbindung von Softwaresystemen und der Integration verschiedener Personengruppen in der Softwareindustrie gespielt hat.
Java wurde bei Sun Microsystems entwickelt, dessen Slogan „The Network is the Computer“ lautete. Als Java geboren wurde, wurde den Menschen klar, dass nicht jedes Netzwerk ausreichte. Das Internet sollte die Zukunft der Informatik prägen.
Bereits die erste Version von Java bot Entwicklern eine einfache integrierte Unterstützung für viele wichtige Funktionen des Internetzeitalters: das HTTP-Protokoll, in Unicode codierte Texte in jeder Sprache, effiziente Parallelität dank Threads, das Rendern von Webgrafikformaten wie JPEG, GIF oder PNG usw.
Java zielte zunächst auf Embedded Computing ab. Die plattformunabhängige JVM und die Java-API wurden so definiert, dass Java-Code einmal geschrieben und überall (auf jedem Gerät) ausgeführt werden konnte. Was die meisten Java-Frühanwender jedoch tatsächlich ausprobieren konnten, waren Applets: Programme, die in einem Webbrowser ausgeführt wurden und das Internet als Distributionskanal zum Herunterladen des Bytecodes nutzten, der auf jeder physischen CPU-Architektur und jedem Betriebssystem lauffähig war. Da JavaScript damals browserübergreifend inkompatibel oder gar nicht verfügbar war, leisteten Java-Applets Pionierarbeit für das interaktive Erlebnis des Internets, das wir heute als selbstverständlich betrachten.
Das Herzstück des Webs ist HTTP, ein Client-Server-Protokoll. Es zeigte sich schnell, dass Java besonders gut auf der Serverseite eingesetzt werden kann. Die zweite Hauptversion von Java wurde mit der Spezifikation Java (2), Enterprise Edition veröffentlicht. Java EE ermöglichte Java, sich auf der Serverseite zu profilieren, indem die Komplexität eines Servers, der viele Client-Anfragen verarbeitet, an den Java EE-Container abstrahiert wird.
Durch das „Einbinden“ in die Java EE-APIs, beispielsweise die Servlet-API, können sich die Anwendungsentwickler auf die Implementierung ihrer Geschäftslogik konzentrieren.
Java ist nicht die einzige Plattform, die das Konzept eines Anwendungsservers unterstützt. Was sie einzigartig macht, ist die Vielzahl unterschiedlicher kommerzieller Anbieter und Open-Source-Projekte, die die Java EE-Spezifikation implementiert haben. Die Herstellerneutralität wird durch den von der Eclipse Foundation verwalteten Jakarta EE-Standard fortgesetzt. Entwickler, die mit dem Standard vertraut sind, können von einer Containerimplementierung zu einer anderen wechseln. Der Standard ermöglicht Wettbewerb und Innovation und hält gleichzeitig die Java-Community zusammen.
Herstellerneutrale Standards waren jedoch nicht immer rechtzeitig verfügbar, um Entwicklern die benötigten Funktionen für die effektive Entwicklung und Integration ihrer Anwendungen zu bieten. Frühe Versionen von Java EE waren berüchtigt für ihre Komplexität und die Menge an Boilerplate-Code. Das Spring Framework löste dieses Problem, indem es eigene Abstraktionen auf Basis der Java-Standard-APIs bereitstellte und damit die Herzen vieler Enterprise-Java-Entwickler eroberte. Aber auch wenn man Spring für seine serverseitige Anwendung nutzt, ist er immer auch von einer bestimmten Java EE-Version abhängig. Viele der von Spring bereitgestellten Abstraktionen inspirierten ähnliche Funktionen in neueren Java/Jakarta EE-Spezifikationen.
Neben der Integration von Remote-APIs müssen unsere Anwendungen intern integriert werden und mehrere Module und Schichten miteinander verbinden. Das Spring Framework bietet ein Framework für die Dependency Injection, um die Kopplung zwischen den Modulen zu vermeiden. Jetzt verfügen wir über Dependency Injection in Java EE mit dem CDI-Standard.
Zusätzlich zu der synchronen Kommunikation über die Servlet-API erkannte Java EE mit der Definition des JMS-Standards auch die Bedeutung der asynchronen Kommunikation über Message Broker. Durch den Wegfall direkter Verbindungen zwischen den Kommunikationspartnern verringert die nachrichtenorientierte Integration die Kopplung und verbessert die Flexibilität und Skalierbarkeit von Systemen. Dank der Standard-API kann derselbe Java-Code mit verschiedenen Messaging-Server-Implementierungen arbeiten.
Wir erleben auch eine zunehmende Beliebtheit von nicht-JMS-basierten asynchronen Kommunikationsplattformen wie Kafka oder verschiedenen cloudbasierten Messaging-Diensten. Mit Java kann man die jeweiligen nativen Clients direkt nutzen. Es gibt aber auch Optionen, die es ermöglichen, die Details der Messaging-Plattform zu abstrahieren und sie als reine „Dumb Pipes“ in Lösungen basierend auf Enterprise Integration Patterns zu verwenden. Apache Camel bietet eine ausgereifte DSL – und Spring Cloud Stream einen modernen funktionalen Programmierstil.
Der Erfolg des Webs und seine Unterstützung durch verschiedene Geräte, Betriebssysteme und Anwendungen brachten die Idee mit sich, dass die Infrastruktur des Webs für mehr als nur HTML-Seiten genutzt werden könnte. Der Begriff Web Services wurde für die Nutzung von Webprotokollen und -infrastruktur zur allgemeinen Anwendungsvernetzung übernommen. Der erste Webservices-Standard (umgangssprachlich auch „SOAP Web Services“ genannt), der vom World Wide Web Consortium (W3C) entwickelt wurde, ermöglichte eine beispiellose Interoperabilität von APIs, die von verschiedenen Technologie-Stacks implementiert wurden. Insbesondere vereinfachte er die Integration Java-basierter Anwendungen mit Microsoft-spezifischen Implementierungen erheblich.
Aufgrund der Vielfalt der Java-Community entstanden zahlreiche Bibliotheken und Frameworks zur Implementierung des W3C-Webservices-Standards: Apache Axis, CXF oder Metro. Ein deklarativer, annotationsbasierter Ansatz für Webservices wurde später von JAX-WS standardisiert.
Die W3C-Webdienste erwiesen sich als unnötig komplex und duplizierten Funktionen, die bereits in HTTP selbst verfügbar waren. Viele Entwickler und Architekten bevorzugten zunehmend RESTful-Webdienste und tauschten XML gegen das einfachere JSON als Format für ihre Daten ein.
REST ist ein sehr beliebter API-Stil und kann mit verschiedenen Java-Frameworks implementiert werden: verschiedenen Implementierungen von JAX-RS (jetzt Jakarta RESTful Web Services) oder Spring Web. Alle Frameworks verwenden Annotationen, um die API-Endpunktpfade, Methoden, Parameter usw. deklarativ anzugeben und JSON-Nutzlasten automatisch in/aus Java-Modellen zu serialisieren/deserialisieren.
Java-Frameworks vereinfachen und beschleunigen die Bereitstellung und Nutzung von Webservices. Soll eine API jedoch über einen längeren Zeitraum und/oder für viele Clients unterstützt werden, lohnt es sich, die API-Spezifikation als klar von der Implementierung getrenntes Dokument zu pflegen und eine allgemein anerkannte Spezifikationssprache zu verwenden. Der W3C (SOAP) Webservices-Standard umfasst das WSDL-Format zur Spezifikation der APIs. Für RESTful Webservices entwickelte sich die am häufigsten verwendete Spezifikationssprache von Swagger zu OpenAPI.
Der specification-first Ansatz zur API-Entwicklung beginnt mit einem hochwertigen OpenAPI-Dokument. Der OpenAPI Generator ist ein ausgereiftes, in Java geschriebenes Open-Source-Projekt, das Java-Schnittstellen und -Modelle aus OpenAPI generieren kann. Mithilfe von Maven- und Gradle-Plugins kann er in den Build integriert werden, sodass der Java-Code stets mit der API-Spezifikation übereinstimmt. Der Generator kann auch Code in vielen anderen Sprachen generieren, sodass OpenAPI eine gute Möglichkeit zur Integration mit Nicht-Java-Systemen bietet.
Wenn man den Code-First-Ansatz bevorzugt, unterstützen alle gängigen Java-Anwendungsframeworks die Generierung von OpenAPI-Dokumenten und einer webbasierten Swagger-UI aus dem annotierten Java-Code.
Unabhängig davon, ob die APIs spezifikationsorientiert oder codeorientiert sind, empfiehlt es sich aus verschiedenen Gründen, sie in einer von der Kerndomänenlogik Ihrer Anwendung getrennten Ebene zu halten. Das bedeutet auch, dass jede Ebene eigene Objekte zur Speicherung der Anwendungsdaten verwenden sollte. Auch hier kommt die Annotationsverarbeitung zur Hilfe: Wir können die zwischen den Ebenen übertragenen Daten deklarativ mit MapStruct umwandeln.
Wir verwenden einen speziellen Typ von Java-Objekten, die sogenannten Data Transfer Objects (DTOs), um die API-Nutzlasten sowie die an die Domänenebenen der Anwendung übergebenen Daten zu modellieren. Es handelt sich dabei nicht um typische Objekte im Sinne der objektorientierten Programmierung. Sie enthalten in der Regel keine anderen Methoden als die zum Erstellen des Objekts (einschließlich Validierung) und zum Lesen seiner Attribute erforderlichen. Die Arbeit mit diesem Objekttyp passt gut zur zunehmenden Beliebtheit der datenorientierten Programmierung (einer Teilmenge der funktionalen Programmierung). Um die Datenobjekte für Streams und parallele Verarbeitung sicher zu machen, empfiehlt es sich, sie unveränderlich (immutable) zu machen. Java reagierte auf diesen Trend, indem es den unveränderlichen record-Typ bereitstellte, der den zur Definition eines DTOs erforderlichen Boilerplate-Code reduziert.
Mit zunehmendem Datenverkehr und steigenden Skalierbarkeitsanforderungen für viele APIs wurde das klassische Thread-pro-Anforderung-Programmiermodell der Servlet-API (und anderer serverseitiger APIs) zunehmend als Engpass wahrgenommen, vor allem weil integrationsbezogener Code einen erheblichen Teil seiner Verarbeitungszeit mit dem Warten auf I/O verbringt. Viele Java-Projekte versuchten zunächst, das Problem durch die Umstellung auf das reaktive Programmierparadigma zu lösen und Anwendungen zu entwickeln, die Daten mithilfe von Reactive Streams verarbeiten, die von RxJava oder Project Reactor implementiert wurden. Die verschiedenen Implementierungen von Reactive Streams nutzen dieselben Standardschnittstellen, die Teil der Java-Sprache wurden.
Für die meisten Anwendungen war die zusätzliche Komplexität der reaktiven Programmierung jedoch nicht allein durch die Notwendigkeit, blockierende Threads zu vermeiden, gerechtfertigt. Virtual Threads aus Project Loom lösen das Problem eleganter. Anwendungen können weiterhin das Thread-pro-Anforderung-Programmiermodell verwenden (das einfach zu lesen und zu debuggen ist), während Java-Standardbibliotheken und die JVM die begrenzte Anzahl an Plattform-Threads (auf Betriebssystemebene verfügbare Threads) wiederverwenden, sodass die Verwendung von Millionen (virtueller) Threads, von denen viele blockiert sind, kein Problem mehr darstellt.
Hier zeigt sich das Muster: Ein häufiges Problem in der Softwarebranche führt zunächst zu Lösungen auf Basis neuer Bibliotheken und Frameworks, die dank der Flexibilität und Typsicherheit von Java praktikabel sind und schnell übernommen werden. Später werden die verschiedenen Lösungen standardisiert, um die Implementierungen interoperabel zu machen. Später können die Implementierungen durch neue Funktionen von Java und JVM vereinfacht werden. Letztendlich kann die gesamte Java-Community die Lösung für das häufige Problem problemlos in verschiedenen Kontexten wiederverwenden.
Skalierbarkeit, häufige Bereitstellungen und die Reduzierung von Abhängigkeiten zwischen Entwicklungsteams sind Motivationsfaktoren für die wachsende Attraktivität verteilter Anwendungen auf Basis der Serviceorientierten Architektur (SOA), später umbenannt in Microservices. Es überrascht nicht, dass eine gute Integration der Dienste durch gutes API-Design und eine entsprechende Implementierung in solchen Systemen von großer Bedeutung ist. Bereitstellung und Betrieb der vielen separaten Dienste verlagerten sich von Anwendungsservern (JEE-Containern) auf Docker-Container und Cloud-Deployments.
Selbstständige Dienste, die eine ausführbare JAR-Datei verwenden, anstatt in einem separaten JEE-Container deployed zu werden, wurden mit Spring Boot eingeführt. Um später die Startzeit und die Effizienz der Rechenressourcen drastisch zu verbessern, war es notwendig, den Prozess der Suche nach Anwendungskomponenten, ihrer Konfiguration und Verbindung so weit wie möglich von der dynamischen Verarbeitung beim Anwendungsstart auf die Auswertung zur Build-Zeit umzustellen. Diese Idee führte zur Entwicklung von Frameworks wie Micronaut und Quarkus.
Die neuen Frameworks versuchten, Entwicklern den Umstieg zu erleichtern, indem sie Konzepte des bestehenden De-facto-Spring-Standards (Micronaut mit Controllern) oder von Java/Jakarta EE wiederverwendeten. Nicht alle Elemente von Jakarta EE ließen sich in den Microservices-Frameworks praktikabel unterstützen, und andererseits gab es neue Aspekte der Dienste, die von einer Standardisierung profitieren konnten, um eine Fragmentierung der Java-Community zu vermeiden. Ebenfalls unter dem Dach der Eclipse Foundation gepflegt, gibt es nun die MicroProfile-Spezifikation, den Standard, der das Core Profile mit Jakarta EE teilt und von Quarkus, Helidon und anderen implementiert wird.
MicroProfile enthält viele integrationsrelevante Komponenten:
- Jakarta RESTful Web Services, JSON-Binding und JSON-Verarbeitung (bereits oben erwähnt)
- Jakarta OpenAPI zur Generierung von OpenAPI aus Java-Code
- Jakarta REST Client zur Nutzung von RESTful Web Services
- Jakarta Fault Tolerance mit Wiederholungsversuchen, Circuit Breaker usw.
- Jakarta JWT Authentication zur Berücksichtigung von Sicherheitsaspekten durch spezialisierte OAuth-Server
- Jakarta Telemetry zur optimalen Überwachung unserer Dienste und APIs
- Jakarta Config und Health zur Konfiguration und Orchestrierung der Dienste in realen Umgebungen
Wenn wir über Integration sprechen, sollten wir das Testen nicht vergessen. Natürlich verfügen wir mit Java über das hervorragende JUnit-Framework zur Automatisierung unserer Tests. Doch die Schichten unserer Anwendungen, die Integrationen und Remote-APIs verarbeiten, haben ihre Besonderheiten. Die kritischen Teile bestehen oft nicht aus Zeilen unseres Java-Codes, deren Testabdeckung prozentual messbar ist. Integrationsfehler verbergen sich meist in Konfigurationen, Authentifizierung, De-/Serialisierung usw. Es reicht daher nicht aus, sich nur auf einige Klassen zu konzentrieren und den Rest durch Mock-ups zu simulieren.
Um aussagekräftige Integrationstests durchführen zu können, müssen wir die Infrastruktur des Anwendungsframeworks nutzen. Die meisten Frameworks bieten zwar eigene Integrationstestunterstützung, leider ist dieser Bereich jedoch nicht durch einen einheitlichen Standard wie Java/Jakarta EE oder MicroProfile vereinigt. Man kann seine Anwendung jedoch mit jedem Framework als Blackbox ausführen und die Integrationstests über die echten APIs und die echten Netzwerkprotokolle kommunizieren lassen. In Java kann man solche Tests mit der praktischen DSL der RestAssured-Bibliothek durchführen. Um APIs außerhalb des Tests zu simulieren, gibt es ein weiteres hervorragendes Java-basiertes Tool: WireMock.
Das aktuelle Top-Thema ist die geschäftliche Nutzung von KI und insbesondere LLMs. Die Landschaft in diesem Bereich verändert sich rasant, und sowohl die Softwarestruktur als auch die Hardwareanforderungen von KI-Systemen unterscheiden sich deutlich von traditionellen Unternehmensanwendungen. Daher dürfte es für viele Unternehmen sinnvoll sein, bestehende Systeme über Remote-APIs mit LLM- oder RAG-Diensten zu integrieren. Obwohl es viele sich schnell entwickelnde und konkurrierende KI-Produkte gibt, bietet Java bereits Adapter, um die Unterschiede in den APIs zu abstrahieren. Sehr gute Beispiele sind hier Langchain4j und Spring AI.
Ich bin überzeugt, einer der Faktoren, die Java seit 30 Jahren zu einer erfolgreichen Sprache und Plattform gemacht haben, ist die Fähigkeit, Softwaresysteme und Entwickler-Communities miteinander zu verbinden. Projekte, die auf Java basieren, profitieren von Effizienzsteigerungen sowohl im Entwicklungsprozess als auch bei der Ausführung der bereitgestellten Anwendungen. Entwickler können davon ausgehen, dass Java auch in Zukunft relevant bleiben wird.
Gute API-Designs und Integrationslösungen sind jedoch auch bei der Verwendung von Java keine Selbstverständlichkeit. Zudem lassen sich Fehler, nachdem die APIs schon genutzt werden, nicht leicht beheben. Die Fähigkeiten zur API-Entwicklung verbessern sich durch gezielte Schulung und Erfahrung. Die Anwendung dieser Fähigkeiten erfordert, dass Unternehmen und Projekt die Bedeutung der Integration erkennen und ihr die entsprechende Aufmerksamkeit widmen.