Services im Zwiegespräch: Synchrone Kommunikation zwischen REST Services mithilfe von OpenFeign

Quelle: Shutterstock

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.

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/

Top Articles About Architecture & Design

Alle News der Java-Welt:

Behind the Tracks

Agile, People & Culture
Teamwork & Methoden

Clouds & Kubernetes
Alles rund um Cloud

Core Java & Languages
Ausblicke & Best Practices

Data & Machine Learning
Speicherung, Processing & mehr

DevOps & CI/CD
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Software-Architektur
Best Practices

Web & JavaScript
JS & Webtechnologien

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management

ALLE NEWS ZUR JAX!