Software-Architektur - JAX https://jax.de/tag/software-architektur/ Java, Architecture & Software Innovation Fri, 02 Sep 2022 10:04:53 +0000 de-DE hourly 1 https://wordpress.org/?v=6.4.2 Mit Quarkus gegen Monolithen https://jax.de/blog/mit-quarkus-gegen-monolithen/ Wed, 12 Jan 2022 12:35:44 +0000 https://jax.de/?p=85447 Firmen stehen häufig vor dem Problem, dass über lange Zeit gewachsene Softwareprojekte unwartbar und immer unverständlicher werden. Irgendwann stellt sich die Frage: Alles hinschmeißen und neu entwickeln? Wir möchten euch einen Einblick in unser Vorgehen, die Entscheidungsprozesse und unsere künftige Architektur geben. Insbesondere gehen wir auf das Set-up, das auf Quarkus basiert, ein und zeigen, dass sich damit nicht nur kleine Services bauen lassen.

The post Mit Quarkus gegen Monolithen appeared first on JAX.

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

Der Monolith

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

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

Stay tuned

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

 

Alles neu?

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

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

Den Monolithen erwürgen

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

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

Strangler Pattern

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

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

ALL ABOUT MICROSERVICES

Microservices-Track entdecken

 

Evolution statt Revolution

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

Entscheidungsprozess

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

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

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

Anti-corruption Layer

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

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

Abb. 1: Schrittweise Ablösung des Monolithen

Nieder mit dem Monolithen

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

  1. Wir verhindern einen neuen Big Ball of Mud

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

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

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

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

 

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

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

Abb. 2: Projektstruktur in der Übersicht

Frontend

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

Abb. 3: Frontend-Projektstruktur in IntelliJ

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

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

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

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

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

Backend

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

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

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

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

 

Runnable

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

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

mvn -pl runnable -am compile quarkus:dev

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

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

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

 

Ausblick

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

Stay tuned

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

 

Links & Literatur

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

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

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

[4] https://quarkus.io

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

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

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

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

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

The post Mit Quarkus gegen Monolithen appeared first on JAX.

]]>
Evolutionäre Software-Architektur https://jax.de/blog/evolutionaere-software-architektur/ Wed, 20 Nov 2019 11:01:31 +0000 https://jax.de/?p=73357 Die Rahmenbedingungen, in denen Software entsteht, sind einem steten Wandel unterworfen. Welche Antworten können Software-Architekten auf die immer volatiler werdenden Märkte geben?

The post Evolutionäre Software-Architektur appeared first on JAX.

]]>
In seiner Keynote von der W-JAX 2019 präsentiert Patrick Kua (N26) das Konzept der evolutionären Software-Architektur. Dabei handelt es sich um einen Architektur-Ansatz, in dem die Veränderbarkeit von Software von Anfang an mitgedacht wird.

Was macht evolutionäre Software-Architekturen aus? Wie hält man die Balance zwischen Stabilität und Flexibilität? Wie lässt sich das Prinzip des Wandels mit dem agilen Prinzip vereinbaren, Software möglichst schnell auszuliefern, die einen echten Mehrwert bietet?

Evolutionäre Prinzipien

Evolutionäre Architekturen tragen zunächst einmal der Erkenntnis Rechnung, dass Software in inkrementellen Schritten entsteht. Software-Entwicklung ist kein linearer Prozess, an dessen Anfang eine Architektur-Skizze steht, die dann stur implementiert wird. Evolutionäre Software-Architektur vollzieht sich vielmehr in Schleifen: Nach dem Release einer kleinen Einheit wird Feedback eingeholt, auf dessen Basis Entscheidungen für die Weiterentwicklung getroffen werden können.

Weiterentwicklung vollzieht sich stets auf mehreren Dimensionen. Auf technischer Ebene sind dies zum Beispiel neue Sprachen, Frameworks, Tools und Systemupdates. Auf fachlicher Ebene können Änderungen des Marktes, neue Einkommensmodelle, ein verändertes Konsumverhalten oder eine neue Wettbewerbssituation Anpassungen an die Software-Architektur verlangen.

Kuas Fazit: Software-Architektur beschäftigt sich nicht nur mit dem Problem der richtigen technischen Entscheidung. Software-Architekten sind zunehmend mit der Frage konfrontiert, wie gewisse Entscheidungen getroffen werden sollten. Es geht um die Entscheidungskultur, die Grundlage für eine evolutionäre Software-Architektur ist.

 

Quarkus-Spickzettel


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

 

Jetzt herunterladen!

The post Evolutionäre Software-Architektur appeared first on JAX.

]]>
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 -&gt; 6;
    case TUESDAY -&gt; 7;
    case THURSDAY, SATURDAY -&gt; 8;
    case WEDNESDAY -&gt; 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 -&gt; numericString = "one";
    case 2 -&gt; numericString = "two";
    case 3 -&gt; numericString = "three";
    default -&gt; 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 + " ==&gt; " + 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

&lt;plugins&gt;
  &lt;plugin&gt;
    &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
    &lt;version&gt;3.8.0&lt;/version&gt;
    &lt;configuration&gt;
      &lt;source&gt;12&lt;/source&gt;
      &lt;target&gt;12&lt;/target&gt;

      &lt;!- - Wichtig für Java 12 Syntax-Neuerungen -&gt;
      &lt;compilerArgs&gt;--enable-preview&lt;/compilerArgs&gt;
    &lt;/configuration&gt;	
  &lt;/plugin&gt;
&lt;/plugins&gt;

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.

]]>