API do cliente JAX-RS reativa

API do cliente JAX-RS reativa

1. Introdução

Neste tutorial, examinamos o suporte JAX-RS para programação reativa (Rx) usando a API de Jersey. Este artigo pressupõe que o leitor tenha conhecimento da API do cliente REST de Jersey.

Alguma familiaridade comreactive programming concepts será útil, mas não é necessária.

2. Dependências

Primeiro, precisamos das dependências padrão da biblioteca cliente de Jersey:


    org.glassfish.jersey.core
    jersey-client
    2.27


    org.glassfish.jersey.inject
    jersey-hk2
    2.27

Essas dependências nos fornecem suporte de programação reativa em estoque JAX-RS. As versões atuais dejersey-clientejersey-hk2 estão disponíveis no Maven Central.

Para suporte de estrutura reativa de terceiros, usaremos estas extensões:


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

A dependência acima fornece suporte para RxJava’sObservable; para o RxJava2’sFlowablewe mais recente, use a seguinte extensão:


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

As dependências derxjavaerxjava2 também estão disponíveis no Maven Central.

3. Por que precisamos de clientes JAX-RS reativos

Digamos que temos três APIs REST para consumir:

  • oid-service fornece uma lista de IDs de usuário longos

  • oname-service fornece um nome de usuário para um determinado ID de usuário

  • ohash-service retornará um hash do ID do usuário e do nome de usuário

Criamos um cliente para cada um dos serviços:

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}");

Este é um exemplo artificial, mas funciona com o objetivo de nossa ilustração. A especificação JAX-RS suporta pelo menos três abordagens para consumir esses serviços juntos:

  • Síncrono (bloqueio)

  • Assíncrono (sem bloqueio)

  • Reativo (funcional, sem bloqueio)

3.1. O problema com a invocação do cliente Jersey síncrona

A abordagem comum para consumir esses serviços nos fará consumirid-service para obter os IDs de usuário e, em seguida, chamar as APIsname-serviceehash-service sequencialmente para cada ID retornado.

With this approach, each call blocks the running thread until the request is fulfilled, gastando muito tempo no total para atender à solicitação combinada. Isso é claramente menos do que satisfatório em qualquer caso de uso não trivial.

3.2. O problema com a invocação de cliente Jersey assíncrona

Uma abordagem mais sofisticada é usar o smechanismInvocationCallback uportado por JAX-RS. Em sua forma mais básica, passamos um retorno de chamada para o métodoget para definir o que acontece quando a chamada API é concluída.

Embora agora tenhamos uma verdadeira execução assíncrona (with some limitations on thread efficiency), é fácil ver comothis style of code can get unreadable and unwieldy em qualquer situação, menos em cenários triviais. OJAX-RS specification destaca especificamente este cenário como oPyramid of Doom:

// 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");
}

Portanto, alcançamos um código assíncrono e eficiente em termos de tempo, mas:

  • é difícil de ler

  • cada chamada gera um novo segmento

Observe que estamos usandoCountDownLatch em todos os exemplos de código para esperar que todos os valores esperados sejam entregues porhash-service.. Fazemos isso para que possamos afirmar que o código funciona em um teste de unidade verificando se todos os valores esperados foram realmente entregues.

Um cliente comum não esperaria, mas faria o que deveria ser feito com o resultado no retorno de chamada para não bloquear o encadeamento.

3.3. A solução funcional e reativa

Uma abordagem funcional e reativa nos dará:

  • Ótima legibilidade de código

  • Estilo de codificação fluente

  • Gerenciamento eficaz de threads

O JAX-RS suporta esses objetivos nos seguintes componentes:

  • CompletionStageRxInvoker suporta a interfaceCompletionStage como o componente reativo padrão

  • RxObservableInvokerProvider suportaObservable  de RxJava

  • RxFlowableInvokerProvider oferece suporte aFlowable de RxJava

Também existe umAPI para adicionar suporte para outras bibliotecas reativas.

4. Suporte ao componente reativo JAX-RS

4.1. CompletionStage en JAX-RS

Usando oCompletionStage lixa sua implementação concreta -CompletableFuture – we pode escrever uma orquestração de chamada de serviço fluente, elegante e sem bloqueios.

Vamos começar recuperando os IDs de usuário:

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. Usamos a funçãoexceptionally para definir fluentemente nosso cenário de tratamento de exceções.

A partir daqui, podemos orquestrar de forma limpa as chamadas para recuperar o nome de usuário do serviço de nome e, em seguida, misturar com hash a combinação do nome e do ID do usuário:

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);

No exemplo acima, compomos nossa execução dos 3 serviços com código fluente e legível.

O métodothenAcceptAsync executará a função fornecidaafter enquanto oCompletionStage fornecido concluiu a execução (ou lançou uma exceção).

Cada chamada sucessiva é sem bloqueio, fazendo um uso criterioso dos recursos do sistema.

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 em uma orquestração de várias etapas (ou uma única chamada de serviço).

4.2. Observable en JAX-RS

Para usar o componenteObservable RxJava, devemos primeiro registrar oRxObservableInvokerProvider provider (e não o “ObservableRxInvokerProvider” as declarado no documento de especificação de Jersey) no cliente:

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

Em seguida, substituímos o invocador padrão:

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

A partir deste ponto, nóscan use standard Observable semantics to orchestrate the processing flow:

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

4.3. Flowable en JAX-RS

A semântica para usar RxJavaFlowable  é semelhante à deObservable.  Registramos o provedor apropriado:

client.register(RxFlowableInvokerProvider.class);

Em seguida, fornecemos oRxFlowableInvoker:

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

Em seguida, podemos usar aFlowable API normal.

5. Conclusão

A especificação JAX-RS fornece um bom número de opções que produzem execução limpa e sem bloqueio de chamadas REST.

A sinterfaceCompletionStage , em particular, fornece um conjunto robusto de métodos que cobrem uma variedade de cenários de orquestração de serviço, bem como oportunidades para fornecerExecutors  para um controle mais refinado do gerenciamento de encadeamento.

Você pode verificar o código deste artigoover on Github.