Реактивный клиентский API JAX-RS

Реактивный клиентский API JAX-RS

1. Вступление

В этом руководстве мы рассмотрим поддержку JAX-RS для реактивного (Rx) программирования с использованием API Джерси. В этой статье предполагается, что читатель знаком с клиентским API REST Jersey.

Некоторое знакомство сreactive programming concepts будет полезно, но не обязательно.

2. зависимости

Во-первых, нам нужны стандартные зависимости клиентской библиотеки Джерси:


    org.glassfish.jersey.core
    jersey-client
    2.27


    org.glassfish.jersey.inject
    jersey-hk2
    2.27

Эти зависимости дают нам базовую поддержку реактивного программирования JAX-RS. Текущие версииjersey-client иjersey-hk2 доступны на Maven Central.

Для поддержки стороннего реактивного фреймворка мы будем использовать следующие расширения:


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

Приведенная выше зависимость обеспечивает поддержкуObservable; RxJava для более новогоFlowablewe RxJava2 с использованием следующего расширения:


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

Зависимости дляrxjava иrxjava2 также доступны в Maven Central.

3. Зачем нужны реактивные клиенты JAX-RS

Допустим, у нас есть три REST API:

  • id-service предоставляет список длинных идентификаторов пользователей

  • name-service предоставляет имя пользователя для данного идентификатора пользователя

  • hash-service вернет хеш идентификатора пользователя и имени пользователя

Мы создаем клиента для каждой из услуг:

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

Это надуманный пример, но он работает для целей нашей иллюстрации. Спецификация JAX-RS поддерживает как минимум три подхода для совместного использования этих сервисов:

  • Синхронный (блокировка)

  • Асинхронный (неблокирующий)

  • Реактивный (функциональный, неблокирующий)

3.1. Проблема с синхронным вызовом клиента Джерси

При обычном подходе к использованию этих сервисов мы будем использоватьid-service для получения идентификаторов пользователей, а затем последовательно вызывать APIname-service иhash-service для каждого возвращенного идентификатора.

With this approach, each call blocks the running thread until the request is fulfilled, тратя в общей сложности много времени на выполнение комбинированного запроса. Это явно менее чем удовлетворительно в любом нетривиальном случае использования.

3.2. Проблема с асинхронным вызовом клиента Джерси

Более сложный подход - использовать механизмInvocationCallback , поддерживаемый JAX-RS. В самой простой форме мы передаем обратный вызов методуget, чтобы определить, что происходит, когда данный вызов API завершается.

Хотя теперь мы получаем истинное асинхронное выполнение (with some limitations on thread efficiency), легко увидеть, какthis style of code can get unreadable and unwieldy происходит в любых, кроме тривиальных, сценариях. JAX-RS specification специально выделяет этот сценарий какPyramid 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");
}

Итак, мы получили асинхронный, эффективный по времени код, но:

  • трудно читать

  • каждый вызов порождает новую тему

Обратите внимание, что мы используемCountDownLatch во всех примерах кода, чтобы ждать, пока все ожидаемые значения будут доставленыhash-service.. Мы делаем это, чтобы мы могли утверждать, что код работает в модульном тесте. проверив, что все ожидаемые значения действительно были доставлены.

Обычный клиент не будет ждать, а будет делать то, что должно быть сделано с результатом в обратном вызове, чтобы не блокировать поток.

3.3. Функциональное, реактивное решение

Функциональный и реактивный подход даст нам:

  • Отличная читаемость кода

  • Свободный стиль кодирования

  • Эффективное управление потоками

JAX-RS поддерживает эти цели в следующих компонентах:

  • CompletionStageRxInvoker  поддерживает интерфейсCompletionStage в качестве реактивного компонента по умолчанию

  • RxObservableInvokerProvider поддерживаетObservable  RxJava

  • RxFlowableInvokerProvider поддерживаетFlowable RxJava

Также естьAPI для добавления поддержки других реактивных библиотек.

4. Поддержка реактивных компонентов JAX-RS

4.1. CompletionStage in JAX-RS

ИспользуяCompletionStage and, его конкретную реализацию -CompletableFuture – we можно написать элегантную, неблокирующую и плавную оркестровку вызовов службы.

Начнем с получения идентификаторов пользователей:

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. Мы используем функциюexceptionally, чтобы плавно определить наш сценарий обработки исключений.

Отсюда мы можем аккуратно организовать вызовы, чтобы получить имя пользователя из службы имен, а затем хэшировать комбинацию имени и идентификатора пользователя:

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

В приведенном выше примере мы сочетаем выполнение трех сервисов с помощью свободного и удобочитаемого кода.

МетодthenAcceptAsync выполнит предоставленную функциюafter , если данныйCompletionStage завершил выполнение (или вызвал исключение).

Каждый последующий вызов является неблокирующим, что позволяет рационально использовать системные ресурсы.

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 в многоэтапной оркестровке (или одном сервисном вызове).

4.2. Observable in JAX-RS

Чтобы использовать компонентObservable RxJava, мы должны сначала зарегистрировать sproviderRxObservableInvokerProvider (а не «ObservableRxInvokerProvider” as, как указано в документе спецификации Джерси)» на клиенте:

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

Затем мы переопределяем по умолчанию invoker:

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

С этого момента мыcan 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

Семантика использования RxJavaFlowable  аналогична семантикеObservable. . Регистрируем соответствующего провайдера:

client.register(RxFlowableInvokerProvider.class);

Затем мы поставляемRxFlowableInvoker:

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

После этого мы можем использовать обычныйFlowable API.

5. Заключение

Спецификация JAX-RS предоставляет множество опций, которые обеспечивают чистое неблокирующее выполнение вызовов REST.

СинтерфейсCompletionStage , в частности, предоставляет надежный набор методов, охватывающих множество сценариев оркестровки служб, а также возможности предоставления пользовательскогоExecutors  для более детального контроля управления потоками.

Вы можете проверить код этой статьиover on Github.