Перейти к основному содержимому

Запросы Spring WebClient с параметрами

· 6 мин. чтения

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 .