Zeit zum Aufräumen: MicroProfile 4.0

Das aktuelle MicroProfile 4.0 hat deutlich länger auf sich warten lassen als geplant. Grund hierfür waren vor allem politische Überlegungen. Aber es gab auch die eine oder andere interessante Änderung innerhalb der APIs.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 14 Min.
Von
  • Lars Röwekamp
Inhaltsverzeichnis

Das aktuelle MicroProfile 4.0 hat deutlich länger auf sich warten lassen als geplant. Grund hierfür waren vor allem politische Überlegungen. Aber es gab auch die eine oder andere interessante Änderung innerhalb der APIs.

Wie bereits im ersten Teil dieses Blog-Beitrags berichtet, fand im Rahmen der Spezifikation des aktuellen MicroProfile 4.0 ein grundlegendes organisatorisches Alignment statt, das zu einer fast sechsmonatigen Verzögerung des ursprünglich geplanten Erscheinungsdatums geführt hatte.

Da die Mitglieder der einzelnen API-Spezifikationsgruppen für das vorliegende Release mit der konsequenten Migration der internen Abhängigkeiten weg von Java EE 8 hin zu Jakarta EE 8 beschäftigt waren, blieb in MicroProfile 4.0 kaum Zeit und Raum für bahnbrechende Neuerungen. Aber auch wenn sich die Änderungen innerhalb der einzelnen APIs in Grenzen halten, gibt es doch die eine oder andere interessante Neuerung.

Allein an fünf (von acht) der MicroProfile-spezifischen APIs fanden Änderungen statt, die mit der Abwärtskompatibilität brechen und daher einen genaueren Blick wert sind:

  • Config 2.0
  • Open API 2.0
  • Health 3.0
  • Metrics 3.0
  • Fault Tolerance 3.0

Mit der MicroProfile Config API können zur Laufzeit Konfigurationen aus externen Quellen herangezogen werden. Per Spezifikation muss eine MicroProfile-Config API-Implementierung mindestens die drei Quellen System Properties und Environment Variables und .properties-Datei unterstützen. Weitere externe Konfigurationsquellen, wie Datenbanken oder Cloud-basierte Key-Value-Stores, können durch Implementierung des Interfaces ConfigSource realisiert und eingebunden werden. Da die MicroProfile Config API von nahezu allen anderen MicroProfile APIs als Konfigurationsmechanismus genutzt wird, sind Änderungen an dieser API immer von besonderem Interesse.

Während in den bisherigen Versionen die Key-Value Paare der Konfigurationen jeweils einzeln aus einer der drei vorgegebenen Konfigurationsquellen beziehungsweise einer eigenen Implementierung des ConfigSource-Interfaces eingelesen werden mussten, erlaubt Version 2.0 der Config API zusätzlich einen komfortablen Bulk-Zugriff. Statt also mehrere Attribute einer Klasse mit @ConfigProperty annotieren zu müssen, reicht zukünftig die einmalige Annotation der Klasse mit @ConfigProperties, wie folgendes Beispiel aus der Config-2.0-Spezifikation verdeutlicht.

Stellen wir uns einmal folgende Konfigurationsquelle vor, die unter anderem Detailinformationen zu einer Server-Konfiguration enthält:

...
server.host = localhost
server.port=9080
server.endpoint=query
server.old.location=London
...

Um die oben gezeigten Werte en bloc in eine Konfigurationsklasse einzulesen, muss diese lediglich mit @ConfigProperties annotiert und die Gruppe der einzulesenden Properties via Präfix – in diesem Fall "server" – angegeben werden:

@ConfigProperties(prefix="server")
@Dependent
public class ServerDetails {
// the value of the configuration property server.host
public String host;
// the value of the configuration property server.port

public int port;
//the value of the configuration property server.endpoint
private String endpoint;
//the value of the configuration property server.old.location
public @ConfigProperty(name="old.location")
String location;

public String getEndpoint() {
return endpoint;
}
}

Ebenfalls interessant ist die in Version 2.0 neu hinzugekommene Unterstützung von Property Expressions. Mit ihrer Hilfe ist es möglich, Konfigurationswerte innerhalb einer Konfigurationsquelle mittels Ausdrucks zu parametrisieren. Im folgenden Beispiel würde beim Einlesen der Ausdruck von server.url ausgewertet und durch den Wert von server.host ersetzt werden:

server.url=http://${server.host}/endpoint
server.host=example.org

Das Ergebnis der obigen Konfiguration wäre somit:

server.url=http://example.org/endpoint

Natürlich gelten für die Property Expressions dieselben Regeln, wie für alle anderen Konfigurationen auch. Die Konfigurationswerte können aus beliebigen Konfigurationsquellen stammen und bei Mehrfachvorkommen wird derjenige Wert herangezogen, dessen Konfigurationsquelle die höchste Ordinalität besitzt.

Neben einfachen Ausdrücken sind zusätzlich aneinandergehängte und verschachtelte Ausdrücke erlaubt. Und auch die Angabe von Default-Werten, für den Fall, dass ein Ausdruck nicht ausgewertet werden kann, ist möglich:

server.url=http://${server.host:example.org}:${server.port}/${server.endpoint}
server.port=8080
server.endpoint=${server.endpoint.path.${server.endpoint.path.bar}}
server.endpoint.path.foo=foo
server.endpoint.path.bar=foo

Im obigen Beispiel würde der Ausdruck server.host innerhalb der Konfiguration von server.url nicht aufgelöst werden können und daher durch den mittels ":" angegebenen Default-Wert example.org ersetzt werden. Der Ausdruck server.endpoint dagegen ergibt sich durch die verschachtelte Auswertung von server.endpoint.path.${server.endpoint.path.bar}, was wiederum dem Ausdruck server.endpoint.path.foo entspricht.

http://example.org:8080/foo

Was aber passiert, wenn einer der Ausdrücke nicht ausgewertet werden kann, gleichzeitig aber auch kein Default-Wert angegeben wurde? In dem Fall wird beim Einlesen des Ausdrucks eine NoSuchElementException geworfen beziehungsweise im Falle eines Optional Attribute ein leeres Optional zurückgegeben.

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Config-2.0-Spezifikation sowie im zugehörigen MicroProfile Config 2.0 GitHub Repository.

Die MicroProfile-Open-API-Spezifikation dient zur Bereitstellung von API-Beschreibungen. Mit ihrer Hilfe lassen sich mit OpenAPI v3 konforme Dokumentationen der anwendungseigenen JAX-RS Schnittstellen generieren.

Innerhalb der neuen Version 2.0 der MicroProfile Open API haben hauptsächlich Aufräumarbeiten stattgefunden. So wurden zum Beispiel die beiden Model Interfaces Scope und ServerVariables, die bereits in der Version 1.1. als "deprecated" markiert wurden, endgültig entfernt. Gleiches gilt für etliche als "deprecated" markierte Methoden der Model Interfaces APIResponses, Callback, Content, OASFactory, OAuthFlow, OpenAPI, Path, PathItem, Schema, SecurityRequirement und Server. Grund für die Markierung als "veraltet" war in der Version 1.1 übrigens eine Vereinheitlichung der Namensgebung, die nun in der Version 2.0 der Open API voll zum Tragen kommt.

Neben den eben beschriebenen Aufräumarbeiten hat es vor allem Neuerungen im Bereich der Schema-Annotationen gegeben, also der Art und Weise, wie die Datentypen von Eingaben und Ausgaben definiert werden können. So können zum Beispiel dank der neu eingeführten Annotation @SchemaProperty die Properties für ein Schema inline definiert werden, was die bisher zusätzlich notwendigen Schema-Annotationen innerhalb der annotierten Klasse erspart:

@Schema(properties={
@SchemaProperty(name="creditCard", required=true),
@SchemaProperty(name="departureFlight", description="... ."),
@SchemaProperty(name="returningFlight")
})
public class Booking {
...
}

Und auch für den Request- und den Response-Body gibt es nun eigene Schema-Annotationen, um so deren Schema-Definition deutlich zu vereinfachen:

@RequestBodySchema(MyRequestObject.class)   
@APIResponseSchema(MyResponseObject.class)

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Open-API-2.0-Spezifikation sowie im zugehörigen MicroProfile Open API 2.0 GitHub Repository.

Die MicroProfile Health API erlaubt die Abfrage des "Gesundheitszustands" einer Anwendung beziehungsweise eines Service mithilfe sogenannter Health Checks. Die Spezifikation unterscheidet dabei zwischen den beiden Varianten Liveness Check und Readiness Check.

Während das Ergebnis eines Liveness Check signalisiert, ob eine Anwendung zum Zeitpunkt der Anfrage läuft, kann mittels Readiness Check zusätzlich abgefragt werden, ob die Anwendung auch bereit ist, Anfragen entgegenzunehmen und zu verarbeiten.

Eine positive (UP) beziehungsweise negative (DOWN) Antwort auf einen Liveness Check hilft somit zum Beispiel, 3rd-Party-Services zu entscheiden, ob eine Anwendung in einem Zustand ist, dass sie ordnungsgemäß – also ohne Verluste von Daten – heruntergefahren beziehungsweise beendet werden kann. Eine positive oder negative Antwort auf einen Readiness Check dagegen hilft 3rd-Party-Services zu entscheiden, ob Anfragen an die Anwendung bezeihungsweise den Service weitergeleitet werden können.

Die MicroProfile-Health-Spezifikation sah bisher vor, dass ein MicroProfile-Container so lange eine negative Antwort, also ein DOWN, als Default für einen Readiness Check zurückliefern muss, bis eine selbstgeschriebene Implementierung eines HealtCheck-Interfaces vom Typ @Readiness diesen auf UP setzt. Mit der neuen Version 3.0 ist nun zusätzlich die Möglichkeit geschaffen worden, diesen Wert via Konfiguration

mp.health.default.readiness.empty.response

zu setzen, wodurch als Standardwert auch ein UP als Rückgabewert möglich ist, ohne dass eine entsprechende Implementierung vorliegen muss.

Darüber hinaus wurde die bereits seit der Version 2.0 als veraltet (deprecated) markierte @Health-Annotation auf optional (pruned) gesetzt. Das bedeutet, dass die eine oder andere Implementierung diese Annotation eventuell auch weiterhin anbieten wird, man aber keinesfalls davon ausgehen darf, dass sie weiterhin zur Verfügung steht. Dieser Schritt ergibt Sinn, da die @Health-Annotation bereits in der Version 2.0 durch die deutlich aussagekräftigeren Pendants @Liveness und @Readiness ersetzt wurde.

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Health-3.0-Spezifikation sowie im zugehörigen MicroProfile Health 3.0 GitHub Repository.

Die MicroProfile Metrics API stellt Telemetriedaten einer Anwendung über Zeitreihen hinweg mittels entsprechenden Monitoring-Endpoint /metrics in dem OpenMetrics Text Format (aka Prometheus Exposition Format) oder alternativ in Form von JSON zur Verfügung.

Die wohl augenscheinlichste Änderung innerhalb der MicroProfile-Metrics-3.0-Spezifikation ist die Überführung der ehemals abstrakten Klasse MetricRegistry hin zu einem Interface. Einhergehend mit dieser Änderung sind dem neuen Interface gleich eine ganze Reihe neuer statischer Methoden zur Registrierung beziehungsweise zum Zugriff auf die verschiedenen Metriktypen spendiert worden.

Da der Zugriff auf eine konkrete Implementierung des Interfaces für die verschiedenen Scopes (Type.APPLICATION, Type.BASE, Type.VENDOR) allerdings in der Regel via Injection vollzogen wird, hat sich in der Verwendung durch Entwickler – mit Ausnahme der neuen Methoden – allerdings kaum etwas geändert:

@Inject
@RegistryType(type=MetricRegistry.Type.APPLICATION)
MetricRegistry appRegistry

@Inject
@RegistryType(type=MetricRegistry.Type.BASE)
MetricRegistry baseRegistry;

@Inject
@RegistryType(type=MetricRegistry.Type.VENDOR)
MetricRegistry vendorRegistry;

Eine weitere Änderung innerhalb der Metrics API betrifft die gezielte Wiederverwendung von Metriken. Musste bis zur Version 2.3 eine gewünschte Wiederverwendung einer Metrik – zum Beispiel eines Aufrufzählers – über mehrere Methoden hinweg noch explizit via reusable=true bei der Registrierung der Metrik angegeben werden, ist dieses Verhalten seit der Version 3.0 Standard und kann auch nicht ausgeschaltet werden. Eine Ausnahme bildet hier lediglich die Gauge-Metrik, die weiterhin keine Wiederverwendung ein und derselben Metric-ID über mehrere Methoden hinweg erlaubt und im Falle eines Mehrfachvorkommens automatisch zu einer IllegalArgumentException beim Start der Anwendung führt.

Ebenfalls entfallen ist die Möglichkeit, die @Metric-Annotation zur Registrierung von durch CDI-Producer-Methoden erzeugten Metriken zu nutzen. Diese müssen nun über einen anderen Weg, zum Beispiel durch die Verwendung des MetricRegestry-Interfaces, registriert werden. Die @Metric-Annotation kann somit zukünftig nur noch als Injection-Point im Zusammenspiel mit der @Inject-Annotation auf Feld- oder Parameterebene verwendet werden.

Änderungen gab es auch im Bereich der Timer-Metriken, die jetzt nicht mehr mit Parametern vom Typ long und java.util.concurrent.TimeUnit, sondern stattdessen einfach mit einem Parameter vom Typ java.time.Duration aktualisiert werden können.

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Metrics-3.0-Spezifikation sowie im zugehörigen MicroProfile Metrics 3.0 GitHub Repository.

Die MicroProfile Fault Tolerance API bietet eine Reihe etablierter Resilience-Strategien – Timeout, Retry, Fallback, Bulkhead und Circuit Breaker – zur Behandlung von Fehlersituationen innerhalb einer MicroProfile-basierten Anwendung.

Die Liste der Änderungen innerhalb der neuen Version 3.0 der MicroProfile Fault Tolerance API ist recht überschaubar und erstreckt sich im Wesentlichen auf zwei Punkte:

  • Zum einen hat es eine Klärung bezüglich des Lifecycle der beiden Fault-Tolerance-Startegien Circuit Breaker und Bulkhead gegeben. Dies Klärung sieht vor, dass beide Varianten bezüglich der zugehörigen CDI Bean als Singleton implementiert werden müssen. Ist also zum Beispiel eine Methode innerhalb einer @RequestScoped Bean mit @CircuitBreaker annotiert, so muss sichergestellt werden, dass alle Aufrufe der Methode sich – trotz unterschiedlicher Bean Instanzen – ein und denselben State des Circuit Breaker teilen.
  • Die zweite Änderung betrifft die, durch die Fault Tolerance Annotationen @Retry, @Timeout, @CircuitBreaker, @Bulkhead und @Fallback automatisch generierten Metriken. Diese finden sich zukünftig aus Gründen der Konsistenz mit den anderen MicroProfile APIs nicht mehr im Scope application: sondern im Scope base: und werden entsprechend über den Endpunkt /metrics/base statt /metrics/application zur Verfügung gestellt.

Darüber hinaus nutzen die Metriken von Fault Tolerance 3.0 zukünftig die in MicroProfile Metrics 2.0 eingeführten Tags zur genaueren Spezifizierung der jeweiligen Metrik.

Eine annotierte Klasse

package com.exmaple;

@Timeout(1000)
public class MyClass {

@Retry
public void doWork() {
// do some very important work
}

}

würde somit aufgrund der eben genannten Änderungen im Bereich der Scopes und Tags unter anderem zu folgender Metrik führen:

base:ft.timeout.calls.total{method=“com.example.MyClass.doWork“, timedOut=”true”}

statt wie bisher zu

application:ft.com.example.MyClass.doWork.timout.callsTimedOut.total

In Konsequenz bedeutet dies, dass bestehende Dashboards und Abfragen, die auf die verschiedenen Metriken der Fault-Tolerance-3.0-Annotationen zugreifen, entsprechend angepasst werden müssen.

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Fault-Tolerance-3.0-Spezifikation sowie im zugehörigen MicroProfile Fault Tolerance 3.0 GitHub Repository.

Auch wenn die Neuerungen und Änderungen innerhalb der MicroProfile 4.0 APIs eher als überschaubar zu bezeichnen sind, haben die verschiedenen API-Spezifikationsgruppen doch die Chance des anstehenden Major-Release-Sprungs genutzt, um ein wenig innerhalb ihrer API aufzuräumen.

Besonders positiv fällt dabei ins Auge, dass alle Spezifikationsgruppen anscheinend großen Wert auf eine MicroProfile-weite und somit API-übergreifende Vereinheitlichung zu setzen. Das spiegelt sich zum Beispiel in der Umbenennung von Methoden oder aber der Namensgebung erzeugter Metriken wieder.

Ebenfalls großer Wert wurde auf eine gute Developer Experience gelegt, was sich unter anderem an der neu eingeführten @ConfigProperties-Annotation zum Bulk-Einlesen von Konfigurationsdaten innerhalb der Config-API-Spezifikation oder aber der vereinfachten Schema-Definition innerhalb der Open-API-Spezifikation zeigt.

Aus der Praxis für die Praxis. Diesem Motto ist man trotz der Einführung von Working Group und Specification Process treu geblieben – und das ist auch gut so. ()