«1. Введение

В этом уроке мы сравним две реализации веб-клиента Spring — RestTemplate и новую реактивную альтернативу Spring 5 WebClient.

2. Блокирующий и неблокирующий клиент

В веб-приложениях обычно требуется выполнять HTTP-вызовы к другим службам. Поэтому нам нужен веб-клиент.

2.1. Блокирующий клиент RestTemplate

Долгое время Spring предлагал RestTemplate в качестве абстракции веб-клиента. Под капотом RestTemplate использует Java Servlet API, основанный на модели «поток на запрос».

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

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

Рано или поздно запросы, ожидающие результатов, накапливаются. Следовательно, приложение создаст много потоков, которые исчерпают пул потоков или займут всю доступную память. Мы также можем столкнуться со снижением производительности из-за частого переключения контекста ЦП (потока).

2.2. WebClient Non-Blocking Client

С другой стороны, WebClient использует асинхронное неблокирующее решение, предоставляемое инфраструктурой Spring Reactive.

В то время как RestTemplate использует поток вызывающей стороны для каждого события (вызов HTTP), WebClient создаст что-то вроде «задачи» для каждого события. За кулисами Reactive framework поставит эти «задачи» в очередь и выполнит их только тогда, когда будет доступен соответствующий ответ.

Reactive framework использует архитектуру, управляемую событиями. Он предоставляет средства для создания асинхронной логики через API Reactive Streams. В результате реактивный подход может обрабатывать больше логики при использовании меньшего количества потоков и системных ресурсов по сравнению с синхронным/блокирующим методом.

WebClient является частью библиотеки Spring WebFlux. Поэтому мы можем дополнительно написать клиентский код, используя функциональный, гибкий API с реактивными типами (Mono и Flux) в качестве декларативной композиции.

3. Сравнительный пример

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

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

Для целей этой статьи давайте реализуем две конечные точки REST, одну с помощью RestTemplate, а другую с помощью WebClient. Их задача — вызвать еще один медленный веб-сервис REST, который возвращает список твитов.

Для начала нам понадобится стартовая зависимость Spring Boot WebFlux:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Кроме того, вот наша конечная точка медленного сервиса REST:

@GetMapping("/slow-service-tweets")
private List<Tweet> getAllTweets() {
    Thread.sleep(2000L); // delay
    return Arrays.asList(
      new Tweet("RestTemplate rules", "@user1"),
      new Tweet("WebClient is better", "@user2"),
      new Tweet("OK, both are useful", "@user1"));
}

3.1. Использование RestTemplate для вызова медленной службы

Теперь давайте реализуем другую конечную точку REST, которая будет вызывать нашу медленную службу через веб-клиент.

Во-первых, мы будем использовать RestTemplate:

@GetMapping("/tweets-blocking")
public List<Tweet> getTweetsBlocking() {
    log.info("Starting BLOCKING Controller!");
    final String uri = getSlowServiceUri();

    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<List<Tweet>> response = restTemplate.exchange(
      uri, HttpMethod.GET, null,
      new ParameterizedTypeReference<List<Tweet>>(){});

    List<Tweet> result = response.getBody();
    result.forEach(tweet -> log.info(tweet.toString()));
    log.info("Exiting BLOCKING Controller!");
    return result;
}

Когда мы вызываем эту конечную точку, из-за синхронного характера RestTemplate код будет блокироваться в ожидании ответа от нашего медленного сервиса. Только когда ответ будет получен, остальная часть кода в этом методе будет выполнена. В логах увидим:

Starting BLOCKING Controller!
Tweet(text=RestTemplate rules, [email protected])
Tweet(text=WebClient is better, [email protected])
Tweet(text=OK, both are useful, [email protected])
Exiting BLOCKING Controller!

3.2. Использование WebClient для вызова медленного сервиса

Во-вторых, давайте воспользуемся WebClient для вызова медленного сервиса:

@GetMapping(value = "/tweets-non-blocking", 
            produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Tweet> getTweetsNonBlocking() {
    log.info("Starting NON-BLOCKING Controller!");
    Flux<Tweet> tweetFlux = WebClient.create()
      .get()
      .uri(getSlowServiceUri())
      .retrieve()
      .bodyToFlux(Tweet.class);

    tweetFlux.subscribe(tweet -> log.info(tweet.toString()));
    log.info("Exiting NON-BLOCKING Controller!");
    return tweetFlux;
}

В этом случае WebClient возвращает издателя Flux, и выполнение метода завершается. Как только результат будет доступен, издатель начнет рассылать твиты своим подписчикам. Обратите внимание, что клиент (в данном случае веб-браузер), вызывающий эту конечную точку /tweets-non-blocking, также будет подписан на возвращаемый объект Flux.

На этот раз посмотрим на журнал:

Starting NON-BLOCKING Controller!
Exiting NON-BLOCKING Controller!
Tweet(text=RestTemplate rules, [email protected])
Tweet(text=WebClient is better, [email protected])
Tweet(text=OK, both are useful, [email protected])

Обратите внимание, что этот метод конечной точки завершился до того, как был получен ответ.

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

«В этой статье мы рассмотрели два разных способа использования веб-клиентов в Spring.


RestTemplate использует Java Servlet API и поэтому является синхронным и блокирующим. Напротив, WebClient является асинхронным и не будет блокировать исполняемый поток, ожидая ответа. Только когда ответ будет готов, будет произведено уведомление.

RestTemplate по-прежнему будет использоваться. В некоторых случаях неблокирующий подход использует гораздо меньше системных ресурсов по сравнению с блокирующим. Следовательно, в таких случаях WebClient является предпочтительным выбором.

Все фрагменты кода, упомянутые в статье, можно найти на GitHub.