Spring in Action Covers Spring 5-1--11 — страница 61 из 63

   return WebClient.create("http://localhost:8080");

}

Затем в любом месте, где вам нужно сделать запросы с использованием этого базового URI, WebClient компонент может быть внедрен и использован следующим образом:

@Autowired

WebClient webClient;

public Mono getIngredientById(String ingredientId) {

   Mono ingredient = webClient

      .get()

      .uri("/ingredients/{id}", ingredientId)

      .retrieve()

      .bodyToMono(Ingredient.class);

   ingredient.subscribe(i -> { ... })

}

Поскольку WebClient уже был создан, вы можете сразу приступить к работе, вызвав get(). Что касается URI, то при вызове uri() необходимо указать только путь относительно базового URI.

ТАЙМ-АУТ ДЛЯ ДЛИТЕЛЬНЫХ ЗАПРОСОВ

Единственное, на что вы можете рассчитывать, - это то, что сети не всегда надежны или так быстры, как вы ожидаете. Или, может быть, удаленный сервер вяло обрабатывает запрос. В идеале, запрос к удаленной службе будет возвращен в разумные сроки. Но если нет, было бы здорово, если бы клиент не замер в ожидании ответа слишком долго.

Чтобы избежать задержки клиентских запросов медленной сетью или службой, можно использовать метод timeout() из Flux или Mono, чтобы ограничить время ожидания публикации данных. В качестве примера рассмотрим, как можно использовать функцию timeout() при извлечении данных ингредиента:

Flux ingredients = WebClient.create()

   .get()

   .uri("http://localhost:8080/ingredients")

   .retrieve()

   .bodyToFlux(Ingredient.class);

ingredients

   .timeout(Duration.ofSeconds(1))

   .subscribe(

      i -> { ... },

      e -> {

         // ошибка тайм-аута обработки

})

Как вы можете видеть, прежде чем подписаться на Flux, вы вызвали timeout(), указав продолжительность 1 секунда. Если запрос может быть выполнен менее чем за 1 секунду, то нет никаких проблем. Но если запрос занимает больше 1 секунды, он истекает и вызывается обработчик ошибок, указанный в качестве второго параметра для subscribe().

11.4.2 Отправка ресурсов

Отправка данных с помощью WebClient не сильно отличается от получения данных. В качестве примера предположим, что у вас есть Mono и вы хотите отправить запрос POST с Ingredient, опубликованным Mono, в URI с относительным путем /ingredients. Все, что вам нужно сделать, это использовать метод post() вместо get() и указать, что Mono будет использоваться для заполнения тела запроса путем вызова body():

Mono ingredientMono = ...;

Mono result = webClient

   .post()

   .uri("/ingredients")

   .body(ingredientMono, Ingredient.class)

   .retrieve()

   .bodyToMono(Ingredient.class);

result.subscribe(i -> { ... })

Если у вас нет Mono или Flux для отправки, но вместо этого есть объект домена, вы можете использовать syncBody(). Например, предположим, что вместо Mono у вас есть Ingredient, который вы хотите отправить в теле запроса:

Ingedient ingredient = ...;

Mono result = webClient

   .post()

   .uri("/ingredients")

   .syncBody(ingredient)

   .retrieve()

   .bodyToMono(Ingredient.class);

result.subscribe(i -> { ... })

Если вместо запроса POST вы хотите обновить Ingredient с помощью запроса PUT, вы вызываете put() вместо post() и соответственно корректируете путь URI:

Mono result = webClient

   .put()

   .uri("/ingredients/{id}", ingredient.getId())

   .syncBody(ingredient)

   .retrieve()

   .bodyToMono(Void.class)

   .subscribe();

Запросы PUT обычно имеют пустые полезные нагрузки ответа, поэтому вы должны указать bodyToMono() вернуть Mono типа Void. При подписке на этот Mono, запрос будет отправлен.

11.4.3 Удаление ресурсов

WebClient также позволяет удалять ресурсы с помощью метода delete(). Например, следующий код удаляет ингредиент для переданого ID:

Mono result = webClient

   .delete()

   .uri("/ingredients/{id}", ingredientId)

   .retrieve()

   .bodyToMono(Void.class)

   .subscribe();

Как и в случае PUT запросов, запросы DELETE обычно не имеют полезной нагрузки. Вы снова возвращаетесь и подписываетесь на Mono, чтобы отправить запрос.

11.4.4 Обработка ошибок

До сих пор все примеры WebClient предполагали удачное завершение; не было ответов с кодами состояния 400 или 500. Если будет возвращен любой из статусов ошибок, WebClient зарегистрирует ошибку; в противном случае, он будет молча игнорировать это.

Если вам нужно обработать такие ошибки, то вызов onStatus() можно использовать для указания того, как должны обрабатываться различные коды состояния HTTP. onStatus() принимает две функции: функцию предиката, которая используется для соответствия статусу HTTP, и функцию, которая, учитывая объект ClientResponse, возвращает Mono.

Чтобы продемонстрировать, как onStatus() может быть использован для создания пользовательского обработчика ошибок, рассмотрим следующее использование WebClient, целью которого является извлечение ингредиента с учетом его идентификатора:

Mono ingredientMono = webClient

   .get()

   .uri("http://localhost:8080/ingredients/{id}", ingredientId)

   .retrieve()

   .bodyToMono(Ingredient.class);

До тех пор значение в идентификаторе ингредиента соответствует известному ресурсу ингредиента, то результирующий Mono опубликует объект ингредиента, когда он будет подписан. Но что произойдет, если не будет подходящего ингредиента?

При подписке на Mono или Flux, который может закончиться ошибкой, важно зарегистрировать получателя ошибок, а также получателя данных в вызове метода subscribe():

ingredientMono.subscribe(

ingredient -> {

// обрабатывать данные ингредиента

...

},

error-> {

// разобраться с ошибкой

...

});

Если ресурс ингредиента найден, то первая лямбда (потребитель данных), предоставленная subscribe(), вызывается с соответствующим объектом ингредиента. Но если он не найден, то запрос отвечает кодом состояния HTTP 404 (не найден), что приводит к тому, что вторая лямбда (потребитель ошибок) по умолчанию получает исключение WebClientResponseException.

Самая большая проблема с WebClientResponseException заключается в том, что он довольно неспецифичен относительно того, что могло пойти не так, чтобы вызвать сбой Mono. Его название предполагает, что была ошибка в ответе на запрос, сделанный WebClient, но вам нужно изучить исключение WebClientResponseException, чтобы узнать, что пошло не так. И в любом случае было бы неплохо, если бы исключение, предоставленное потребителю ошибок, было более специфичным для домена, а не для WebClient.

Добавляя пользовательский обработчик ошибок, вы можете предоставить код, который преобразует код состояния в Throwable по вашему выбору. Предположим, что вы хотите, чтобы неудачный запрос для ресурса ингредиента привел к тому, что Mono завершилось ошибкой с UnknownIngredientException. Вы можете добавить вызов onStatus() после вызова retrieve(), чтобы добиться этого:

Mono ingredientMono = webClient

   .get()

   .uri("http://localhost:8080/ingredients/{id}", ingredientId)

   .retrieve()

   .onStatus(HttpStatus::is4xxClientError,

   response -> Mono.just(new UnknownIngredientException()))

   .bodyToMono(Ingredient.class);

Первый аргумент в вызове onStatus() - это предикат, задающий состояние Http и возвращающий значение true, если требуется обработать код состояния. И если код состояния совпадает, то ответ будет возвращен функции во втором аргументе для обработки, как он считает нужным, в конечном итоге возвращая Mono типа Throwable.

В этом примере, если код состояния представляет собой код состояния уровня 400 (например, ошибка клиента), Mono будет возвращен с UnknownIngredientException.  Это приводит к тому, что ingredientMono терпит неудачу с этим исключением.

Обратите внимание, что HttpStatus::is4xxClientError - это ссылка на метод is4xxClientError метода HttpStatus. Именно этот метод будет вызван для данного HttpStatus объекта. Если вы хотите, вы можете использовать другой метод в HttpStatus в качестве ссылки на метод; или вы можете предоставить свою собственную функцию в виде ссылки на лямбду или метод, которая возвращает boolean.

Например, вы можете получить еще более точную обработку ошибок, специально проверив состояние HTTP 404 (НЕ НАЙДЕНО), изменив вызов onStatus(), чтобы он выглядел следующим образом:

Mono ingredientMono = webClient

   .get()

   .uri("http://localhost:8080/ingredients/{id}", ingredientId)

   .retrieve()

   .onStatus(status -> status == HttpStatus.NOT_FOUND,

      response -> Mono.just(new UnknownIngredientException()))

   .bodyToMono(Ingredient.class);

Стоит также отметить, что вы можете иметь столько вызовов onStatus(), сколько вам нужно для обработки любых кодов состояния HTTP, которые могут возвращаться в ответе.

11.4.5 Обмен запросами

До этого момента вы использовали метод retrieve() для обозначения отправки запроса при работе с WebClient. В этих случаях метод retrieve() возвращает объект типа ResponseSpec, с помощью которого можно обрабатывать ответ с вызовами таких методов, как onStatus(), bodyToFlux() и bodyToMono(). Работа со спецификацией ответа хороша для простых случаев, но она несколько ограничена. Например, если вам нужен доступ к заголовкам ответа или значениям cookie, ResponseSpec вам не подойдет.

Когда ResponseSpec заканчивается, вы можете попробовать вызвать exchange() вместо retrieve(). Метод exchange() возвращает Mono типа ClientResponse, к которому можно применить реактивные операции для проверки и использования данных из всего ответа, включая полезную нагрузку,