REST - JAX https://jax.de/tag/rest/ Java, Architecture & Software Innovation Fri, 18 Oct 2024 13:05:23 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Von Monolithen über modulare Architekturen zu Microservices mit DDD https://jax.de/blog/microservices/von-monolithen-ueber-modulare-architekturen-zu-microservices-mit-ddd/ Mon, 25 Mar 2019 16:06:38 +0000 https://jax.de/?p=67483 In jedem Unternehmen gibt es große Softwaresysteme, die über viele Jahre weiterentwickelt wurden und deren Wartung Jahr für Jahr immer zäher und teurer wird. Vor dem Hintergrund neuer Architekturparadigmen wie Microservices sollen diese Systeme nun modern, skalierbar und flexibel werden. Dabei ist die Hoffnung, dass man sich der großen, schwerfälligen Monolithen entledigen kann, indem man sie in kleinere, besser zu beherrschende Microservices zerlegt.

The post Von Monolithen über modulare Architekturen zu Microservices mit DDD appeared first on JAX.

]]>

von Dr. Carola Lilienthal

Dieses Heilsversprechen klingt gut, beinhaltet aber viele Fallstricke, Missverständnisse und Herausforderungen. Ein Umbau hin zu Microservices ist aufwendig und kann, falsch eingeleitet, zu einem schlechteren Ergebnis führen als es die ursprüngliche Architektur einmal war. Auf Basis der Erfahrungen aus Kundenprojekten der letzten Jahre werde ich in diesem Artikel sinnvolle von unsinnigen Maßnahmen trennen und pragmatische Lösungen vorstellen.

Microservices: Warum?

Microservices sind in den letzten Jahren als neues Architekturparadigma aufgekommen. Viele Entwickler und Architekten dachten zuerst, es ginge bei Microservices nur darum, Softwaresysteme in voneinander unabhängig deploybare Services aufzuteilen. Aber eigentlich haben Microservices einen anderen Sinn: Bei Microservices geht es darum, Software so auf Entwicklungsteams aufzuteilen, dass die Teams unabhängig und eigenständig schneller entwickeln können als vorher. Bei Microservices geht es also zuerst einmal nicht um Technik, sondern um den Menschen.

Ein schlagkräftiges Entwicklungsteam hat eine Größe, bei der Urlaub und Krankheit nicht zu Stillstand führen (also ab drei Personen) und bei der die Kommunikation beherrschbar bleibt (also nicht mehr als sieben bis acht Personen, die sich gegenseitig auf dem Stand der Dinge halten müssen). Ein solches Team soll nun ein Stück Software, ein Modul zum (Weiter-)Entwickeln bekommen, das unabhängig vom Rest des Systems ist. Denn nur, wenn das Modul unabhängig ist, kann das Team eigenständig Entscheidungen treffen und sein Modul weiterentwickeln, ohne auf andere Teams und deren Zulieferungen zu warten.

Diese Unabhängigkeit von anderen Teams ist nur dann möglich, wenn das Softwaresystem nach fachlichen Kriterien zerlegt wird. Technische Kriterien führen dazu, dass es irgendwelche Arten von Frontend- und Backend-Teams gibt (Abb. 1). In so einer technischen Teamaufteilung ist mindestens das Frontend-Team davon abhängig, dass das Backend-Team die Frontend-Schnittstelle um die benötigten Features erweitert. Gibt es noch ein Datenbankteam, so hat das Backend-Team auch keine Freiheit und muss seinerseits auf Anpassungen durch das Datenbankteam warten. Neue Features betreffen in einer solchen Teamstruktur fast immer mehrere Teams, die bei der Umsetzung voneinander abhängig sind und viel miteinander kommunizieren müssen, damit die Schnittstellen stimmen.


Abbildung 1: Technische Aufteilung von Teams

Eine Aufteilung von Teams nach fachlichen Kriterien macht es im Gegensatz dazu möglich, dass ein Team für ein fachliches Modul in der Software zuständig ist, das sich durch alle technischen Schichten von der Oberfläche bis zur Datenbank zieht (Abb. 2).


Abbildung 2: Fachliche Aufteilung von Teams

Neue Features sollten, wenn der Schnitt in fachliche Module gut gelungen ist, jeweils einem Team und seinem Modul zugeordnet werden können. Natürlich ist das erst einmal eine Idealvorstellung – in der Praxis können neue Features dazu führen, dass der Modulschnitt überdacht werden muss, weil das neue Feature die aktuelle fachliche Zerlegung über den Haufen wirft. Oder sie führen dazu, dass das wahrscheinlich etwas zu große Feature so geschickt in kleinere Features zerlegt werden muss, dass verschiedene Teams ihren jeweiligen fachlichen Anteil an dem großen Feature unabhängig von den anderen Teams umsetzen können. Die jeweiligen Teilfeatures sind dann hoffentlich auch allein sinnvoll und können unabhängig voneinander ausgeliefert werden. Der Mehrwert des großen Features wird dem Anwender allerdings erst am Ende zur Verfügung stehen, wenn alle beteiligten Teams fertig sind.

Fachliche Zerlegung: Wie geht das?

Strebt man eine fachliche Aufteilung seines großen Monolithen an, stellt sich die Frage: Wie findet man stabile, unabhängige fachliche Schnitte, entlang derer eine Zerlegung möglich wird? Das relativ vage beschriebene Konzept von Microservices gibt darauf keine Antwort. Deshalb hat in den letzten Jahren Domain-driven Design (DDD) von Eric Evans an Bedeutung gewonnen. DDD bietet neben vielen anderen Best Practices eine Anleitung, wie Domänen fachlich aufgeteilt werden können. Diese fachliche Aufteilung in Subdomänen überträgt Eric Evans auf Softwaresysteme. Das Äquivalent zu Subdomänen ist in der Software der Bounded Context. Sind die Subdomänen gut gewählt und die Bounded Contexts entsprechend in der Software umgesetzt, dann entsteht eine gute fachliche Zerlegung.

Um eine gute fachliche Zerlegung zu finden, hat es sich in unseren Projekten als sinnvoll erwiesen, den Monolithen und die in ihm möglicherweise vorhandene Struktur erst einmal beiseite zu legen und sich noch einmal grundlegend mit der Fachlichkeit, also der Aufteilung der Domäne in Subdomänen, zu beschäftigen. Wir fangen in der Regel damit an, uns zusammen mit den Anwendern und Fachexperten einen Überblick über unsere Domäne zu verschaffen. Das kann entweder mittels Event Storming oder mittels Domain Storytelling geschehen – zwei Methoden, die für Anwender und Entwickler gleichermaßen gut verständlich sind.

In Abbildung 3 ist eine Domain Story zu sehen, die mit den Anwendern und Entwicklern eines kleinen Programmkinos erstellt wurde. Die grundsätzliche Frage, die wir uns bei der Modellierung gestellt haben, ist: Wer macht was womit wozu? Nimmt man diese Frage als Ausgangspunkt, so lässt sich in der Regel sehr schnell ein gemeinsames Verständnis der Domäne erarbeiten.


Abbildung 3: Überblicks-Domain-Story für ein Programmkino

Als Personen bzw. Rollen oder Gruppen sind in dieser Domain Story erkennbar: die Werbeagentur, der Kinomanager, der Verleiher, der Kassenmitarbeiter und der Kinobesucher. Die einzelnen Rollen tauschen Dokumente und Informationen aus, wie den Buchungsplan der Werbung, die Vorgaben für Filme und die Verfügbarkeit von Filmen. Sie arbeiten aber auch mit „Gegenständen“ aus ihrer Domäne, die in einem Softwaresystem abgebildet sind: dem Wochenplan und dem Saalplan. Diese computergestützten Gegenstände sind mit einem gelben Blitz in der Domain Story markiert. Die Überblicks-Domain-Story beginnt links oben mit der Ziffer 1, wo die Werbeagentur dem Kinomanager den Buchungsplan mit der Werbung mitteilt, und endet bei der 16, wenn der Kassenmitarbeiter den Saalplan schließt.

An diesem Überblick lassen sich verschiedene Indikatoren erklären, die beim Schneiden einer Domäne helfen:

Abteilungsgrenzen oder verschiedene Gruppen von Domänenexperten deuten darauf hin, dass die Domain Story mehrere Subdomänen enthält. In unserem Beispiel könnte man sich eine Abteilung Kinomanagement und eine Abteilung Kartenverkauf vorstellen (Abb. 4).

Werden Schlüsselkonzepte der Domäne von den verschiedenen Rollen unterschiedlich verwendet oder definiert, deutet das auf mehrere Subdomänen hin. In unserem Beispiel wird das Schlüsselkonzept „Wochenplan“ vom Kinomanager deutlich umfangreicher definiert als der ausgedruckte Wochenplan, den der Kinobesucher zu Gesicht bekommt. Für den Kinomanager enthält der Wochenplan neben den Vorstellungen in den einzelnen Sälen auch die geplante Werbung, den Eisverkauf und die Reinigungskräfte. Diese Informationen sind für den Kinobesucher irrelevant (gestrichelte Kreise in Abb. 4).

Enthält die Überblicks-Domain-Story Teilprozesse, die von verschiedenen Triggern ausgelöst werden und in unterschiedlichen Rhythmen ablaufen, dann könnten diese Teilprozesse eigene Subdomänen bilden (durchgezogene Kreise in Abb. 4).

Gibt es im Überblick Prozessschritte, an denen Information nur in eine Richtung läuft, könnte diese Stelle ein guter Ansatzpunkt für einen Schnitt zwischen zwei Subdomänen sein (hellblauer Pfeil in Abb. 4).


Abbildung 4: Überblicks-Domain Story mit Subdomänengrenzen

Für echte große Anwendungen in Unternehmen sind die Überblicks-Domain-Stories in der Regel deutlich größer und umfassen mehr Schritte. Sogar bei unserem kleinen Programmkino fehlen im Überblick die Eisverkäufer und das Reinigungspersonal, die sicherlich auch mit der Software interagieren werden. Die Indikatoren, nach denen man in seiner Überblicks-Domain-Story suchen muss, gelten allerdings sowohl für kleine als auch für größere Domänen.

Übertragung auf den Monolithen

Mit der fachlichen Aufteilung in Subdomänen im Rücken können wir uns nun wieder dem Monolithen und seinen Strukturen zuwenden. Bei dieser Zerlegung setzen wir Architekturanalysetools ein, die es uns erlauben, die Architektur im Tool umzubauen und Refactorings zu definieren, die für den echten Umbau des Sourcecodes notwendig sind. Hier eignen sich unterschiedliche Tools: der Sotograph, der Sonargraph, Structure101, Lattix, Teamscale, Axivion Bauhaus Suite und andere.


Abbildung 5: Mob Architecting mit dem Team

In Abbildung 5 sieht man, wie die Zerlegung des Monolithen mit einem Analysetool durchgeführt wird. Die Analyse wird von einem Tool-Pilot, der sich mit dem jeweiligen Tool und der bzw. den eingesetzten Programmiersprachen auskennt, gemeinsam mit allen Architekten und Entwicklern des Systems in einem Workshop durchgeführt. Zu Beginn des Workshops wird der Sourcecode des Systems mit dem Analysewerkzeug geparst (Abb. 5, 1) und so werden die vorhandenen Strukturen erfasst (zum Beispiel Build Units, Eclipse-/VisualStudio-Projekte, Maven-Module, Package-/Namespace-/Directory-Bäume, Klassen). Auf diese vorhandenen Strukturen werden nun fachliche Module modelliert (Abb. 5, 2), die der fachlichen Zerlegung entsprechen, die mit den Fachexperten entwickelt wurde. Dabei kann das ganze Team sehen, wo die aktuelle Struktur nahe an der fachlichen Zerlegung ist und wo es deutliche Abweichungen gibt. Nun macht sich der Tool-Pilot gemeinsam mit dem Entwicklungsteam auf die Suche nach einfachen Lösungen, wie vorhandene Struktur durch Refactorings an die fachliche Zerlegung angeglichen werden kann (Abb. 5, 3). Diese Refactorings werden gesammelt und priorisiert (Abb. 5, 4). Manchmal stellen der Tool-Pilot und das Entwicklungsteam in der Diskussion fest, dass die im Sourcecode gewählte Lösung besser oder weitergehender ist als die fachliche Zerlegung aus den Workshops mit den Anwendern. Manchmal ist aber auch weder die vorhandene Struktur noch die gewünschte fachliche Zerlegung die beste Lösung, und beides muss noch einmal grundsätzlich überdacht werden.

Erst modular, dann Micro

Mit den so gefundenen Refactorings kann die Arbeit am Monolithen beginnen. Endlich können wir ihn in Microservices zerlegen. Doch halt! Spätestens hier sollte man sich die Frage stellen, ob man sein System tatsächlich in einzelne deploybare Einheiten zerlegen will oder ob nicht ein gut strukturierter Monolith ausreicht. Durch das Aufsplitten des Monolithen in einzelne Deployables kauft man sich eine weitere Stufe von Komplexität ein, nämlich die Verteilung. Braucht man Verteilung, weil der Monolith nicht mehr performant genug ist, dann muss man diesen Schritt gehen. Unabhängige Teams kann man mit den heute sehr weit entwickelten Build-Pipelines aber auch in einem wohlstrukturierten Monolithen bekommen.

Ein wohlstrukturierter Monolith, also ein modularer Monolith oder (wie Dr. Gernot Starke einmal sagte) „ein Modulith“, besteht aus einzelnen fachlichen Modulen, die in einem Deployable existieren (Abb. 6). In manchen Architekturen sind die User Interfaces der einzelnen fachlichen Module hochintegriert. Details zu dieser Variante sprengen diesen Artikel und finden sich in [1].


Abbildung 6: Der Modulith, ein wohlstrukturierter Monolith

 

Ein solcher Modulith setzt durch seine Aufteilung in möglichst unabhängige Module ein grundlegendes softwaretechnisches Prinzip guter Softwarearchitektur um: hohe Kohäsion und lose Kopplung. Die Klassen in den einzelnen fachlichen Modulen gehören jeweils zu einer Subdomäne und sind als Bounded Context in der Software zu finden. Das heißt, diese Klassen setzen gemeinsam eine fachliche Aufgabe um und arbeiten dafür umfassend zusammen. Innerhalb eines fachlichen Moduls herrscht also hohe Kohäsion. Um ihre fachliche Aufgabe zu erledigen, sollten die Klassen in einem Modul nichts von Klassen aus anderen fachlichen Modulen brauchen.

Maximal sollten fachliche Updates, zum Beispiel als Events, über für andere fachliche Module möglicherweise interessante Änderungen zwischen den Modulen ausgetauscht werden und Arbeitsaufträge, die in einen anderen Bounded Context gehören, als Command an andere Module weitergegeben werden. Allerdings sollte man hier immer darauf achten, dass diese Benachrichtigungen nicht überhandnehmen oder als verkappte direkte Aufrufe an andere fachliche Module missbraucht werden. Denn lose Kopplung zwischen fachlichen Modulen bedeutet, dass es so wenig Beziehungen wie möglich gibt. Lose Kopplung lässt sich niemals durch den Einsatz eines technischen Event-Mechanismus erreichen. Denn technische Lösungen schaffen eine technische Entkopplung, aber keine fachlich lose Kopplung.

So ein wohlstrukturierter Modulith ist hervorragend auf eine möglicherweise später notwendige Zerlegung in Microservices vorbereitet, weil er bereits aus fachlich möglichst unabhängigen Modulen besteht. Wir haben also alle Vorteile auf der Hand: Unabhängige Teams, die in den Grenzen ihres Bounded Contexts schnell arbeiten können, und eine Architektur, die auf Zerlegung in mehrere Deployables vorbereitet ist.

Der Knackpunkt: Das Domänenmodell

Wenn ich mir große Monolithen anschaue, dann finde ich dort in der Regel ein kanonisches Domänenmodell. Dieses Domänenmodell wird aus allen Teilen der Software verwendet, und die Klassen im Domänenmodell haben im Vergleich zum Rest des Systems sehr viele Methoden und viele Attribute. Die zentralen Domänenklassen, wie zum Beispiel Produkt, Vertrag, Kunde etc., sind dann meist auch die größten Klassen im System. Was ist passiert?

Jeder Entwickler, der neue Funktionalität in das System eingebaut hat, hat dafür die zentralen Klassen des Domänenmodells gebraucht. Allerdings musste er diese Klassen auch ein bisschen erweitern, damit seine neue Funktionalität umgesetzt werden konnte. So bekamen die zentralen Klassen mit jeder neuen Funktionalität ein bis zwei neue Methoden und Attribute hinzu. Genau! So macht man das! Wenn es schon eine Klasse Produkt im System gibt und ich Funktionalität entwickle, die das Produkt braucht, dann verwende ich die eine Klasse Produkt im System und erweitere sie so, dass es passt. Ich will nämlich die vorhandene Klasse wiederverwenden und nur an einer Stelle suchen müssen, wenn beim Produkt ein Fehler auftritt. Schade ist nur, dass diese neuen Methoden im Rest des Systems gar nicht benötigt, sondern nur für die neue Funktionalität eingebaut werden.

Domain-driven Design und Microservices gehen an dieser Stelle den entgegengesetzten Weg. In einem Modulithen, der fachlich zerlegt ist, oder in einer verteilten Microservices-Architektur gibt es in jedem Bounded Context, der die Klasse Produkt braucht, eine eigene Klasse Produkt. Diese kontextspezifische Klasse Produkt ist auf ihren Bounded Context zugeschnitten und bietet nur die Methoden an, die in diesem Kontext benötigt werden. Den Wochenplan aus unserem Kinobeispiel in Abbildung 3 und 4 gibt es im fachlich zerlegten System zweimal. Einmal im Bounded Context Kinomanagement mit einer sehr reichhaltigen Schnittstelle, über die man Werbung zu Vorstellungen einplanen und den Eisverkauf und die Reinigungskräfte einteilen kann. Und zum anderen im Bounded Context Kartenverkauf, wo die Schnittstelle lediglich das Suchen von Filmen und die Abfrage des Filmangebots zu bestimmten Zeiten ermöglicht. Werbung, Eisverkauf und Reinigungskräfte sind für den Kartenverkauf irrelevant und werden in diesem Bounded Context in der Klasse Wochenplan also auch nicht benötigt.

Will man einen Monolithen fachlich zerlegen, so muss man das kanonische Domänenmodell zerschlagen. Das ist in den meisten großen Monolithen eine Herkulesaufgabe. Zu verwoben sind die auf dem Domänenmodell aufsetzenden Teile des Systems mit den Klassen des Domänenmodells. Um hier weiterzukommen, kopieren wir zuerst die Domänenklassen in jeden Bounded Context. Wir duplizieren also Code und bauen diese Domänenklassen dann jeweils für ihren Bounded Context zurück. So bekommen wir kontextspezifische Domänenklassen, die von ihrem jeweiligen Team unabhängig vom Rest des Systems erweitert und angepasst werden können.

Selbstverständlich müssen bestimmt Eigenschaften von Produkt, beispielsweise die Produkt-ID und der Produktname, in allen Bounded Contexts gleich gehalten werden. Außerdem müssen neue Produkte in allen Bounded Contexts bekanntgemacht werden, wenn in einem Bounded Context ein neues Produkt angelegt wird. Diese Informationen werden über Updates von einem Bounded Context an alle anderen Bounded Contexts gemeldet, die mit Produkten arbeiten.

SOA ist keine Microservices-Architektur

Wenn ein System auf diese Weise zerlegt wird, entsteht eine Struktur, die einer IT-Landschaft aus dem Anfang der 2000er Jahre überraschend ähnelt. Verschiedene Systeme (möglicherweise von verschiedenen Herstellern) werden über Schnittstellen miteinander verbunden, arbeiten aber weiterhin autark auf ihrem eigenen Domänenmodell. In Abbildung 7 sieht man, dass es den Kunden in jedem der vier dargestellten Systeme gibt und die Kundendaten über Schnittstellen zwischen den Systemen ausgetauscht werden.


Abbildung 7: IT-Landschaft mit dem Kunden in allen Systemen

Anfang der 2000er wollte man dieser Verteilung der Kundendaten entgegenwirken, indem man Service-orientierte Architekturen (SOA) als Architekturstil verfolgt hat. Das Ergebnis waren IT-Landschaften, in denen es einen zentralen Kundenservice gibt, den alle anderen Systeme über einen Service Bus nutzen. Abbildung 8 stellt eine solche SOA mit einem Kundenservice schematisch dar.

So eine Service-orientierte Architektur mit zentralen Services hat im Lichte der Diskussion um Microservices den entscheidenden Nachteil, dass alle Entwicklungsteams, die den Kundenservice brauchen, nicht mehr unabhängig arbeiten können und damit an individueller Schlagkraft verlieren. Gleichzeitig ist es vermessen zu erwarten oder zu fordern, dass alle Großrechnersysteme, in denen in vielen Unternehmen die zentrale Datenhaltung sichergestellt wird und die als SOA-Services ansprechbar sind, auf Microservices-Architekturen umgebaut werden. In solchen Fällen treffe ich häufig eine Mischung aus SOA- und Microservices-Architektur an, was durchaus gut funktionieren kann.


Abbildung 8: IT-Landschaft mit SOA und Kundenservice

Standortbestimmung

Um für Monolithen eine Bewertungsmöglichkeit zu schaffen, wie gut der Sourcecode auf eine fachliche Zerlegung vorbereitet ist, haben wir in den vergangenen Jahren den Modularity Maturity Index (MMI) entwickelt. In Abbildung 9 ist eine Auswahl von 21 Softwaresystemen dargestellt, die in einem Zeitraum von fünf Jahren analysiert wurden (X-Achse). Für jedes System ist die Größe in Lines of Code dargestellt (Größe des Punktes) und die Modularität auf einer Skala von 0 bis 10 (Y-Achse).


Abbildung 9: Modularity Maturity Index (MMI)

Liegt ein System in der Bewertung zwischen 8 und 10, so ist es im Inneren bereits modular aufgebaut und wird sich mit wenig Aufwand fachlich zerlegen lassen. Systeme mit einer Bewertung zwischen 4 und 8 haben gute Ansätze, jedoch sind hier einige Refactorings notwendig, um die Modularität zu verbessern. Systeme unterhalb der Marke 4 würde man nach Domain-driven Design als Big Ball of Mud bezeichnen. Hier ist kaum fachliche Struktur zu erkennen, und alles ist mit allem verbunden. Solche Systeme sind nur mit sehr viel Aufwand in fachliche Module zerlegbar.

Fazit

Microservices sind als Architekturstil noch immer in aller Munde. Inzwischen haben verschiedene Organisationen Erfahrungen mit dieser Art von Architekturen gemacht und die Herausforderungen und Irrwege sind deutlich geworden.

Um im eigenen Unternehmen einen Monolithen zu zerlegen, muss zuerst die fachliche Domäne in Subdomänen zerlegt und diese Struktur im Anschluss auf den Sourcecode übertragen werden. Dabei sind insbesondere das kanonische Domänenmodell, unsere Liebe zur Wiederverwendung und das Streben nach Service-orientierten Architekturen Hindernisse, die überwunden werden müssen.

Links & Literatur

[1] Lilienthal, Carola: „Langlebige Softwarearchitekturen – Technische Schulden analysieren, begrenzen und abbauen“; dpunkt.verlag, 2017.

Java-Dossier für Software-Architekten 2019


Mit diesem Dossier sind Sie auf alle Neuerungen in der Java-Community vorbereitet. Die Artikel liefern Ihnen Wissenswertes zu Java Microservices, Req4Arcs, Geschichten des DevOps, Angular-Abenteuer und die neuen Valuetypen in Java 12.

Java-Wissen sichern!

The post Von Monolithen über modulare Architekturen zu Microservices mit DDD appeared first on JAX.

]]>
Java 12 Tutorial: So funktionieren die neuen Switch Expressions https://jax.de/blog/core-java-jvm-languages/wechselhaft-switch-expressions-in-java-12/ Tue, 19 Mar 2019 11:09:04 +0000 https://jax.de/?p=67427 Java verharrte bislang beim switch-case-Konstrukt sehr bei den uralten Wurzeln aus der Anfangszeit der Programmiersprache und den Kompromissen im Sprachdesign, die C++-Entwicklern den Umstieg erleichtern sollten. Relikte wie das break und bei dessen Fehlen das Fall-Through waren wenig intuitiv und luden zu Fehlern ein. Zudem war man beim case recht eingeschränkt bei der Notation der Werte. Das alles ändert sich glücklicherweise mit Java 12. Dazu wurde die Syntax leicht modifiziert und erlaubt nun die Angabe einer Expression sowie mehrerer Werte beim case. Auf diese Weise lassen sich Fallunterscheidungen deutlich eleganter formulieren.

The post Java 12 Tutorial: So funktionieren die neuen Switch Expressions appeared first on JAX.

]]>

Die neuen Switch Expressions in Java 12

Als Beispiel für dieses neue Sprachfeature in Java 12 dient die Abbildung von Wochentagen auf deren textuelle Länge. Analysieren wir zunächst, wieso eine Erweiterung und Modifikation in der Syntax und dem Verhalten von switch sinnvoll ist. Was waren die bisherigen Schwachstellen beim switch? Zur Verdeutlichung schauen wir uns in Listing 1 an, wie man die genannte Abbildung vor Java 12 formulieren würde.

Stay tuned

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

 

Listing 1

DayOfWeek day = DayOfWeek.FRIDAY;

int numLetters = -1;

switch (day)
{
  case MONDAY:
  case FRIDAY:
  case SUNDAY:
    numLetters = 6;
    break;
  case TUESDAY:
    numLetters = 7;
      break;
  case THURSDAY:
  case SATURDAY:
    numLetters = 8;
    break;
  case WEDNESDAY:
    numLetters = 9;
    break;
};

 

Betrachten wir den Sourcecode kritisch. Zunächst einmal ist das gezeigte Konstrukt nicht wirklich elegant und ziemlich lang. Auch die Mehrfachangabe von Werten ist gewöhnungsbedürftig. Schlimmer noch: Es wird ein break benötigt, damit die Verarbeitung überraschungsfrei abläuft und es zu keinem Fall-Through kommt. Zudem müssen wir die (künstliche) Hilfsvariable in jedem Zweig korrekt setzen. Wie geht es also besser?

Syntaxerweiterungen mit Java 12: In Java 12 ist mit „Switch Expressions“ ein als Preview gekennzeichnetes Sprachfeature aufgenommen worden, was die Formulierung von Fallunterscheidungen deutlich erleichtert. In Listing 2 ist die intuitive Schreibweise gut ersichtlich:

Listing 2

public static void switchWithReturnAndMultipleValues();
{
  DayOfWeek day = DayOfWeek.FRIDAY;

  int numLetters = switch (day)
  {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY -> 7;
    case THURSDAY, SATURDAY -> 8;
    case WEDNESDAY -> 9;
};
}

An diesem Beispiel erkennen wir folgende syntaktischen Neuerungen: Zunächst ändert sich die Schreibweise beim case. Neben dem offensichtlichen Pfeil statt des Doppelpunkts können nun auch mehrere Werte aufgeführt werden. Bemerkenswert ist vor allem, dass wir kein break mehr benötigen: Die hinter dem Pfeil angegebenen Anweisungen werden jeweils nur spezifisch für das case ausgeführt, und es existiert bei dieser Syntax kein Fall-Through. Schließlich kann das switch nun einen Wert zurückgeben, wodurch sich die Definition von Hilfsvariablen vermeiden lässt.

Aktivierung der Java-12-Syntaxerweiterungen: Weil es sich bei den Switch Expressions leider nicht um ein finales Feature handelt, muss man beim Kompilieren und Ausführen des Programms den Kommandozeilenparameter –enable-preview angeben. Weitere Hinweise finden Sie weiter unten im Text.

 

Java 12: Syntaxvarianten bei „switch“

Während allein schon die zuvor vorgestellten Syntaxänderungen eine tolle Erweiterung darstellen, darf man sich doch an weiteren verbesserten Varianten von switch erfreuen: Zuweisungen im Lambda und break mit Rückgabewert.

Zuweisungen im Lambda: Im ersten Beispiel zu der Java-12-Syntax wurde auf der rechten Seite lediglich ein Wert zurückgegeben. Es ist aber weiterhin problemlos möglich, dort eine Zuweisung oder Methodenaufrufe vorzunehmen, selbstverständlich nach wie vor ohne die Notwendigkeit für ein break (Listing 3).

Listing 3

public static void switchAssignment()
{
  final int value = 2;
  String numericString;

  switch (value)
  {
    case 1 -> numericString = "one";
    case 2 -> numericString = "two";
    case 3 -> numericString = "three";
    default -> numericString = "N/A";
  }

  System.out.println("value:" + value + " as string: " + numericString);
}

„break“ mit Rückgabewert: Alle, die einmal eine Java-Zertifizierung machen möchten, freuen sich bestimmt über eine weitere Variante der Syntax. Kommen wir dazu nochmals auf die bisherige Syntax zurück und bilden Namen von Farben auf deren Anzahl an Buchstaben ab – hier bewusst mit einem kleinen Flüchtigkeitsfehler zur Demonstration von Fall-Through versehen (Listing 4).

Listing 4

Color color = Color.GREEN;
int numOfChars;

switch (color)
{
  case RED: numOfChars = 3; break;
  case BLUE: numOfChars = 4; break;
  case GREEN: numOfChars = 5; /* break; UPS: FALL-THROUGH */
  case YELLOW: numOfChars = 6; break;
  case ORANGE: numOfChars = 6; break;
  default: numOfChars = -1;
}

System.out.println("OLD: " + numOfChars);

In diesem Beispiel erkennt man die Nachteile der alten Syntax, die doch etwas sperrig in der Handhabung ist und ohne explizite Kennzeichnung den Fall-Through kaum ersichtlich macht. Da ist der Zwang zur Definition der künstlichen Hilfsvariable eher nur ein Schönheitsmakel. Schauen wir, wie es mit Java 12 besser geht.

Umsetzung mit Java 12: Die nachfolgend gezeigte Variante mit Java 12 ist recht nah an der alten Syntax, jedoch mit dedizierten Verbesserungen: Als eher minimale Abwandlung kann man hinter dem break einen Rückgabewert notieren. Zudem können mehrere Werte in case aufgeführt werden. Insgesamt ergibt sich damit die in Listing 5 dargestellte, besser lesbare Variante.

Listing 5

public static void switchBreakReturnsValue()
{

  Color color = Color.GREEN;

  int numOfChars = switch (color)
  {
    case RED: break 3;
    case BLUE: break 4;
    case GREEN: break 5;
    case YELLOW, ORANGE: break 6;
    default: break -1;
  };
  System.out.println("color:" + color + " ==> " + numOfChars);
}

In diesem Beispiel werden die Vorteile der Syntax deutlich. Diese ist zwar sehr ähnlich zu der bisherigen Variante, doch gibt es einige Erleichterungen und Verbesserungen. Zum einen benötigt man keine künstliche Hilfsvariable mehr, sondern kann mit break einen Wert zurückgeben, wodurch die Abarbeitung dort gestoppt wird, also ähnlich zu dem return einer Methode. Schließlich kann man nun mehrere Werte in einem case angeben, wodurch sich eine kompaktere Schreibweise erzielen lässt.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Anpassungen für Build-Tools und IDEs

Zur Demonstration der Auswirkungen von Java 12 auf Build-Tools und IDEs nutzen wir eine einfache Beispielapplikation mit folgenden Verzeichnisaufbau:

Java12Examples
|-- build.gradle
|-- pom.xml
‘-- src
  ‘-- main
    ‘-- java
      ‘-- java12
        ‘-- SwitchExample.java
 

Die Methoden der Klasse SwitchExample haben wir bereits in den vorherigen Abschnitten kennengelernt. Dort finden wir einige Varianten der neuen Syntax der switch-Anweisung.

Java 12 mit Gradle: Für Java 12, insbesondere zum Ausprobieren der neuen Syntax bei switch, benötigt man ein aktuelles Gradle 5.1 (oder neuer) und ein paar passende Angaben in der Datei build.gradle (Listing 6).

Listing 6

apply plugin: ’java’
apply plugin: ’eclipse’

sourceCompatibility=12
targetCompatibility=12

repositories
{
  jcenter();
}

// Aktivierung von switch preview
tasks.withType(JavaCompile) {
  options.compilerArgs += ["--enable-preview"]
}

Danach können wir wie gewohnt mit gradle clean assemble ein korrespondierendes JAR bauen und die Klasse SwitchExample wie folgt daraus starten: java –enable-preview -cp build/libs/Java12Examples.jar java12.SwitchExample. In beiden Fällen ist wichtig, den Kommandozeilenparameter –enable-preview anzugeben, denn nur dadurch lässt sich das Programm auch starten.
Java 12 mit Maven: Für Java 12 benötigt man ein aktuelles Maven 3.6.0 und das Maven Compiler Plugin in der Version 3.8.0. Dann kann man die in Listing 7 dargestellten Angaben in der pom.xml vornehmen:

Listing 7

<plugins>
  <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
      <source>12</source>
      <target>12</target>

      <!- - Wichtig für Java 12 Syntax-Neuerungen ->
      <compilerArgs>--enable-preview</compilerArgs>
    </configuration>	
  </plugin>
</plugins>

Mit diesen Modifikationen lässt sich ein Maven-Build mit mvn clean package ausführen. Nun wollen wir die neue switch-Funktionalität in Aktion erleben und geben dazu Folgendes ein:

java --enable-preview -cp target/SimpleJdk12Application-1.0.0-SNAPSHOT.jar
java12.SwitchExample

Dadurch startet die Applikation erwartungskonform.

Java 12 mit Eclipse: Eclipse bietet Stand Ende Februar 2019 noch keinen Support für Java 12. Es ist aber zu vermuten, dass dieser mit dem neuen Märzrelease von Eclipse bereitstehen wird.

Java 12 mit IntelliJ: Um mit Java 12 zu experimentieren, nutzen Sie bitte das aktuelle IntelliJ 2018.3. Allerdings sind noch ein paar Einstellungen vorzunehmen:

 

  • Im Project Structure-Dialog muss man im Bereich Project SDK den Wert 12 auswählen und im Bereich Project language level den Wert 12 – No new language features einstellen.
  • Zusätzlich ist im Preferences-Dialog im Feld Additional command line parameters der Wert –enable-preview anzugeben. Danach kompiliert der Sourcecode, allerdings moniert der Editor derzeit noch verschiedene Dinge fälschlicherweise als „Unexpected Token“. Das kann man aber einfach ignorieren.
  • Zum Ausführen des Beispiels ist es erforderlich, in der Run Configuration auch noch –enable-preview einzustellen.

Stay tuned

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

 

Fazit

Die Freude über die Neuerungen in Java 12 ist getrübt, weil es die Raw String Literals nicht ins Release geschafft haben. Dafür sind die Verbesserungen im Bereich switch-case sehr angenehm, aber fast schon überfällig. Leider sind auch diese lediglich als Preview integriert und müssen über –enable-preview freigeschaltet werden.

The post Java 12 Tutorial: So funktionieren die neuen Switch Expressions appeared first on JAX.

]]>
Services im Zwiegespräch: Synchrone Kommunikation zwischen REST Services mithilfe von OpenFeign https://jax.de/blog/software-architecture-design/services-im-zwiegespraech-synchrone-kommunikation-zwischen-rest-services-mithilfe-von-openfeign/ Fri, 18 Jan 2019 17:03:00 +0000 https://jax.de/?p=66668 Wer sich heutige Softwareprojekte oder -architekturen anschaut, steht immer wieder vor ähnlichen Herausforderungen. Eine davon ist die Kommunikation zwischen Services. Asynchron oder synchron, das ist hier die Frage.

The post Services im Zwiegespräch: Synchrone Kommunikation zwischen REST Services mithilfe von OpenFeign appeared first on JAX.

]]>

von Jörn Hameister
Ob es Microservices sein müssen oder ob es sich um Anwendungen in einer Systemlandschaft handelt, spielt keine Rolle. Wenn zwei Services miteinander kommunizieren sollen, besteht die Möglichkeit, dass die Kommunikation asynchron (etwa über Messages mit Kafka oder JMS) oder synchron (beispielsweise über REST) abläuft. Auf lange Sicht bietet die asychrone Kommunikation eine Reihe von Vorteilen, wie lose Kopplung von Services und Resilience. Allerdings wird trotzdem häufig die synchrone Kommunikation bevorzugt, weil sie leichter zu verstehen und zu implementieren ist. Zusätzlich fallen auch die Fehlersuche und das Debugging leichter. Deshalb stelle ich hier ein Framework vor, mit dem sich relativ einfach, elegant und übersichtlich die synchrone Kommunikation zwischen REST Services realisieren lässt: OpenFeign [1].

 

Erst nach dem Problem fragen

Wer kennt es nicht: In einer Systemlandschaft existiert ein Service mit einer REST-Schnittstelle, der Daten bereitstellt, die in einem anderen Service benötigt werden. Dazu gehören zum Beispiel Rechnungen, Kundendaten oder Wetterinformationen. Als Erstes stellt sich dann die Frage, mit welcher Technologie und welchem Framework der Service angebunden werden kann und welches Datenformat benutzt werden soll oder muss. JSON, XML, binär oder ein proprietäres Format?

In unserem Artikel gehen wir davon aus, dass JSON als Format zum Einsatz kommt. OpenFeign unterstützt auch alle anderen Formate und bietet die Möglichkeit, eigene proprietäre Formate zu ergänzen, sodass sie verarbeitet werden können. Wenn ein Service mit REST-Schnittstelle angesprochen werden soll, versucht man zuerst oft, den Service mit einem einfachen HTTP Call anzusprechen und die benötigten Daten abzufragen. Wenn von der Schnittstelle nur ein Integer oder String als Wert zurückkommt, ist das eventuell sogar ausreichend. Allerdings ist es oft so, dass komplexe Objekte (Entities, DTOs, …) an der Schnittstelle als JSON zurückgeliefert werden.

Für diesen Fall können wir beispielsweise den Jackson Mapper [2] ergänzen, um die Serialisierung und Deserialisierung der Objekte zu realisieren. Die Alternative ist das REST-Template von Spring. Es bietet sich an, wenn man sowieso im Spring-Boot-Umfeld unterwegs ist. Wer das REST-Template schon einmal eingesetzt hat, weiß, dass man jedes Mal überlegt, welche API-Methode (exchange, getForEntity usw.) man verwenden soll und wie die Parameter gesetzt werden müssen, um den gewünschten Wert abzufragen. Am Ende landet man meistens bei exchange, schaut wieder in die Dokumentation und sucht Codebeispiele, wie die Syntax genau aussieht.

Aus meiner Sicht ist der Java-Code mit seiner Fehlerbehandlung und dem Exception Handling immer wieder recht aufgebläht. Viel praktischer wäre es doch, wenn man einfach nur eine Clientschnittstelle beschreiben würde. Sie gibt an, wie die Service-Schnittstelle angesprochen werden soll. Das bedeutet, wir müssen uns nicht um die Fehlerbehandlung und die technischen Details kümmern. OpenFeign, ehemals Netflix Feign, ermöglicht beides. Schauen wir uns im ersten Schritt an einem kleinen Beispiel an, wie das funktioniert. Später wird an einem komplexeren API demonstriert, welche Möglichkeiten es gibt, um OpenFeign so zu erweitern, dass auch SPDY [3] verarbeitet werden kann.

ItemService

Anhand eines ItemStores, der Items (Dinge) verwaltet und über eine einfache REST-Schnittstelle angesprochen werden kann, erkennen wir, wie OpenFeign generell benutzt wird und funktioniert. Anfangs werfen wir einen kurzen Blick darauf, wie der Zugriff auf die Schnittstelle mit dem REST-Template oder einer http-Verbindung aussehen kann, um klar zu machen, welche Vorteile OpenFeign bietet. Die REST-Schnittstelle des ItemStore findet sich in Listing 1.

Listing 1: „ItemStore“ REST-Interface

@GetMapping(value = "/item")
ResponseEntity<List<Item>> getAllItems()

@PostMapping(value = "/item")
ResponseEntity<Item> createItem(@RequestBody Item item)

@PutMapping(value = "/item")
ResponseEntity<Item> updateItem(@RequestBody Item item)

@DeleteMapping(value = "/item/{id}")
ResponseEntity<Item> deleteItem(@PathVariable("id") long id)

@GetMapping(value = "/item/{location}")
ResponseEntity<List<Item>> getItemAtLocation(@PathVariable("location") String
location)

 

Es ist eine recht überschaubare Schnittstelle mit einer Methode zum Anlegen (createItem), Ändern (updateItem), Löschen (deleteItem) und Suchen (getItemAtLocation) von Items.

 

REST-Template

Wenn man mit der Klasse RestTemplate auf den Service zugreifen möchte, gestaltet sich das so:

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Item[]> responseEntity =
restTemplate.getForEntity("http://localhost:8080/item", Item[].class);
List<Item> listWithItems = Arrays.asList(responseEntity.getBody());

Hier verwenden wir die Methode getForEntity, um alle Items abzufragen. Eine weitere Variante, um lesend mit GET auf den Service zuzugreifen, kann so aussehen:


ResponseEntity<List<Item>> rateResponse =
  restTemplate.exchange("http://localhost:8080/item",
  HttpMethod.GET, null, new ParameterizedTypeReference<List<Item>>() {
  });
  List<Item> itemList = rateResponse.getBody();

Hier wird die exchange-Methode benutzt, um alle Items abzufragen und das Ergebnis in einer Liste zu erhalten. Sobald wir allerdings nicht nur lesend auf die Schnittstelle zugreifen möchten, sondern auch PUT und POST benutzen, müssen wir die Funktion exchange verwenden.

RestTemplate restTemplate = new RestTemplate();
HttpEntity<Item> request = new HttpEntity<>(item);
ResponseEntity<Item> response = restTemplate.exchange("http://localhost:8080/item",
HttpMethod.POST, request, Item.class);

In diesem Beispiel legen wir ein neues Item über das REST API an. Das bedeutet, die Methode createItem wird aus dem Interface in Listing 1 aufgerufen und die Response enthält das neu angelegte Item.

HttpConnection

Natürlich kann man den lesenden Zugriff auch mit einer einfachen HttpConnection und mit dem Jackson ObjectMapper lösen. Allerdings wird schon bei GET deutlich, dass extrem viel Boilerplate-Code entsteht und eine aufwendige Fehlerbehandlung dazukommt (Listing 2).

Listing 2: „HttpConnection“ für GET

private static List<Item> httpClientGet() throws IOException {
  URL url = new URL("http://localhost:8080/item");
  HttpURLConnection con = (HttpURLConnection) url.openConnection();
  con.setRequestMethod("GET");

  BufferedReader in = new BufferedReader(
  new InputStreamReader(con.getInputStream()));
  String inputLine;
  StringBuffer content = new StringBuffer();
  while ((inputLine = in.readLine()) != null) {
  content.append(inputLine);
  }
  in.close();

    ObjectMapper objectMapper = new ObjectMapper();
    return objectMapper.readValue(content.toString(), new
TypeReference<List<Item>>() { });
}

Außerdem ist bei diesem Ansatz auch die Gefahr größer, dass Fehler passieren. Beispielsweise weil vergessen wird, die Streams und Connections zu schließen. Eine andere Fehlerquelle liegt darin, dass wie im Beispiel kein Timeout von etwa fünf Sekunden mit con.setReadTimeout(5000); gesetzt wurde. Es ist eindeutig, dass das keine gute Lösung ist, die man für ein umfangreiches API implementieren und testen möchte.

Mit OpenFeign

Nachdem wir uns angeschaut haben, wie die REST-Schnittstelle mit dem REST-Template und mit HttpConnection angesprochen werden kann, kommen wir dazu, wie sich das mit OpenFeign lösen lässt. Um die Service-Schnittstelle aus Listing 1 mit OpenFeign anzusprechen, legen wir schlicht ein Interface an (Listing 3).

Listing 3: OpenFeign-Interface


package org.hameister.itemmanager;

import feign.Headers;
import feign.Param;
import feign.RequestLine;

import java.util.List;

public interface ItemStoreClient {

  @RequestLine("GET /item/")
  List<Item> getItems();

  @RequestLine("POST /item/")
  @Headers("Content-Type: application/json")
  Item createItem(Item item);

  @RequestLine("PUT /item/")
  @Headers("Content-Type: application/json")
  Item updateItem(Item item);

  @RequestLine("DELETE /item/{id}")
  void deleteItem(@Param("id") String id);

  @RequestLine("GET /item/{location}")
  List<Item> getItemAtLocation(@Param("location") String location);
}

Auf den ersten Blick ist deutlich, dass es nahezu identisch zum Service-Interface ist und keinerlei Boilerplate-Code enthält. Man beschreibt nur die Schnittstelle des Service mit dem Pfad der Operation und den Parametern und legt den Content-Type fest.

Mit der Annotation @RequestLine(“GET /item/”) geben wir die Operation und den Pfad an, der beschreibt, wo die Methode zu finden ist.

Um dieses Interface zu benutzen, lässt sich mit dem Feign.Builder einfach ein Client erzeugen und anschließend übers Interface auf die REST-Schnittstelle des ItemStore zugreifen (Listing 4).

Listing 4: OpenFeign-Client

package org.hameister.itemmanager;

import feign.Feign;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;

import java.time.LocalDate;
import java.util.List;

public class ItemManager {

  public static void main(String[] args) {

    ItemStoreClient api = Feign.builder()
      .encoder(new JacksonEncoder())
      .decoder(new JacksonDecoder())
      .target(ItemStoreClient.class, "http://localhost:8080");

    }
}

Dem Builder kommunizieren wir, welche Decoder und Encoder er verwenden soll, wo der Server mit der REST-Schnittstelle läuft und welches OpenFeign-Interface er verwenden soll. In unserem Beispiel nutzen wir den JacksonDecoder und JacksonEncoder. Diverse andere Standardencoder und -decoder zu verwenden, wäre ebenfalls möglich. Beispielsweise für Gson zum Serialisieren und Deserialisieren von Java-Objekten, JAXB zum Serialisieren und Deserialisieren von XML und SAX zum Serialisieren von XML.

Außerdem lässt sich im Builder das Logging konfigurieren, indem wir einen Logger ergänzen:


Feign.builder().logger(new Slf4jLogger())

Zusätzlich kann man einen Client definieren, der Dinge wie SPDY erledigt.


SwapiFeign api = Feign.builder()
 .client(new OkHttpClient())

Es ist auch möglich, Ribbon für das clientseitige Loadbalancing hinzuzufügen:


SwapiFeign api = Feign.builder().client(new RibbonClient())

Außerdem können wir eigene Encoder und Decoder implementieren und registrieren, sodass proprietäre Formate unterstützt werden können. Dazu muss nur das jeweilige Interface implementiert werden. Für den Decoder:


public class MyCustomDecoder implements Decoder {
  @Override
  public Object decode(Response response, Type type) throws IOException,
DecodeException, FeignException {
    return ...;
  }
}


Für den Encoder:


public class MyCustomEncoder implements Encoder {
  @Override
  public void encode(Object o, Type type, RequestTemplate requestTemplate) throws
EncodeException {
  ...
    }
}


Diese Encoder und Decoder müssen anschließend wie die Standardencoder und -decoder

registriert werden, wenn der Client mit dem Builder erstellt wird. Nicht zu vergessen, dass man bei der Definition des Clientinterface auch direkt Hystrix integrieren kann. Dafür gibt es einen HystrixFeign Builder, der genauso benutzt wird wie der Standard-Builder.


ItemStoreClient api = HystrixFeign.builder()
  .target(ItemStoreClient.class, "http://localhost:8080");


Er ermöglicht uns, die Schnittstelle um ein fehlertolerantes Verhalten zu erweitern. Beispielsweise, wenn der Service nicht oder nur langsam antwortet. Gerade in einem Umfeld, in dem mehrere Services miteinander kommunizieren, lässt sich dadurch verhindern, dass der Ausfall eines Service das Gesamtsystem zum Stehen bringt. Das war es auch schon. Anschließend dient der erstellte ItemStoreClient dazu, über die REST-Schnittstelle auf den ItemStore zuzugreifen.

In Listing 4 zeigt sich, wie der ItemStoreClient erstellt wird. Anschließend können die Methoden übers Interface direkt aufgerufen werden: List<Item> items = api.getItems();

Das Anlegen von Items funktioniert so:


Item item = new Item();
item.setDescription("New Item");
item.setLocation("Schrank 5A");
item.setItemdate(LocalDate.now());
Item newItem = api.createItem(item);


Ein Item zu ändern lässt sich analog durchführen:


newItem.setLocation("Schrank 5B");
Item updateItem = api.updateItem(newItem);


Um ein Item zu löschen, muss die jeweilige ID übergeben werden: api.deleteItem(“1”);.Das ist im Vergleich zum REST-Template oder dem HttpClient erheblich eleganter und verständlicher. Anzumerken ist, dass bei allen Ansätzen auf Clientseite ein DTO für das Item vorhanden sein muss. Entweder wir kopieren die Klasse aus dem Item-Service oder legen eine neue Klasse an (Listing 5).

Listing 5:  Item-DTO

@Data
public class Item {

  Long id;

  private String description;
  private String location;
  private LocalDate itemdate;

  public Item() {
  }
}

 

In dem DTO-Item haben wir Lombok [4] verwendet, um die Getter und Setter automatisch generieren zu lassen.

SWAPI

Wir haben uns angeschaut, wie OpenFeign generell bei einer einfachen Schnittstelle verwendet werden kann und welche Vorteile es gegenüber anderen Ansätzen mitbringt. Jetzt wenden wir uns dem zu, was OpenFeign noch bietet. Das soll am Beispiel von SWAPI (The Star Wars API) gezeigt werden. Dabei handelt es sich um eine öffentliche REST-Schnittstelle, über die man Personen, Filme, Raumschiffe und Planeten aus dem Star-Wars-Universum abfragen kann. Die Schnittstelle ist unter dem URL https://swapi.co zu erreichen. Wie auch schon bei dem Beispiel oben legen wir als Erstes ein Interface für die Schnittstelle an (Listing 6).

Listing 6:  SWAPI-Interface

public interface SwapiFeign {
  @RequestLine("GET /planets/{id}")
  Planet getPlanet(@Param("id") String id);

  @RequestLine("GET /planets/")
  GenericList<Planet> getPlanets();

  @RequestLine("GET /films/")
  GenericList<Film> getFilms();

  @RequestLine("GET /people/")
  GenericList<People> getPeople();

  @RequestLine("GET /starships/")
  GenericList<Starship> getStarships();

  @RequestLine("GET /vehicles/")
  GenericList<Vehicle> getVehicles();

}

Wir müssen für die Rückgabewerte noch DTOs anlegen. Im Vergleich zum anderen Beispiel benötigt man außerdem noch eine Generic List, weil das API die Rückgabewerte untereinander verlinkt. Das heißt, die Rückgabewerte enthalten immer einen Link auf das vorhergehende und nächste Element (Listing 7).

Listing 7:  „GenericList“

public class GenericList<T> {
  public int count;
  public String next;
  public String previous;

  public List<T> results;
}

Anschließend lässt sich wieder ein Client erstellen. Er ermöglicht, die Daten über die REST-Schnittstelle abzufragen. Auch im folgenden Beispiel werden ein JacksonEncoder und ein JacksonDecoder verwendet, um die JSON-Daten von der REST-Schnittstelle zu serialisieren und zu deserialisieren.

SwapiFeign api = Feign.builder()
  .encoder(new JacksonEncoder())
  .decoder(new JacksonDecoder())
  .client(new OkHttpClient())
  .target(SwapiFeign.class, "http://swapi.co/api");

Beim Erstellen des Clients fällt auf, dass der OkHttpClient() gesetzt wird. Das ist notwendig, damit SPDY, HTTP/2 und TLS des REST-Interface bedient werden können. Der komplette Quellcode zum OkHttpClient steht im GitHub Repository zu dem Artikel zur Verfügung [5].

Mit dem api-Objekt lässt sich nun die Schnittstelle ansprechen:

GenericList<Starship> starships = api.getStarships();

Listing 8 zeigt exemplarisch das DTO für das Starship.

Listing 8:  „Starship“

@Data
public class Starship {

  private String name;
  private String model;
  private String manufacturer;
  private String costs_in_credits;
  private String length;
  private String max_atmosphering_speed;
  private String crew;

  private String cargo_capacity;
  private String consumables;
  private String hyperdrive_rating;
  private String MGLT;
  private String starship_class;

  private List<People> pilots;
  private List<Film> films;
  private String created;
  private String edited;
  private String url;

  public Starship() {

  }
}

Um das Schema, also die Felder eines Starships herauszufinden, kann man einfach das API befragen, das unter [6] zu erreichen ist. Auch dies ist eine REST-Schnittstelle, die sich abfragen lässt:

Schema schema = getSchema("https://swapi.co/api/starships/schema");

Wobei das Schema-DTO aussieht wie in Listing 9

Listing 9:  Schema-DTO

@JsonIgnoreProperties(ignoreUnknown = true)
public class Schema {
  public List<String>required;
  public Map<String, Properties> properties;
  public String type;
  public String title;
  public String description;

  public Schema() {
  }
}

Und das verwendete Properties DTO so:

public class Properties {
  public String type;
  public String format;
  public String description;
}

Die getSchema()-Methode mit dem OkHttpClient zum Abfragen des Schemas findet sich in Listing 10.

Listing 10:  „getSchema“-Methode

private static Schema getSchema(String url) throws IOException {
  okhttp3.OkHttpClient okHttpClient = new okhttp3.OkHttpClient();

  Request request = new Request.Builder()
  .url(url)
  .get()
  .build();
  Response response = okHttpClient.newCall(request).execute();
  ObjectMapper objectMapper = new ObjectMapper();
  Schema schema =
objectMapper.readerFor(Schema.class).readValue(response.body().string());
  return schema;
}

 

Hier haben wir bewusst darauf verzichtet, OpenFeign einzusetzen, um zum Abschluss noch einmal zu verdeutlichen, dass der Quellcode ohne OpenFeign länger ist als mit OpenFeign. Zu beachten ist, dass das Exception Handling hier weitgehend ignoriert wurde, indem die IOExceptions einfach an den Aufrufenden zurückgeworfen und nicht behandelt werden.

Um noch einmal zu unterstreichen, wie einfach die Abfrage mit OpenFeign funktioniert, definieren wir zuerst ein Interface:

public interface SwapiSchemaClient {

  @RequestLine("GET ")
  Schema getSchema();
}

Anschließend kann ein Feign-Client mit dem Builder erstellt und daraufhin das Schema

abgefragt werden (Listing 11).

Listing 11:  „SchemaClient“

SwapiSchemaClient api = Feign.builder()
  .encoder(new JacksonEncoder())
  .decoder(new JacksonDecoder())
  .client(new OkHttpClient())
  .target(SwapiSchemaClient.class, "https://swapi.co/api/starships/schema");

Schema schema = api.getSchema();

 

Weniger selbst implementieren

OpenFeign ist eine elegante Möglichkeit, REST-Schnittstellen anzusprechen. Der Anwender bekommt eine Menge Features quasi geschenkt, die er normalerweise selbst implementieren müsste. Allerdings ist es nur eine von vielen Möglichkeiten. Wie so oft bei der Softwareentwicklung muss man immer genau schauen, in welchem Kontext man sich bewegt, welche Rahmenbedingungen es gibt und was dann die beste Lösung in dem Projekt ist. Einen kurzen Einführungsvortrag zu OpenFeign hat Igor Laborie bei der Devoxx 2016 in Belgien gehalten [7].

Anmerken sollte man vielleicht noch, dass ab Java 11 ein HttpClient fester Bestandteil von Java (JEP 321) ist, der sowohl synchrone, als auch asynchrone Requests absetzen kann [8].

 

Cheat-Sheet: Die neuen JEPs im JDK 12


Unser Cheat-Sheet definiert für Sie, wie die neuen Features in Java 12 funktionieren. Von JEP 189 „Shenandoah“ bis JEP 346 „Promptly Return Unused Committed Memory from G1“ fassen wir für Sie zusammen, was sich genau ändern wird!

Cheat-Sheet sichern!

Links & Literatur
[1] OpenFeign: https://github.com/OpenFeign/feign
[2] Jackson Mapper: https://github.com/FasterXML/jackson
[3] SPDY: https://de.wikipedia.org/wiki/SPDY
[4] Project Lombok: https://projectlombok.org
[5] https://github.com/hameister/ItemStoreFeignClient
[6] SWAPI: https://swapi.co/api/starships/schema
[7] OpenFeign in Action: https://youtu.be/kO3Zqk_6HV4
[8] Java 11 HttpClient, Gson, Gradle, and Modularization: https://kousenit.org/2018/09/22/java-11-httpclient-gson-gradle-and-modularization/

The post Services im Zwiegespräch: Synchrone Kommunikation zwischen REST Services mithilfe von OpenFeign appeared first on JAX.

]]>