Ring frei für die nächste Runde

MicroProfile 1.3 Deep Dive: OpenAPI, OpenTracing & Type-safe Rest Client

Michael Hofmann

© Shutterstock.com / Kalakruthi

Pünktlich zum Start ins neue Jahr wurde am 3. Januar ein neues Release von MicroProfile veröffentlicht. In nur drei Monaten hat die Community, neben kleineren Erweiterungen bei schon vorhandenen APIs (Metrics 1.1 und Config 1.2), auch noch drei neue APIs ins Leben gerufen: OpenTracing 1.0, OpenAPI 1.0 und Type-safe Rest Client 1.0.

Mit OpenTracing wird damit das Tracing von Requests in der verteilten Welt von Microservices ermöglicht. Zur Dokumentation von REST Endpoints setzt sich mehr und mehr OpenAPI v3 durch. Dieser Tatsache geschuldet wurde MicroProfile OpenAPI 1.0 in den Standard integriert. Und last but not least kann der Entwickler mit Type-safe Rest Client 1.0 seinen REST Client typsicher gestalten.

Für dieses Jahr sind zwei weitere Releases geplant. Mit MicroProfile 1.4 soll den Entwicklern stärker unter die Arme gegriffen werden, indem man Video-Tutorials und verbesserte Dokumentationen sowie Beispiele und somit notwendige Informationen für die tägliche Programmierarbeit zur Verfügung stellt. Damit soll die Integration der MicroProfile APIs in die Microservice-Implementierung erleichtert werden. Und das ist noch nicht alles für das geplante Release am 23. März. Zur gleichen Zeit ist MicroProfile 2.0 angekündigt, das alle vorhandenen MicroProfile-APIs fit für Java EE 8 machen wird. Damit hält die Community das Versprechen, weiterhin am Java EE (bzw. EE4J) Standard festzuhalten und keine neue Parallelwelt aufzubauen.

MicroProfile OpenTracing 1.0

Eine grundsätzliche Herausforderung bei verteilten Systemen – und somit auch bei Microservice-Applikationen – ist das Nachvollziehen von Aufrufketten. Da sich Requests teilweise über mehrere Microservice-Instanzen hinweg erstrecken können, ist es nicht ganz einfach zu erkennen, welche Requests in den jeweiligen Microservice-Instanzen zusammengehören. Um dies zu erreichen, ist es notwendig, den Request, der die Microservice-Applikation aufruft, mit einer Correlation ID zu versehen und diese dann bei jedem weiteren Aufruf an nachgelagerte Microservices mitzugeben. So markierte Requests werden dann an ein zentrales Tracing-System übermittelt und dort gespeichert. Darüber hinaus bietet das Tracing-System Möglichkeiten zur Auswertung von Request-Ketten an.

Mit OpenTracing hat sich ein Standard etabliert, der herstellerneutrale APIs definiert, um mit solchen Trace-Daten umzugehen. Damit werden die Anwendungen unabhängig vom eingesetzten Tracing-System. Es gibt bereits mehrere Tracing-Systeme, die an diesem Standard beteiligt sind, so bspw. Zipkin oder Jaeger. Auf der Seite opentracing.io sind noch weitere Systeme aufgeführt, die dem Standard folgen.

Mit MicroProfile OpenTracing wird ein auf JAX-RS basierender Microservice mit den Fähigkeiten des Tracings ausgestattet. Der Microservice kann also automatisch Correlation IDs für nachgelagerte Microservices erzeugen und weiterleiten. Die Übermittlung der Trace-Informationen an das jeweilige Tracing-System erfolgt mithilfe von OpenTracing. Zusätzlich wird dafür gesorgt, dass die Aufrufkette (Span) mit einheitlichen Informationen arbeitet (SpanContext). Sonst würden Microservices, die mit diesen Informationen nicht umgehen können, vom Span ausgeschlossen. Diese Services würden die Aufrufkette unterbrechen oder beenden. Es werden zwei verschiedene Arten der Integration von MicroProfile OpenTracing in den Microservice unterstützt. Entweder konfigurativ, d.h. ohne Anwendungscode schreiben zu müssen, oder programmatisch, indem der Tracer mit einer Annotation (@Traced) injiziert wird.

Für die Konfigurationsvariante schreibt MicroProfile OpenTracing vor, dass eine Implementierung von io.opentracing.Tracer für jede Applikation vom Applikationsserver zur Verfügung gestellt werden muss. Die Einstellungen des Tracers müssen von außen gesetzt werden können, d.h. über Konfigurationseinstellungen im Applikationsserver. Somit wird sichergestellt, dass ein Wechsel des Tracing-Systems keinen Einfluss auf den Microservice hat. Aus Sicht des Entwicklers eine Komfortfunktion, da er für dieses Resultat in seinem Microservice keine einzige Zeile Code schreiben muss.

Falls der Entwickler darüber hinaus noch den Bedarf hat, das Tracing selbst in die Hand zu nehmen, kann er das mithilfe der folgenden Annotation: org.eclipse.microprofile.opentracing.Traced. Diese Annotation kann auf Klassen- oder Methodenebene angewandt werden und hat zwei optionale Argumente. Mit dem Argument value=[true|false] kann das Tracing der Klasse bzw. Methode an- oder abgeschaltet werden. Mit dem Argument operationName kann man die Standard-Bezeichnung (<HTTP method>:<package name>.<class name>.<method name>) des Spans verändern. Falls man noch weiter in den Tracer eingreifen möchte, so kann man sich diesen auch injizieren lassen:

@Inject
io.opentracing.Tracer myTracer;

Hiermit bekommt man beispielsweise die Möglichkeit, weitere Spans innerhalb einer Business-Methode zu starten.

Wie schon beschrieben, können diese Trace-Ausgaben auf dem Tracing-System persistiert und ausgewertet werden. Damit lässt sich auch nachträglich das Aufrufverhalten innerhalb der Microservice-Anwendung analysieren. Neben einigen OpenSource-Systemen können auch kommerzielle Hersteller wie Dynatrace mit dem opentracing.io-Format umgehen.

MicroProfile OpenAPI 1.0

Mit der OpenAPI-Spezifikation (OAS) ist ein Standard etabliert worden, um RESTful APIs, die von einem Service zur Verfügung gestellt werden, zu modellieren bzw. zu dokumentieren. Diese Beschreibung soll dabei von Mensch und Maschine gleichermaßen verstanden werden. Diesem Fortschritt trägt MicroProfile mit der OpenAPI-1.0-Spezifikation Rechnung, indem sie dem Entwickler Möglichkeiten an die Hand gibt, aus seiner JAX-RS-Applikation heraus OAS-Dokumente zu generieren.

Ein Projekt, das den API-First-Ansatz verfolgt, d.h. mit einem geeigneten Editor (z.B. Swagger) eine OAS-Beschreibung seiner Services vorab erzeugt und dann mit der Implementierung der Services beginnt (oder die Service-Endpoints aus der OAS-Datei generieren lässt), kann ebenfalls von der neuen MicroProfile-Spezifikation profitieren. Denn auch Mischformen sind möglich. Die statische OAS-Datei, die in der Anwendung hinterlegt worden ist (META-INF-Verzeichnis mit folgendem Dateinamen: openapi.[.yml|.yaml|.json]), kann mit der Bottom-Up-Generierung kombiniert werden. Dabei wird nach spezifischen Annotationen im Source Code gescannt und damit die vorhandene OAS-Datei angereichert. Aus der Kombination der beiden Informationsquellen kann nun eine gemeinsame OAS-Datei erzeugt werden. Microservices, die nach dem API-First-Ansatz entwickelt werden, können die Bottom-Up-Generierung komplett deaktivieren, indem einfach das Konfigurationsattribut mp.openapi.scan.disable auf true gesetzt wird.

Lesen Sie auch: Jakarta EE – der neue Name für Java EE steht fest

Bei der Bottom-Up-Generierung ist es mitunter notwendig, auf Konfigurationswerte zurückzugreifen, beispielsweise geänderte Host-Namen. Um dies zu erreichen, setzt die MicroProfile OpenAPI-Spezifikation auf MicroProfile Config auf. Das Verhalten des Scan-Vorgangs, der Inhalt und die Generierung der OAS-Datei können dabei beeinflusst werden. Zusätzlich zu den vom Standard vorgegebenen Konfigurationsattributen (beginnend mit mp.openapi) kann ein Applikationsserver weitere Attribute (Prefix mp.openapi.extensions) definieren. Das Ergebnis der Generierung wird dann unter der Anwendungs-URL /openapi angezeigt.

Der Scan-Vorgang des Applikationsservers muss dabei nicht nur die JAX-RS-Annotationen, sondern auch die POJOs mit einbeziehen. Diese werden als Input- oder Output-Parameter der REST-Endpoints verwendet. Darüber hinaus werden weitere, im Code hinterlegte, OpenAPI-Annotationen mit in das OAS-Dokument aufgenommen.

Der schnellste und einfachste Einstieg gelingt, wenn man sich die generierte OAS-Datei seiner JAX-RS-Applikation anzeigen lässt. Falls das Ergebnis nicht den gewünschten Erwartungen entspricht, kann mit einem selbst programmierten OASModelReader (Interface org.eclipse.microprofile.openapi.OASModelReader) oder OASFilter (Interface org.eclipse.microprofile.openapi.OASFilter) das Ergebnis verändert oder Inhalte herausgefiltert werden. Der Applikationsserver muss dabei nach folgendem Regelwerk vorgehen:

  1. Konfigurationswerte (openapi) einlesen
  2. OASModelReader aufrufen
  3. Einlesen der statischen OpenAPI-Datei (falls vorhanden)
  4. Annotationen aus dem Source Code lesen und verarbeiten
  5. Das Ergebnis mit dem OASFilter filtern

Das Ergebnis steht dann am Endpoint /openapi im YAML-Format (default) oder JSON-Format zur Verfügung. Noch nicht spezifiziert ist das Verhalten, wenn mehrere Anwendungen auf einem Applikationsserver betrieben werden. Dann sollte der Applikationsserver für ein vernünftiges Zusammenführen aller OAS-Ergebnisdokumente sorgen.

Leider fehlt noch eine Möglichkeit, seine APIs internationalisiert zur Verfügung stellen zu können. Auch eine Validierung des Generierungsergebnisses oder der Umgang mit der CORS-Problematik (Cross-Origin Resource Sharing) ist noch nicht spezifiziert. Trotzdem sollten die derzeitigen Möglichkeiten für den ersten Wurf einer neuen Spezifikation ausreichend sein, um seine APIs vernünftig präsentieren zu können.

MicroProfile Type-safe Rest Client 1.0

REST Clients, die derzeit mit JAX-RS 2.0 entwickelt werden, sind noch ein wenig low-level, Entwickler müssen sich also noch mit den Spezifika des Protokolls beschäftigen. Folgendes Beispiel soll dies verdeutlichen. Ausgangspunkt ist folgendes Service Interface:

@Path("/customers")
@Produces("application/json")
public interface CustomerService {
	@GET
	@Path("/{customerId}")
	public Customer getCustomer(@PathParam("customerId") String customerId);
…
}

Ein JAX-RS 2.0 Client würde wie folgt auf die Ressource zugreifen:

public class CustomerRestClient {
	private static final String CUSTOMER_URI = “http://localhost:9080/api/customers";
  	private Client client = ClientBuilder.newClient();

   public Customer getCustomer(String customerId) {
		return client.request(MediaType.APPLICATION_JSON)
   			.target(CUSTOMER_URI)
   			.path(customerId)
          		.get(Customer.class);
   }
   ...
}

Mit dieser Art der Client-Programmierung muss der Entwickler zum Beispiel den Media Type selbst angeben und das Casting auf das Ergebnis-POJO, durch die manuelle Angabe der entsprechenden Java-Klasse, festlegen. Auch ein falsch zusammengebauter Pfad würde zu Fehlern im Aufruf führen. Genau hier will MicroProfile mit seinem typsicheren REST Client ansetzen:

public class CustomerRestClient {
	private static final String CUSTOMER_URI = “http://localhost:9080/api/customers";
  	private CustomerService customerService = RestClientBuilder.newBuilder()
			.baseUrl(CUSTOMER_URI)
			.build(CustomerService.class);

   public Customer getCustomer(String customerId) {
		return customerService.getCustomer(customerId);
   }
...
}

Im Vergleich zum normalen JAX-RS Client gibt es einige Verbesserungen. Nachdem das Service Interface des Endpoints (CustomerService) zusammen mit der URI an den RestClientBuilder übergeben worden ist, können die Methoden des Interfaces wie andere Java-Methoden aufgerufen werden. Dadurch ist der Compiler in der Lage, die Typsicherheit festzustellen und bei falscher Verwendung mit einem Compiler-Fehler zu warnen. Die korrekte Konvertierung in den vorgegeben MIME Type wird vom Client automatisch übernommen. Darüber hinaus wird auch hier wieder volle CDI-Unterstützung angeboten.

Da der RestClientBuilder das Interface javax.ws.rs.core.Configurable implementiert, wird es möglich Custom Providers (analog JAX-RS) zu registrieren. Es können also eigene ClientResponseFilter und ClientRequestFilter, MessageBodyReader und MessageBodyWriter sowie ReaderInterceptor und WriterInterceptor und ParamConverter mit eingebunden werden.

Das Exception-Mapping beim MicroProfile-basierten REST-Client erfolgt genau umgekehrt zum Exception-Mapping des JAX-RS Servers. Wenn der Client einen HTTP-Status-Code >= 400 empfängt, versucht er diesen Code in eine Sub Class von Throwable umzuwandeln. Das zentrale Interface, welches von der Client-Laufzeit verwendet wird, ist org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper<T extends Throwable>. Die Client-Laufzeit scannt dabei nach ResponseExceptionMapper-Klassen, die entsprechend einer angegebenen Priority (von niedrigen zu hohen Priority-Werten) sortiert werden.

Lesen Sie auch: Jakarta EE: Aus Raider wird Twix! Sonst ändert sich nix?

Entsprechend dieser Reihenfolge wird mit dem empfangenen javax.ws.rs.core.Response-Objekt die toThrowable(Response)-Methode des jeweiligen ResponseExceptionMapper-Objekts aufgerufen. Falls die Methode mit null antwortet, wird die Anfrage an den nächsten ResponseExceptionMapper übergeben. Dies erfolgt so lange, bis sich ein Mapper für den HTTP-Status-Code zuständig fühlt und ein passendes Throwable-Objekt erzeugt. Als Fallback implementiert die Client Runtime einen speziellen Mapper, der mit der maximalen Priority versehen ist und die Runtime Exception javax.ws.rs.WebApplicationException erzeugt.

Das vom Mapper erzeugte Throwable wird dann von der Client-Laufzeit geworfen, sofern die Methodensignatur der Service-Methode dies erlaubt. Sollte die Methodensignatur dagegen sprechen, werden die restlichen Exception Mapper abgefragt, bis schließlich einer mit der passenden (Runtime) Exception antwortet. Als letzte Möglichkeit greift dann noch die Fallback-Funktionalität der Client Runtime.

Zur Verdeutlichung soll das folgende Beispiel dienen. Das Service Interface des REST Endpoints, das der Client Runtime bekannt ist, besteht aus zwei Methoden:

@Path("/api")
public interface MyService {
	 @PUT
	 public void createSomething(String parameter);
	 @GET
	 public String selectSomething() throws SpecialException;
...
}

Desweiteren folgender Exception-Mapper:

public class MyResponseExceptionMapper implements ResponseExceptionMapper  { 	@Override
	 public SpecialException toThrowable(Response response) {
		… // check HTTP-Staus-Code und set message
		 return new SpecialException(message);
	}
}

In diesem Beispiel wird im Fehlerfall nur beim Aufruf der Methode @GET die erzeugte Exception (SpecialException) geworfen. Bei der Methode @PUT wird nach einer passenden RuntimeException gesucht, da die Methodensignatur von createSomething keine Checked Exceptions besitzt. Im Fallback-Fall wird von der Client Runtime WebApplicationException geworfen.

Analog der Providerregistrierung im JAX-RS können auch im MicroProfile REST-Client-Provider – wie JSON-P oder MessageBody – Readers und Writers registriert werden. Dazu müssen die jeweiligen Provider-Klassen mit der Annotation org.eclipse.microprofile.rest.client.annotation.RegisterProvider versehen werden, um vom RestClientBuilder erfasst zu werden.

Auch in dieser MicroProfile-Spezifikation wird die Unterstützung für CDI konsequent umgesetzt. Jedes der REST Client Inferfaces (Beispiel siehe oben „MyService„) kann mit der Annotation org.eclipse.microprofile.rest.client.inject.RegisterRestClient versehen werden. Die Client Runtime kümmert sich dann um das Erzeugen und Injizieren der Client Service CDI Bean an der richtigen Stelle:

@RequestScoped
public class MyServiceBean {
	@Inject
	@RestClient
	private MyService client;
...
}

Es stellt sich noch die Frage, wie die Annotation (@RegisterRestClient) in das Service Interface eingetragen wird, denn eigentlich könnte das Service Interface vom Swagger-generierten Source Code des REST Endpoints übernommen werden. Sowohl eine kleine Erweiterung der Code-Generierung, als auch eine Anpassung am Build-System, könnte hier helfen.

Abschließend sollte noch erwähnt werden, dass MicroProfile Type-safe Rest Client 1.0 auch mit der MicroProfile-Spezifikation Config kombiniert werden kann. So wird auch hier die Verwendung der Config-Spezifikation ermöglicht.

Die eben erwähnte MicroProfile-Config-Spezifikation hat sich marginal verändert und wurde mit der Version 1.2 in das aktuelle MicroProfile Release 1.3 aufgenommen. Auch an MicroProfile Metrics wurde weitergearbeitet. Daher ist Metrics als Version 1.1 Teil von MicroProfile 1.3.

Fazit

Beim derzeitigen Funktionsumfang von MicroProfile bleiben kaum noch Wünsche übrig. Daher wird es spannend zu sehen, was die Community für das Nachfolgerelease von MicroProfile 2.0 plant. Derzeit werden viele Anstrengungen in die Unterstützung der Entwickler gesteckt, da es für die Akzeptanz der Spezifikationen wichtig ist, ausreichend Infos für die Entwicklerwelt zur Verfügung zu stellen.

Um Entwicklern mit unterschiedlichen Informationen unter die Arme zu greifen, wurde deshalb bereits mit der Entwicklung von MicroProfile 1.4 begonnen. Die ersten Resultate dieses kommenden Releases sind in Form von Tutorials und Video-Tutorials bereits im Netz verfügbar. Das Etablieren neuer Spezifikationen und die Arbeit an verabschiedeten Spezifikationen zeigen, dass die MicroProfile Community sehr aktiv ist. Falls erkannt wird, dass noch etwas am Funktionsumfang fehlt, so wird ein weiteres Release (wie zum Beispiel MicroProfile 1.4) auf den Weg gebracht. Zusätzlich gewünschte Funktionalität kann in den Diskussionsforen eingebracht werden, Antworten kommen in der Regel zeitnah.

Also dran bleiben, denn nach dem Release ist vor dem Release.

Geschrieben von
Michael Hofmann
Michael Hofmann
Michael Hofmann ist freiberuflich als Berater, Coach, Referent und Autor tätig. Seine langjährigen Projekterfahrungen in den Bereichen Softwarearchitektur, Java Enterprise und DevOps hat er im deutschen und internationalen Umfeld gesammelt. Mail: info@hofmann-itconsulting.de
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: