1. Обзор
Многие фреймворки и проекты внедряют реактивное программирование и асинхронную обработку запросов . Следовательно, Spring 5 представил реактивную реализацию WebClient
как часть среды WebFlux .
В этом руководстве мы увидим, как реактивно использовать конечные точки REST API с помощью WebClient
.
2. Конечные точки REST API
Для начала давайте определим пример REST API со следующими конечными точками GET :
/products
— получить все товары/products/{id}
— получить товар по ID/products/{id}/attributes/{attributeId}
— получить атрибут товара по id/products/?name={name}&deliveryDate={deliveryDate}&color={color}
– найти товары/products/?tag[]={tag1}&tag[]={tag2}
– получить товары по тегам/products/?category={category1}&category={category2}
– получить товары по категориям
Итак, мы только что определили несколько разных URI. Через мгновение мы выясним, как создавать и отправлять каждый тип URI с помощью WebClient
.
Обратите внимание, что URI для получения товаров по тегам и по категориям содержат массивы в качестве параметров запроса. Однако синтаксис отличается. Поскольку нет строгого определения того, как массивы должны быть представлены в URI . В первую очередь это зависит от реализации на стороне сервера. Соответственно, мы рассмотрим оба случая.
3. Настройка веб- клиента
Сначала нам нужно создать экземпляр WebClient
. В этой статье мы будем использовать фиктивный объект , поскольку нам нужно только убедиться, что запрошен действительный URI.
Давайте определим клиент и связанные с ним фиктивные объекты:
exchangeFunction = mock(ExchangeFunction.class);
ClientResponse mockResponse = mock(ClientResponse.class);
when(mockResponse.bodyToMono(String.class))
.thenReturn(Mono.just("test"));
when(exchangeFunction.exchange(argumentCaptor.capture()))
.thenReturn(Mono.just(mockResponse));
webClient = WebClient
.builder()
.baseUrl("https://example.com/api")
.exchangeFunction(exchangeFunction)
.build();
Кроме того, мы передали базовый URL-адрес, который будет добавляться ко всем запросам, сделанным клиентом.
Наконец, чтобы убедиться, что конкретный URI был передан базовому экземпляру ExchangeFunction
, воспользуемся следующим вспомогательным методом:
private void verifyCalledUrl(String relativeUrl) {
ClientRequest request = argumentCaptor.getValue();
assertEquals(String.format("%s%s", BASE_URL, relativeUrl), request.url().toString());
verify(this.exchangeFunction).exchange(request);
verifyNoMoreInteractions(this.exchangeFunction);
}
Класс WebClientBuilder
имеет метод uri()
, который предоставляет экземпляр UriBuilder
в качестве аргумента. Как правило, вызов API обычно выполняется следующим образом:
webClient.get()
.uri(uriBuilder -> uriBuilder
//... building a URI
.build())
.retrieve()
.bodyToMono(String.class)
.block();
В этом руководстве мы будем широко использовать UriBuilder
для создания URI. Стоит отметить, что мы можем создать URI любым другим способом, а затем просто передать сгенерированный URI как строку.
4. Компонент пути URI
Компонент пути состоит из последовательности сегментов пути, разделенных косой чертой (/) . Во-первых, давайте начнем с простого случая, когда в URI нет переменных segments /products
:
webClient.get()
.uri("/products")
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products");
В этом случае мы можем просто передать строку
в качестве аргумента.
Далее возьмем конечную точку /products/{id}
и создадим соответствующий URI:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/{id}")
.build(2))
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/2");
Из приведенного выше кода видно, что фактические значения сегментов передаются в метод build()
.
Теперь аналогичным образом мы можем создать URI с несколькими сегментами пути для конечной точки /products/{id}/attributes/{attributeId}
:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/{id}/attributes/{attributeId}")
.build(2, 13))
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/2/attributes/13");
URI может иметь столько сегментов пути, сколько требуется. Конечно, если конечная длина URI не превышает ограничений. Наконец, не забудьте сохранить правильный порядок фактических значений сегментов, передаваемых в метод build()
.
5. Параметры запроса URI
Обычно параметр запроса представляет собой простую пару «ключ-значение», например title=ForEach
. Давайте посмотрим, как создавать такие URI.
5.1. Параметры с одним значением
Начнем с параметров с одним значением и возьмем конечную точку /products/?name={name}&deliveryDate={deliveryDate}&color={color}
. Чтобы установить параметр запроса, мы вызываем метод queryParam () интерфейса
UriBuilder
:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("name", "AndroidPhone")
.queryParam("color", "black")
.queryParam("deliveryDate", "13/04/2019")
.build())
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");
Здесь мы добавили три параметра запроса и сразу присвоили фактические значения. Кроме того, вместо точных значений также можно оставить заполнители:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("name", "{title}")
.queryParam("color", "{authorId}")
.queryParam("deliveryDate", "{date}")
.build("AndroidPhone", "black", "13/04/2019"))
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13%2F04%2F2019");
В частности, это может быть полезно при передаче объекта построителя дальше по цепочке. Обратите внимание на одно важное различие между двумя приведенными выше фрагментами кода .
Обращая внимание на ожидаемые URI, мы видим, что они были закодированы по-разному . В частности, в последнем примере экранировался символ косой черты (/) . Вообще говоря, RFC3986 не требует кодирования косых черт в запросе.
Однако для некоторых серверных приложений такое преобразование может потребоваться. Поэтому позже в этом руководстве мы увидим, как изменить это поведение.
5.2. Параметры массива
Точно так же нам может понадобиться передать массив значений. Тем не менее, нет строгих правил для передачи массивов в строке запроса. Поэтому представление массива в строке запроса отличается от проекта к проекту и обычно зависит от базовых фреймворков . Мы рассмотрим наиболее широко используемые форматы.
Начнем с конечной точки /products/?tag[]={tag1}&tag[]={tag2}
:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("tag[]", "Snapdragon", "NFC")
.build())
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/?tag%5B%5D=Snapdragon&tag%5B%5D=NFC");
Как мы видим, окончательный URI содержит несколько параметров тега, за которыми следуют закодированные квадратные скобки. Метод queryParam()
принимает переменные аргументы в качестве значений, поэтому нет необходимости вызывать метод несколько раз.
В качестве альтернативы мы можем опустить квадратные скобки и просто передать несколько параметров запроса с одним и тем же ключом , но разными значениями — /products/?category={category1}&category={category2}
:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("category", "Phones", "Tablets")
.build())
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/?category=Phones&category=Tablets");
В заключение, есть еще один широко используемый метод кодирования массива — передача значений, разделенных запятыми. Преобразуем наш предыдущий пример в значения, разделенные запятыми:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("category", String.join(",", "Phones", "Tablets"))
.build())
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/?category=Phones,Tablets");
Таким образом, мы просто используем метод join()
класса String
для создания строки, разделенной запятыми. Конечно, мы можем использовать любой другой разделитель, ожидаемый приложением.
6. Режим кодирования
Помните, как мы упоминали о кодировании URL-адресов ранее.
Если поведение по умолчанию не соответствует нашим требованиям, мы можем изменить его. Нам нужно предоставить реализацию UriBuilderFactory
при создании экземпляра WebClient .
В этом случае мы будем использовать класс DefaultUriBuilderFactory
. Чтобы установить кодировку, вызовите метод setEncodingMode()
. Доступны следующие режимы:
- TEMPLATE_AND_VALUES : Предварительно кодируйте шаблон URI и строго кодируйте переменные URI при раскрытии.
- VALUES_ONLY : не кодировать шаблон URI, а строго кодировать переменные URI после их расширения в шаблоне.
- URI_COMPONENTS : кодировать значение компонента URI после расширения переменных URI.
- NONE : Кодировка не применяется.
Значение по умолчанию — TEMPLATE_AND_VALUES . Давайте установим режим URI_COMPONENTS :
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
webClient = WebClient
.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.exchangeFunction(exchangeFunction)
.build();
В результате будет выполнено следующее утверждение:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("name", "AndroidPhone")
.queryParam("color", "black")
.queryParam("deliveryDate", "13/04/2019")
.build())
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");
И, конечно же, мы можем предоставить полностью настраиваемую реализацию UriBuilderFactory
для ручного создания URI.
7. Заключение
В этом руководстве мы увидели, как создавать различные типы URI с помощью WebClient
и DefaultUriBuilder.
Попутно мы рассмотрели различные типы и форматы параметров запроса. И мы закончили, изменив режим кодирования по умолчанию в конструкторе URL.
Все фрагменты кода из статьи, как всегда, доступны в репозитории GitHub .