1. Обзор
В этом руководстве мы собираемся изучить WebClient
— реактивный веб-клиент, представленный в Spring 5.
Мы также рассмотрим WebTestClient,
WebClient ,
предназначенный для использования в тестах.
2. Что такое веб- клиент
?
Проще говоря, WebClient
— это интерфейс, представляющий основную точку входа для выполнения веб-запросов.
Он был создан как часть модуля Spring Web Reactive и заменит классический RestTemplate
в этих сценариях. Кроме того, новый клиент представляет собой реактивное неблокирующее решение, работающее по протоколу HTTP/1.1.
Важно отметить, что хотя на самом деле это неблокирующий клиент и он принадлежит библиотеке spring-webflux
, решение предлагает поддержку как синхронных, так и асинхронных операций, что делает его подходящим также для приложений, работающих в стеке сервлетов. .
Этого можно добиться, заблокировав операцию до получения результата. Конечно, эта практика не рекомендуется, если мы работаем с реактивным стеком.
Наконец, интерфейс имеет единственную реализацию — класс DefaultWebClient
, с которым мы будем работать.
3. Зависимости
Поскольку мы используем приложение Spring Boot, все, что нам нужно, — это зависимость spring-boot-starter-webflux
для получения поддержки Spring Framework Reactive Web.
3.1. Строительство с Maven
Добавим в файл pom.xml
следующие зависимости :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
3.2. Создание с помощью Gradle
С Gradle нам нужно добавить следующие записи в файл build.gradle
:
dependencies {
compile 'org.springframework.boot:spring-boot-starter-webflux'
}
4. Работа с веб- клиентом
Чтобы правильно работать с клиентом, нам необходимо знать, как:
- создать экземпляр
- обратиться с просьбой
- обрабатывать ответ
4.1. Создание экземпляра веб -клиента
Есть три варианта на выбор. Первый — создание объекта WebClient
с настройками по умолчанию:
WebClient client = WebClient.create();
Второй вариант — инициировать экземпляр WebClient
с заданным базовым URI:
WebClient client = WebClient.create("http://localhost:8080");
Третий вариант (и самый продвинутый) — это сборка клиента с использованием класса DefaultWebClientBuilder
, который позволяет выполнять полную настройку:
WebClient client = WebClient.builder()
.baseUrl("http://localhost:8080")
.defaultCookie("cookieKey", "cookieValue")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
.build();
4.2. Создание экземпляра WebClient с тайм-аутами
Часто время ожидания HTTP по умолчанию в 30 секунд слишком медленно для наших нужд, чтобы настроить это поведение, мы можем создать экземпляр HttpClient
и настроить наш WebClient
для его использования.
Мы можем:
- установите время ожидания соединения с помощью опции
ChannelOption.CONNECT_TIMEOUT_MILLIS
- установите таймауты чтения и записи с помощью
ReadTimeoutHandler
иWriteTimeoutHandler
соответственно - настроить время ожидания ответа с помощью директивы
responseTimeout
Как мы уже говорили, все это должно быть указано в экземпляре HttpClient
, который мы настроим:
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofMillis(5000))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Обратите внимание, что хотя мы можем вызвать тайм
-аут и для нашего клиентского запроса, это тайм-аут сигнала, а не HTTP-соединения, чтения/записи или тайм-аут ответа; это тайм-аут для издателя Mono/Flux.
4.3. Подготовка запроса – определение метода
Во-первых, нам нужно указать HTTP-метод запроса, вызвав метод (метод HttpMethod)
:
UriSpec<RequestBodySpec> uriSpec = client.method(HttpMethod.POST);
Или вызов его методов быстрого доступа, таких как get
, post
и delete
:
UriSpec<RequestBodySpec> uriSpec = client.post();
Примечание: хотя может показаться, что мы повторно используем переменные спецификации запроса ( WebClient.UriSpec
, WebClient.RequestBodySpec
, WebClient.RequestHeadersSpec
, WebClient.ResponseSpec
)
, это просто для простоты представления различных подходов. Эти директивы не следует повторно использовать для разных запросов, они извлекают ссылки, и поэтому последние операции изменят определения, которые мы сделали на предыдущих шагах.
4.4. Подготовка запроса — определение URL
Следующим шагом является предоставление URL. Опять же, у нас есть разные способы сделать это.
Мы можем передать его API uri в виде
строки:
RequestBodySpec bodySpec = uriSpec.uri("/resource");
Использование функции UriBuilder
:
RequestBodySpec bodySpec = uriSpec.uri(
uriBuilder -> uriBuilder.pathSegment("/resource").build());
Или как экземпляр java.net.URL :
RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));
Имейте в виду, что если бы мы определили базовый URL-адрес по умолчанию для WebClient
, этот последний метод переопределил бы это значение.
4.5. Подготовка запроса – определение тела
Затем мы можем установить тело запроса, тип контента, длину, файлы cookie или заголовки, если нам нужно.
Например, если мы хотим установить тело запроса, есть несколько доступных способов. Вероятно, наиболее распространенным и простым вариантом является использование метода bodyValue
:
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");
Или предоставив Publisher
(и тип элементов, которые будут опубликованы) методу body :
RequestHeadersSpec<?> headersSpec = bodySpec.body(
Mono.just(new Foo("name")), Foo.class);
В качестве альтернативы мы можем использовать служебный класс BodyInserters .
Например, давайте посмотрим, как мы можем заполнить тело запроса с помощью простого объекта, как мы это сделали с методом bodyValue
:
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromValue("data"));
Точно так же мы можем использовать метод BodyInserters#fromPublisher
, если используем экземпляр Reactor:
RequestHeadersSpec headersSpec = bodySpec.body(
BodyInserters.fromPublisher(Mono.just("data")),
String.class);
Этот класс также предлагает другие интуитивно понятные функции для более сложных сценариев. Например, если нам нужно отправить составные запросы:
LinkedMultiValueMap map = new LinkedMultiValueMap();
map.add("key1", "value1");
map.add("key2", "value2");
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromMultipartData(map));
Все эти методы создают экземпляр BodyInserter
, который затем можно представить как тело
запроса.
BodyInserter — это интерфейс, отвечающий за заполнение тела
ReactiveHttpOutputMessage
заданным выходным сообщением и контекстом, используемым во время вставки.
Издатель
— это реактивный компонент, отвечающий за предоставление потенциально неограниченного количества упорядоченных элементов. Это тоже интерфейс, и самые популярные реализации — Mono
и Flux.
4.6. Подготовка запроса – определение заголовков
После того, как мы установили тело, мы можем установить заголовки, файлы cookie и допустимые типы мультимедиа. Значения будут добавлены к тем, которые уже были установлены при создании экземпляра клиента.
Кроме того, имеется дополнительная поддержка наиболее часто используемых заголовков, таких как «If-None-Match», «If-Modified-Since», «Accept»
и «Accept-Charset».
Вот пример того, как можно использовать эти значения:
ResponseSpec responseSpec = headersSpec.header(
HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
.acceptCharset(StandardCharsets.UTF_8)
.ifNoneMatch("*")
.ifModifiedSince(ZonedDateTime.now())
.retrieve();
4.7. Получение ответа
Завершающий этап — отправка запроса и получение ответа. Мы можем добиться этого, используя метод exchangeToMono/exchangeToFlux
или метод получения
.
Методы exchangeToMono
и exchangeToFlux
позволяют получить доступ к ClientResponse
вместе с его статусом и заголовками:
Mono<String> response = headersSpec.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(String.class);
} else if (response.statusCode().is4xxClientError()) {
return Mono.just("Error response");
} else {
return response.createException()
.flatMap(Mono::error);
}
});
В то время как метод извлечения
— это кратчайший путь к получению тела напрямую:
Mono<String> response = headersSpec.retrieve()
.bodyToMono(String.class);
Важно обратить внимание на ResponseSpec.
bodyToMono
, который выдает исключение WebClientException
, если код состояния равен 4xx
(ошибка клиента) или 5xx
(ошибка сервера).
5. Работа с WebTestClient
WebTestClient — это
основная точка входа для тестирования конечных точек сервера WebFlux. Он имеет API, очень похожий на WebClient
, и делегирует большую часть работы внутреннему экземпляру WebClient
, уделяя основное внимание предоставлению тестового контекста. Класс DefaultWebTestClient
представляет собой реализацию единого интерфейса.
Клиент для тестирования может быть привязан к реальному серверу или работать с конкретными контроллерами или функциями.
5.1. Привязка к серверу
Чтобы выполнить сквозные интеграционные тесты с реальными запросами к работающему серверу, мы можем использовать метод bindToServer
:
WebTestClient testClient = WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build();
5.2. Привязка к роутеру
Мы можем протестировать конкретную RouterFunction
, передав ее методу bindToRouterFunction
:
RouterFunction function = RouterFunctions.route(
RequestPredicates.GET("/resource"),
request -> ServerResponse.ok().build()
);
WebTestClient
.bindToRouterFunction(function)
.build().get().uri("/resource")
.exchange()
.expectStatus().isOk()
.expectBody().isEmpty();
5.3. Привязка к веб-обработчику
Такого же поведения можно добиться с помощью метода bindToWebHandler
, который принимает экземпляр WebHandler
:
WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();
5.4. Привязка к контексту приложения
Более интересная ситуация возникает, когда мы используем метод bindToApplicationContext
. Он принимает ApplicationContext
и анализирует контекст для bean-компонентов контроллера и конфигураций @EnableWebFlux
.
Если мы внедрим экземпляр ApplicationContext
, простой фрагмент кода может выглядеть так:
@Autowired
private ApplicationContext context;
WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
.build();
5.5. Привязка к контроллеру
Более коротким подходом было бы предоставление массива контроллеров, которые мы хотим протестировать, с помощью метода bindToController
. Предполагая, что у нас есть класс Controller
и мы внедрили его в нужный класс, мы можем написать:
@Autowired
private Controller controller;
WebTestClient testClient = WebTestClient.bindToController(controller).build();
5.6. Сделать запрос
После создания объекта WebTestClient
все последующие операции в цепочке будут аналогичны WebClient
, пока не появится метод exchange
(один из способов получения ответа), который предоставляет интерфейс WebTestClient.ResponseSpec
для работы с полезными методами, такими как expectStatus
, expectBody.
и ожидаемый заголовок
:
WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build()
.post()
.uri("/resource")
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals("Content-Type", "application/json")
.expectBody().jsonPath("field").isEqualTo("value");
6. Заключение
В этой статье мы рассмотрели WebClient,
новый усовершенствованный механизм Spring для выполнения запросов на стороне клиента.
Мы также рассмотрели преимущества, которые он предоставляет, пройдя настройку клиента, подготовив запрос и обработав ответ.
Все фрагменты кода, упомянутые в статье, можно найти в нашем репозитории GitHub .