Microservice-Entwicklung mit Java EE – eine Einführung in Eclipse MicroProfile

Seite 2: Die 12 Bestandteile von Eclipse MicroProfile

Inhaltsverzeichnis

Eclipse MicroProfile 2.0 besteht aus zwölf miteinander integrierten Spezifikationen. Vier davon sind vielen Entwicklern bereits aus Java EE 8 bekannt: CDI 2.0, JSON-P 1.1, JAX-RS 2.1 und JSON-B 1.0. Somit basiert das zentrale Entwicklungsmodell von Eclipse MicroProfile auf erprobten Java-EE-Spezifikationen, mit denen viele Entwicklungsteams bereits vertraut sind. Das vereinfacht nicht nur den Umstieg, sondern ermöglicht es auch bereits bestehende Komponenten und Bibliotheken weiterzuverwenden. Andere Java-EE-Spezifikationen sind nicht Bestandteil von Eclipse MicroProfile 2.0, stehen je nach MicroProfile-Implementierung aber bereits zur Verfügung oder lassen sich bei Bedarf hinzufügen.

Die anderen acht Spezifikationen wurden im Rahmen des Eclipse-MicroProfile-Projekts erstellt und standardisieren häufig Konzepte, die bereits in populären Bibliotheken umgesetzt wurden. Sie behandeln typische Fragestellungen, die beim Betrieb eines verteilten, horizontal skalierten Microservice-Systems auftreten.

Die Konfiguration von Anwendungen hat Java EE bisher nicht behandelt. Daher stellt Eclipse MicroProfile eine eigene Spezifikation bereit, die eine Standardisierung verschiedener Konfigurationslösungen und somit deren implementierungsunabhängige Verwendung ermöglicht.

Die meisten Enterprise-Anwendungen verwenden Konfigurationsdaten, mit denen sie sich an die jeweilige Ausführungsumgebung anpassen lassen. Dazu zählen beispielsweise die Verbindungsdaten zu externen Systemen, umgebungsspezifische Pfadangaben und Timeouts.

Auf Basis der MicroProfile-Config-Spezifikation lassen sich diese Informationen aus standardisierten Quellen wie lokalen Systemeigenschaften und Dateien, aber auch aus eigenen Quellen wie einer Datenbank, laden und zu einer Anwendungskonfiguration zusammenführen.

Innerhalb der Anwendung besteht die Möglichkeit auf die Konfigurationsparameter programmatisch zuzugreifen oder einzelne Konfigurationsparameter zu injecten. Im folgenden Beispiel wird der Wert des Konfigurationsparameter my.config.someService.url injected. Falls dieser von keiner Konfigurationsquelle bereitgestellt wird, löst es während des Deployments der Anwendung eine DeploymentException aus.

@ApplicationScoped
public class InjectConfigurationParameter {

@Inject
@ConfigProperty(name="my.config.someService.url")
private String someUrl;


}

Die Überwachung eines Systems von Microservices ist deutlich komplexer als die Überwachung monolithischer Anwendungen. Anstelle weniger Instanzen einer einzigen Anwendung gilt es nun mehrere Instanzen von unterschiedlichen, verteilten Services zu überwachen. Health Checks stellen dabei die einfachste Möglichkeit der Überwachung dar. Sie liefern lediglich die Information, ob eine Serviceinstanz verfügbar ist. Dieser Mechanismus wird in vielen Cloud-Umgebungen eingesetzt, um ausgefallene Microservice-Instanzen automatisiert neu zu starten.

Die Health-Check-Spezifikation definiert dafür den /health-REST-Endpoint, den jede MicroProfile-Implementierung bereitstellen muss. Wenn das System verfügbar ist, antwortet dieser Endpoint mit dem HTTP Code 200 und dem Status UP. Zusätzlich lassen sich weitere Informationen über einzelne Komponenten des Service bereitstellen und komplexere Prüfungen implementieren.

{ 
"outcome" : "UP"
"checks": []
}

Werden detailliertere Informationen über den Zustand eines Microservice benötigt, kann der zusätzliche Metriken exportieren. Diese lassen sich anschließend von Monitoring-Anwendungen überwachen und darstellen. Die MicroProfile-Metrics-Spezifikation definiert dafür eine einheitliche API, über die alle MicroProfile-Implementierungen ihre Metriken bereitstellen.

Die Spezifikation unterteilt die Metriken in drei Kategorien:

  • • erforderliche Metriken,
  • • anbieterspezifische Metriken und
  • • anwendungsspezifische Metriken.

Die erforderlichen Metriken stehen unter /metrics/base bereit und liefern Statistiken über die JVM und das Betriebssystem wie die Größe des Heap, die Anzahl der aktuell verwendeten Threads oder die bisher für die Garbage Collection aufgewendete Zeit.

Alle Anbieter einer MicroProfile-Implementierung können die Metriken um eigene Statistiken erweitern, die dann unter /metrics/vendor bereitstehen.

Die Spezifikation definiert außerdem eine API, mit der Anwendungen eigene Metriken bereitstellen können, die sich unter /metrics/application abrufen lassen. Um eigene Metriken zu implementieren, muss lediglich ein Typ, eine Methode oder ein Konstruktor mit @Counted, [i]@Gauge, @Metered, @Timed oder @Metric annotiert werden. Die MicroProfile-Metrics-Implementierung zählt dann die Anzahl der Aufrufe, stellt den Rückgabewert einer Methode bereit, bestimmt die Frequenz der Aufrufe, misst die Ausführungszeit der Aufrufe oder registriert eine durch den Entwickler verwaltete Metrik.

Der folgende Codeausschnitt zeigt ein Beispiel für eine Metrik, die die Ausführungszeit der buildName-Methode misst.

@Timed(absolute = true,
name = "example.buildName",
displayName = "Time to build the name",
description = "Time of buildName in ns",
unit = MetricUnits.NANOSECONDS)
public String buildName() {
// do something
return getNameFromRegistry();
}

Auf diese Art lassen sich detaillierte Informationen über den Zustand einer Anwendung sammeln und für die Überwachung und Steuerung des Systems bereitstellen.

Fehlertoleranz ist in verteilten Systemen und somit bei der Erstellung von Microservices von besonderer Bedeutung. Während monolithische Systeme entweder vollständig verfügbar oder nicht verfügbar sind, können bei einer Microservice-Architektur einzelne Services ausfallen. Dadurch erhöht sich die Anzahl der Fehlerquellen und somit die Komplexität des Gesamtsystems. Es bietet bei einer lose gekoppelten, fehlertoleranten Implementierung der Services jedoch auch den Vorteil, dass sich Ausfälle in einem Teilbereich des Systems behandeln oder isolieren lassen und somit andere Teile des Systems verfügbar bleiben.

Es gibt bereits einige Bibliotheken, beispielsweise Hystrix, mit denen sich die technischen Anforderungen einer solchen Implementierung umsetzen lassen. Die Fault-Tolerance-Spezifikation definiert darauf aufbauend eine API, mit der sich bekannte Konzepte wie Timeouts, Retry-Mechanismen, Fallback-Verhalten, Bulkheads und CircuitBreaker verwenden lassen.

Das Ziel der Spezifikation ist es, Anwendungsentwicklern die technisch erforderlichen Komponenten für eine fehlertolerante Implementierung durch den Container bereitzustellen. Dafür definiert sie verschiedene InterceptorBindings, mit denen sich die zuvor genannten Konzepte auf einzelne Methoden oder CDI Beans anwenden lassen. Um die InterceptorBindings zu verwenden, ist lediglich eine CDI Bean oder eine Methode einer CDI Bean mit einer oder mehrerer der folgenden Annotationen zu annotieren: @Retry, @Timeout, @CircuitBreaker, @Bulkhead und @Fallback.

Im folgenden Codebeispiel dienen diese InterceptorBindings dazu ein Retry- und Fallback-Verhalten für die buildName-Methode zu definieren.

package org.example;



public class MyBean {
@Retry(maxRetries = 3)
@Fallback(fallbackMethod= "defaultName")
public String buildName() {
// do something
return getNameFromRegistry();
}
private String defaultName () {
return "aDefaultName";
}
}

Die @Retry-Annotation definiert, dass, falls ein Aufruf der buildName-Methode fehlschlägt, bis zu drei zusätzliche Aufrufversuche ausgeführt werden. Falls keiner dieser Aufrufe erfolgreich ist, wird die durch die @Fallback-Annotation definierte fallbackMethod ausgeführt. In diesem Beispiel ist das die defaultName-Method, die einen vordefinierten String zurückgibt. Die Parameter und der Rückgabewert der beiden Methoden müssen dabei identisch sein, um eine FaultToleranceDefinitionException zu vermeiden.