Umsetzung von REST-Services

1. Technologieauswahl

Die IsyFact legt folgende Frameworks für die Umsetzung von REST-Services fest.

Tabelle 1. Frameworks für die Umsetzung
Framework Technologie-Stack Beschreibung

Spring WebFlux

Java/Spring

Umsetzung von REST-Clients

(unter Nutzung von Apache HTTPComponents)

Spring MVC

Java/Spring

Umsetzung von REST-Services

(zur Nutzung mit Tomcat)

Spring RestClient

Java/Spring

Umsetzung von REST-Clients

Angular Framework Modul: @angular/common/http

TypeScript/Angular

Umsetzung von REST-Clients

OpenAPI

technologie-unabhängig

Dokumentation der Service-Schnittstelle und Erzeugung von Service und Client

2. Spring MVC im Vergleich zu Spring WebFlux

Das Spring Framework stellt seit Version 5 Spring Webflux als reaktive Alternative zu Spring MVC bereit.

Mit WebFlux ist es möglich, reaktive REST APIs zu implementieren. Beim reaktiven Programmierparadigma geht es um die Reaktion auf Veränderungen (Observer Pattern), im Fall von REST APIs um die Reaktion auf gesendete Daten. Das ist hilfreich, wenn man große Datenmengen (z. B. Mediendateien oder größere Ergebnismengen aus einer Datenbankabfrage) über das REST API übertragen möchte. Die Daten werden dann in mehreren Paketen gesendet, der Empfänger reagiert jeweils auf den Erhalt eines Pakets.

Der Hauptvorteil von WebFlux liegt aber im geringeren Ressourcenverbrauch auf der Server Seite. WebFlux basiert auf einem Event-Loop Mechanismus, während Spring MVC auf Thread Pools basiert. Der Performance Vorteil von WebFlux macht sich insbesondere bei vielen gleichzeiten Service-Aufrufen und/oder der Übertragung größerer Datenmengen bei einem Service-Aufruf bemerkbar.

Trotzdem empfiehlt die IsyFact grundsätzlich, Spring MVC für die Umsetzung von REST-Services einzusetzen. Die Vorteile des reaktiven Programmierparadigmas erfüllen sich nur, wenn alle Teile der Umsetzung entsprechend gestaltet sind. Dies ist aktuell nicht der Fall.

Für REST-Clients empfiehlt die IsyFact die Verwendung des synchronen RestClient, um die Kompatibilität zu bestehenden Bausteinen der IsyFact aufrecht zu halten. Bei der Verwendung des asynchronen WebClient sind die Einschränkungen reaktiver Verarbeitung mit IsyFact-Bausteinen zu beachten.

Für die Anbindung von Angular-Clients enthält der Baustein Angular eine entsprechende Komponente.

3. Umsetzung von REST-Clients

Die IsyFact empfiehlt für REST-Clients den RestClient aus dem Spring Framework ab Spring Framework 6.1 (Spring Boot 3.2).

Tabelle 2. Abgrenzung der Spring-Clients für REST
Client Beschreibung Einordnung durch die IsyFact

RestClient

blockierend, fluide API

empfohlen ab Spring Framework 6.1

RestTemplate

blockierend, Template-Method-API

nicht empfohlen; seit Spring Framework 7.0 als @Deprecated gekennzeichnet, Entfernung in einer künftigen Version angekündigt. Funktionsumfang bleibt bis zur Entfernung erhalten, Weiterentwicklung auf Fehler- und Sicherheitskorrekturen beschränkt.

WebClient

nicht-blockierend, reaktiv, fluide API

nicht empfohlen; bei durchgängig reaktiver Fachlogik nur in Ausnahmefällen einsetzbar, sofern die Einschränkungen im Fachverfahren abgefangen und in der Architekturentscheidung dokumentiert sind.

Der RestClient ist der Nachfolger des RestTemplate, welcher nicht mehr aktiv weiter entwickelt wird, und ersetzt diesen weitgehend. Beide bauen auf denselben Abstraktionen des Spring Framework auf:

  • ClientHttpRequestFactory/SimpleClientHttpRequestFactory als Anbindung an die HTTP-Client-Bibliothek

  • HttpMessageConverter für Serialisierung und Deserialisierung

  • ClientHttpRequestInterceptor für die Querschnittserweiterung von Anfragen

  • UriBuilderFactory für die URI-Erzeugung

  • RestResponseErrorHandler für die Fehlerbehandlung

Bestehende Erweiterungen - insbesondere die in den Bausteinen isy-logging, isy-security und isy-task genutzten Interceptor- und Fehlerbehandlungsmechanismen - können daher nach Umstellung auf den RestClient übernommen werden.

Der RestClient folgt dem imperativen Programmiermodell der IsyFact: Die Anfrage wird synchron auf dem aufrufenden Thread ausgeführt und beendet. Die für den WebClient beschriebenen Einschränkungen durch Thread-Wechsel treten also nicht auf.

Der RestClient ist kein Quellcode-kompatibler Austausch des RestTemplate. Der Aufruf erfolgt über eine Fluent API, z.B:

Listing 1. Code-Beispiel RestClient
RestClient restClient = RestClient.builder()
        .baseUrl("https://beispiel-basis-url.de")
        .build();
DatenDto daten = restClient.get()
        .uri("/daten/{id}", 12)
        .accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .body(DatenDto.class);

Seit Spring Framework 7.0 ist RestTemplate als deprecated markiert und wird aus künftigen Versionen entfernt. Zu einer Migration gemäß der Anleitung Migrating to RestClient wird daher geraten.

Die Spring-Dokumentation empfiehlt bei der Migration eine zweistufige Vorgehensweise.

  1. Das komponentenweise Ersetzen von RestTemplate- durch RestClient-Instanzen:

    RestClient restClient = RestClient.create(restTemplate);
  2. Replikation der RestTemplate-Instanziierung unter Verwendung von äquivalenten Methoden, die der RestClient.Builder bereitstellt (siehe Code-Beispiel RestClient).

Den WebClient blockierend zu nutzen, ist kein moderner Ersatz für RestTemplate. Abgesehen von der steigenden Code-Komplexität geht der Thread-Kontext (MDC, SecurityContext) beim Threadwechsel ohne zusätzliche Übertragungsmechanismen zur Propagation verloren.

Listing 2. WebClient blockierend
DatenDto daten = webClient.get()
        .retrieve()
        .bodyToMono(DatenDto.class)
        .block();// Thread-Kontext geht verloren!

4. Einschränkungen reaktiver Verarbeitung mit IsyFact-Bausteinen

Die folgenden Einschränkungen gelten für die reaktive Verarbeitung mit Project Reactor, insbesondere beim Einsatz des WebClient. Bei Nutzung des RestClient treten sie nicht auf.

Der WebClient basiert auf Project Reactor und verarbeitet HTTP-Anfragen in einem reaktiven, nicht-blockierenden Ausführungsmodell. Innerhalb einer reaktiven Pipeline kann Project Reactor zwischen Threads wechseln: Ein Request wird auf einem Thread begonnen und nach einer asynchronen Operation auf einem anderen Thread fortgesetzt.

Die IsyFact setzt grundsätzlich auf dem imperativen Programmiermodell auf und nutzt ThreadLocal-Mechanismen zur Propagierung von Kontext-Informationen. Diese Mechanismen funktionieren im reaktiven Modell nicht zuverlässig, da ThreadLocal-Werte beim Threadwechsel auf dem neuen Thread nicht zur Verfügung stehen.

Reaktive Verarbeitung wird durch die IsyFact aktuell nicht vollständig unterstützt. Fachverfahren, die den WebClient einsetzen und gleichzeitig isy-logging, isy-security oder isy-task nutzen, sind von den im Folgenden beschriebenen Einschränkungen betroffen.

4.1. Einschränkungen bei isy-logging

Der Baustein isy-logging basiert auf dem ThreadLocal-basierten Mapped Diagnostic Context (MDC) von Logback. Der MDC speichert Kontextinformationen wie die Korrelations-ID und die System-ID individuell pro Thread.

Beim Einsatz des WebClient wechselt Project Reactor bei asynchronen Operationen den ausführenden Thread. Die MDC-Map des neuen Threads ist leer oder enthält Werte eines anderen Requests, der zufällig auf demselben Thread verarbeitet wird.

Dies hat Konsequenzen:

  • Die Korrelations-ID und alle weiteren MDC-Felder gehen bei Thread-Wechseln verloren oder enthalten falsche Werte.

  • Dieser Datenverlust tritt ohne Fehler oder Exception auf und ist im Logging nicht erkennbar.

  • Die Logging-Nachvollziehbarkeit über Komponentengrenzen hinweg ist also nicht gewährleistet.

Darüber hinaus basiert der LogInterceptor des Bausteins isy-logging auf Spring AOP. Spring-AOP-Aspekte sind an den aufrufenden Thread gebunden und können reaktive Pipelines daher nur eingeschränkt instrumentieren.

4.2. Einschränkungen bei isy-security

Der Baustein isy-security nutzt den SecurityContextHolder von Spring Security. Dieser speichert den SecurityContext in einem ThreadLocal. In diesem sind Authentifizierungsinformationen, zugewiesene Rollen und Behördenkennzeichen enthalten.

Bei reaktiver Verarbeitung ist der SecurityContext nach einem Thread-Wechsel auf dem neuen Thread nicht mehr verfügbar. Dies hat folgende Auswirkungen:

  • Wird eine durch isy-security abgesicherte Methode nach einem Thread-Wechsel aufgerufen, schlägt die Autorisierung mit einer OAuth2AuthenticationException fehl.

  • Werden keine abgesicherten Methoden aufgerufen, verhalten sich diese wie für einen anonymen Aufrufer. Das Fehlen des SecurityContext fällt in diesem Fall nicht auf.

Die Propagierung des SecurityContext in reaktiven Pipelines über den ReactiveSecurityContextHolder von Spring Security ist aktuell nicht Bestandteil der IsyFact.

4.3. Einschränkungen bei isy-task

Der Baustein isy-task integriert isy-logging und isy-security in das Spring Scheduling. Wird innerhalb von Tasks reaktiver Code ausgeführt z.B. durch Nutzung des WebClient – werden die oben beschriebenen Einschränkungen von isy-logging und isy-security geerbt.

Zusätzlich ist die Task-Überwachung von isy-task für den imperativen Ausführungsfall konzipiert. Im reaktiven Modell endet die gemessene Ausführungsdauer eines Tasks bereits beim Aufruf von subscribe() und nicht erst nach Abschluss der reaktiven Pipeline. Die tatsächliche Laufzeit eines reaktiven Tasks wird damit nicht korrekt gemessen.

4.4. Architekturelle Einordnung

Die fehlende Unterstützung reaktiver Verarbeitung durch die IsyFact stellt eine bekannte architekturelle Lücke dar. Wird der WebClient eingesetzt, können die beschriebenen Probleme nicht eigenständig behoben werden: Eigene AOP-Aspekte sind gemäß den Vorgaben der IsyFact nicht zulässig, und aktuell werden keine reaktiven Alternativen bereitgestellt.

Technische Lösungsansätze existieren grundsätzlich:

  • Project Reactor unterstützt in Kombination mit Micrometer die automatische Propagierung von Kontext-Informationen in reaktiven Pipelines (Hooks.enableAutomaticContextPropagation()). Darüber ließe sich die MDC-Propagierung von isy-logging reaktiv-kompatibel gestalten.

  • Spring Security stellt mit dem ReactiveSecurityContextHolder eine reaktive Alternative zum SecurityContextHolder bereit, die den SecurityContext im Reactor-Kontext verwaltet.

  • Der AOP-Aspekt der Task-Überwachung in isy-task müsste für reaktive Rückgabetypen angepasst werden, sodass der Kontext erst nach Abschluss der reaktiven Pipeline bereinigt wird.

Diese Lösungsansätze sind aktuell nicht Bestandteil der IsyFact.

5. Schnittstellendokumentation

Alle auf IsyFact basierten Anwendungen müssen ihre REST-Services mit der OpenAPI 3.0 Spezifikation beschreiben. Sowohl YAML als auch JSON sind als Format der Schnittstellendokumentation zulässig. Die IsyFact empfiehlt zur Erstellung der Schnittstellendokumentation den OpenAPI (Swagger) Editor.

5.1. Code-Generierung

Zur Erstellung von REST-APIs gibt es grundsätzlich 2 Ansätze: Contract/API First oder Code First. Bei Contract/API First wird zunächst die Schnittstellenbeschreibung erstellt und daraus der Code (Server und Client) generiert. Bei Code First wird zuerst die API implementiert und mit Annotationen für die Schnittstellenbeschreibung versehen. Aus den Annotationen wird dann die Schnittstellenbeschreibung generiert.

Die IsyFact empfiehlt den Contract/API First Ansatz und sieht hierzu die Erstellung einer OpenAPI-Spezifikation vor.

Für die Generierung von Code empfiehlt die IsyFact den OpenAPI Generator.

5.2. Bereitstellen von generierten Clients

Neben dem Server können aus einer OpenAPI-Spezifikation auch Clients für diverse Technologie-Stacks generiert werden. Aus Convenience-Gründen wird empfohlen, dass Anbieter von Services fertig generierte Clients für die gängigen Technologie-Stacks zur Verfügung stellen: Java (Spring) und TypeScript (Angular).

6. Verwendung von Transportobjekten

Verwendung von Transferobjekten

REST-Services verwenden ausschließlich Transferobjekte (Data Transfer Objects, DTOs).

Die DTOs werden ebenfalls innerhalb der Schnittstellenbeschreibung schematisch beschrieben.

Der Code für die DTOs wird daraus generiert.

Innerhalb der Schnittstellenbeschreibung des Service werden die Schemata der DTOs üblicherweise als Referenz mittels #ref eingebunden. Die Referenz kann sich dabei auf eine Beschreibung innerhalb derselben Datei beziehen oder auch auf eine externe Datei. Die Datei kann dabei sowohl über einen Dateipfad als auch über eine URL adressiert werden.

Listing 3. Beispiel aus Swagger Petstore
requestBody:
   content:
      application/json:
         schema:
            $ref: '#/components/schemas/Pet'

# [...]

components:
   schemas:
      Pet:
         name:
            type: string
            example: doggie