Reaktive JAX-RS-Client-API

Reaktive JAX-RS-Client-API

1. Einführung

In diesem Tutorial befassen wir uns mit der JAX-RS-Unterstützung für die reaktive (Rx-) Programmierung mit der Jersey-API. In diesem Artikel wird davon ausgegangen, dass der Leser über Kenntnisse der Jersey REST-Client-API verfügt.

Eine gewisse Vertrautheit mitreactive programming concepts ist hilfreich, aber nicht erforderlich.

2. Abhängigkeiten

Zunächst benötigen wir die Standardabhängigkeiten der Jersey-Clientbibliothek:


    org.glassfish.jersey.core
    jersey-client
    2.27


    org.glassfish.jersey.inject
    jersey-hk2
    2.27

Diese Abhängigkeiten unterstützen uns bei der reaktiven Programmierung mit JAX-RS. Die aktuellen Versionen vonjersey-client undjersey-hk2 sind in Maven Central verfügbar.

Für die Unterstützung reaktiver Frameworks von Drittanbietern verwenden wir die folgenden Erweiterungen:


    org.glassfish.jersey.ext.rx
    jersey-rx-client-rxjava
    2.27

Die obige Abhängigkeit bietet Unterstützung fürObservable;von RxJava fürFlowabledes neueren RxJava2. Verwenden Sie die folgende Erweiterung:


    org.glassfish.jersey.ext.rx
    jersey-rx-client-rxjava2
    2.27

Die Abhängigkeiten zurxjava undrxjava2 sind auch in Maven Central verfügbar.

3. Warum wir reaktive JAX-RS-Clients brauchen

Nehmen wir an, wir müssen drei REST-APIs verwenden:

  • id-service enthält eine Liste mit langen Benutzer-IDs

  • name-service gibt einen Benutzernamen für eine bestimmte Benutzer-ID an

  • hash-service gibt einen Hash sowohl der Benutzer-ID als auch des Benutzernamens zurück

Wir erstellen für jede Dienstleistung einen Kunden:

Client client = ClientBuilder.newClient();
WebTarget userIdService = client.target("http://localhost:8080/id-service/ids");
WebTarget nameService
  = client.target("http://localhost:8080/name-service/users/{userId}/name");
WebTarget hashService = client.target("http://localhost:8080/hash-service/{rawValue}");

Dies ist ein ausgedachtes Beispiel, aber es funktioniert zum Zweck unserer Veranschaulichung. Die JAX-RS-Spezifikation unterstützt mindestens drei Ansätze, um diese Dienste zusammen zu nutzen:

  • Synchron (blockieren)

  • Asynchron (nicht blockierend)

  • Reaktiv (funktional, nicht blockierend)

3.1. Das Problem beim synchronen Aufruf des Jersey-Clients

Beim Vanilla-Ansatz zum Konsumieren dieser Dienste werden wirid-service verbrauchen, um die Benutzer-IDs abzurufen, und dann die APIsname-service undhash-service nacheinander für jede zurückgegebene ID aufrufen.

With this approach, each call blocks the running thread until the request is fulfilled, die insgesamt viel Zeit damit verbringen, die kombinierte Anforderung zu erfüllen. Dies ist in jedem nicht trivialen Anwendungsfall eindeutig weniger als zufriedenstellend.

3.2. Das Problem mit dem asynchronen Jersey-Client-Aufruf

Ein ausgefeilterer Ansatz ist die Verwendung des von JAX-RS unterstütztenInvocationCallback -Sechanismus. In der einfachsten Form übergeben wir einen Rückruf an dieget-Methode, um zu definieren, was passiert, wenn der angegebene API-Aufruf abgeschlossen ist.

Während wir jetzt eine echte asynchrone Ausführung (with some limitations on thread efficiency) erhalten, ist es leicht zu erkennen, wiethis style of code can get unreadable and unwieldy in alles andere als trivialen Szenarien ist. DieJAX-RS specification heben dieses Szenario speziell alsPyramid of Doom hervor:

// used to keep track of the progress of the subsequent calls
CountDownLatch completionTracker = new CountDownLatch(expectedHashValues.size());

userIdService.request()
  .accept(MediaType.APPLICATION_JSON)
  .async()
  .get(new InvocationCallback>() {
    @Override
    public void completed(List employeeIds) {
        employeeIds.forEach((id) -> {
        // for each employee ID, get the name
        nameService.resolveTemplate("userId", id).request()
          .async()
          .get(new InvocationCallback() {
              @Override
              public void completed(String response) {
                     hashService.resolveTemplate("rawValue", response + id).request()
                    .async()
                    .get(new InvocationCallback() {
                        @Override
                        public void completed(String response) {
                            //complete the business logic
                        }
                        // ommitted implementation of the failed() method
                    });
              }
              // omitted implementation of the failed() method
          });
        });
    }
    // omitted implementation of the failed() method
});

// wait for inner requests to complete in 10 seconds
if (!completionTracker.await(10, TimeUnit.SECONDS)) {
    logger.warn("Some requests didn't complete within the timeout");
}

So haben wir asynchronen, zeiteffizienten Code erreicht, aber:

  • es ist schwer zu lesen

  • Jeder Aufruf erzeugt einen neuen Thread

Beachten Sie, dass wir in allen Codebeispielen einCountDownLatch verwenden, um darauf zu warten, dass alle erwarteten Werte vonhash-service. geliefert werden. Wir tun dies, damit wir behaupten können, dass der Code in einem Komponententest funktioniert indem überprüft wird, ob alle erwarteten Werte tatsächlich geliefert wurden.

Ein gewöhnlicher Client würde nicht warten, sondern alles tun, was mit dem Ergebnis innerhalb des Rückrufs geschehen sollte, um den Thread nicht zu blockieren.

3.3. Die funktionale, reaktive Lösung

Ein funktionaler und reaktiver Ansatz gibt uns:

  • Hervorragende Lesbarkeit des Codes

  • Fließende Codierung

  • Effektives Thread-Management

JAX-RS unterstützt diese Ziele in folgenden Komponenten:

  • CompletionStageRxInvoker unterstützt dieCompletionStage-Schnittstelle als reaktive Standardkomponente

  • RxObservableInvokerProvider unterstützt RxJavasObservable 

  • RxFlowableInvokerProviderunterstützen RxJavasFlowable

Es gibt auchAPI, um Unterstützung für andere reaktive Bibliotheken hinzuzufügen.

4. Unterstützung für reaktive JAX-RS-Komponenten

4.1. CompletionStage in JAX-RS

Mit demCompletionStage -Sand kann seine konkrete Implementierung -CompletableFuture – we eine elegante, nicht blockierende und fließende Orchestrierung von Serviceanrufen schreiben.

Beginnen wir mit dem Abrufen der Benutzer-IDs:

CompletionStage> userIdStage = userIdService.request()
  .accept(MediaType.APPLICATION_JSON)
  .rx()
  .get(new GenericType>() {
}).exceptionally((throwable) -> {
    logger.warn("An error has occurred");
    return null;
});

The rx() method call is the point from which the reactive handling kicks in. Wir verwenden die Funktionexceptionally, um unser Ausnahmebehandlungsszenario fließend zu definieren.

Von hier aus können wir die Aufrufe sauber orchestrieren, um den Benutzernamen vom Namensdienst abzurufen und dann die Kombination aus Name und Benutzer-ID zu hashen:

List expectedHashValues = ...;
List receivedHashValues = new ArrayList<>();

// used to keep track of the progress of the subsequent calls
CountDownLatch completionTracker = new CountDownLatch(expectedHashValues.size());

userIdStage.thenAcceptAsync(employeeIds -> {
  logger.info("id-service result: {}", employeeIds);
  employeeIds.forEach((Long id) -> {
    CompletableFuture completable = nameService.resolveTemplate("userId", id).request()
      .rx()
      .get(String.class)
      .toCompletableFuture();

    completable.thenAccept((String userName) -> {
        logger.info("name-service result: {}", userName);
        hashService.resolveTemplate("rawValue", userName + id).request()
          .rx()
          .get(String.class)
          .toCompletableFuture()
          .thenAcceptAsync(hashValue -> {
              logger.info("hash-service result: {}", hashValue);
              receivedHashValues.add(hashValue);
              completionTracker.countDown();
          }).exceptionally((throwable) -> {
              logger.warn("Hash computation failed for {}", id);
              return null;
         });
    });
  });
});

if (!completionTracker.await(10, TimeUnit.SECONDS)) {
    logger.warn("Some requests didn't complete within the timeout");
}

assertThat(receivedHashValues).containsAll(expectedHashValues);

Im obigen Beispiel stellen wir unsere Ausführung der 3 Dienste mit flüssigem und lesbarem Code zusammen.

Die MethodethenAcceptAsync führt die bereitgestellte Funktionafter aus, wennCompletionStage die Ausführung abgeschlossen hat (oder eine Ausnahme ausgelöst hat).

Jeder nachfolgende Aufruf ist nicht blockierend, sodass die Systemressourcen sinnvoll genutzt werden.

The CompletionStage interface provides a wide variety of staging and orchestration methods that allow us to compose, order and asynchronously execute any number of steps in einer mehrstufigen Orchestrierung (oder einem einzelnen Serviceabruf).

4.2. Observable in JAX-RS

Um dieObservable RxJava-Komponente zu verwenden, müssen wir zuerst denRxObservableInvokerProvider -Sprovider (und nicht den im Jersey-Spezifikationsdokument angegebenen „ObservableRxInvokerProvider” as“) auf dem Client registrieren:

Client client = client.register(RxObservableInvokerProvider.class);

Dann überschreiben wir den Standard-Aufrufer:

Observable> userIdObservable = userIdService
  .request()
  .rx(RxObservableInvoker.class)
  .get(new GenericType>(){});

Ab diesem Punkt haben wircan use standard Observable semantics to orchestrate the processing flow:

userIdObservable.subscribe((List listOfIds)-> {
  /** define processing flow for each ID */
});

4.3. Flowable in JAX-RS

Die Semantik für die Verwendung von RxJavaFlowable ist ähnlich der vonObservable. . Wir registrieren den entsprechenden Anbieter:

client.register(RxFlowableInvokerProvider.class);

Dann liefern wir dieRxFlowableInvoker:

Flowable> userIdFlowable = userIdService
  .request()
  .rx(RxFlowableInvoker.class)
  .get(new GenericType>(){});

Anschließend können wir den normalenFlowable API verwenden.

5. Fazit

Die JAX-RS-Spezifikation bietet eine Reihe von Optionen, die eine saubere, nicht blockierende Ausführung von REST-Aufrufen ermöglichen.

Insbesondere dieCompletionStage -Sinterschnittstelle bietet eine Reihe robuster Methoden, die eine Vielzahl von Service-Orchestrierungsszenarien abdecken, sowie Möglichkeiten, benutzerdefinierteExecutors  bereitzustellen, um die Thread-Verwaltung genauer steuern zu können.

Sie können den Code für diesen Artikelover on Github überprüfen.