WebClient da Primavera 5

WebClient da Primavera 5

*1. Visão geral *

Neste artigo, mostraremos o WebClient - um cliente da Web reativo que está sendo introduzido no Spring 5.

Também veremos o WebTestClient - que é um WebClient projetado para ser usado em testes.

Leitura adicional:

https://www..com/spring-webclient-filters [Filtros do Spring WebClient]

Aprenda sobre os filtros WebClient no Spring WebFlux

https://www..com/webflux-webclient-parameters [Solicitações do Spring WebClient com parâmetros]

Aprenda a consumir reativamente os pontos de extremidade da API REST com o WebClient do Spring Webflux.

===* 2. O que é o _WebClient _? *

Simplificando, WebClient é uma interface que representa o principal ponto de entrada para executar solicitações da web.

Ele foi criado como parte do módulo Spring Web Reactive e substituirá o clássico RestTemplate nesses cenários. O novo cliente é uma solução reativa e sem bloqueio que funciona sobre o protocolo HTTP/1.1.

Por fim, a interface possui uma única implementação - a classe DefaultWebClient - com a qual trabalharemos.

===* 3. Dependências *

Como estamos usando um aplicativo Spring Boot, precisamos do https://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A%22spring-boot-starot-webflux%22%20AND%20g%3A % 22org.springframework.boot% 22 [spring-boot-starter-webflux], bem como a o projeto do Reactor.

====* 3.1 Construindo com o Maven *

Vamos adicionar as seguintes dependências ao arquivo pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.projectreactor</groupId>
    <artifactId>reactor-spring</artifactId>
    <version>1.0.1.RELEASE</version>
</dependency>

====* 3.2 Construindo com Gradle *

Com o Gradle, precisamos adicionar as seguintes entradas ao arquivo build.gradle:

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-webflux'
    compile 'org.projectreactor:reactor-spring:1.0.1.RELEASE'
}

===* 4. Trabalhando com o WebClient *

Para trabalhar corretamente com o cliente, precisamos saber como:

  • criar uma instância

  • faça um pedido *lidar com a resposta

====* 4.1 Criando uma instância WebClient

Existem três opções para você escolher. O primeiro é criar um objeto WebClient com configurações padrão:

WebClient client1 = WebClient.create();

A segunda alternativa permite iniciar uma instância WebClient com um determinado URI base:

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

A última maneira (e a mais avançada) é criar um cliente usando a classe DefaultWebClientBuilder, que permite personalização completa:

WebClient client3 = 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 Preparando uma solicitação

Primeiro, precisamos especificar um método HTTP de uma solicitação, chamando o method (método HttpMethod) _ ou chamando seus métodos de atalho, como _get, post, delete:

WebClient.UriSpec<WebClient.RequestBodySpec> request1 = client3.method(HttpMethod.POST);
WebClient.UriSpec<WebClient.RequestBodySpec> request2 = client3.post();

O próximo passo é fornecer um URL. Podemos passá-lo para a API uri - como uma instância String ou java.net.URL:

WebClient.RequestBodySpec uri1 = client3
  .method(HttpMethod.POST)
  .uri("/resource");

WebClient.RequestBodySpec uri2 = client3
  .post()
  .uri(URI.create("/resource"));

Seguindo em frente, podemos definir um corpo de solicitação, tipo de conteúdo, comprimento, cookies ou cabeçalhos - se necessário.

Por exemplo, se queremos definir um corpo de solicitação - há duas maneiras disponíveis - preenchê-lo com um BodyInserter ou delegar este trabalho a um Publisher:

WebClient.RequestHeadersSpec requestSpec1 = WebClient
  .create()
  .method(HttpMethod.POST)
  .uri("/resource")
  .body(BodyInserters.fromPublisher(Mono.just("data")), String.class);

WebClient.RequestHeadersSpec<?> requestSpec2 = WebClient
  .create("http://localhost:8080")
  .post()
  .uri(URI.create("/resource"))
  .body(BodyInserters.fromObject("data"));
*O _BodyInserter_ é uma interface responsável por preencher um corpo _ReactiveHttpOutputMessage_ com uma determinada mensagem de saída e um contexto usado durante a inserção.* Um _Publisher_ é um componente reativo encarregado de fornecer um número potencialmente ilimitado de elementos sequenciados.

A segunda maneira é o método body, que é um atalho para o método _body original (inseridor do BodyInserter) _.

Para aliviar esse processo de preenchimento de um BodyInserter, existe uma classe _BodyInserters com vários métodos úteis de utilidade:

BodyInserter<Publisher<String>, ReactiveHttpOutputMessage> inserter1 = BodyInserters
  .fromPublisher(Subscriber::onComplete, String.class);

Também é possível com um MultiValueMap:

LinkedMultiValueMap map = new LinkedMultiValueMap();

map.add("key1", "value1");
map.add("key2", "value2");

BodyInserter<MultiValueMap, ClientHttpRequest> inserter2
 = BodyInserters.fromMultipartData(map);

Ou usando um único objeto:

BodyInserter<Object, ReactiveHttpOutputMessage> inserter3
 = BodyInserters.fromObject(new Object());

Depois de definir o corpo, podemos definir cabeçalhos, cookies e tipos de mídia aceitáveis. Os valores serão adicionados aos que foram definidos ao instanciar o cliente.

Além disso, há suporte adicional para os cabeçalhos mais usados, como _ “If-None-Match”, “If-Modified-Since”, “Accept”, “Accept-Charset” ._

Aqui está um exemplo de como esses valores podem ser usados:

WebClient.ResponseSpec response1 = uri1
  .body(inserter3)
    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
    .acceptCharset(Charset.forName("UTF-8"))
    .ifNoneMatch("*")
    .ifModifiedSince(ZonedDateTime.now())
  .retrieve();

4.3 Obtendo uma resposta

A etapa final é enviar a solicitação e receber uma resposta. Isso pode ser feito com os métodos exchange ou retrieve.

Eles diferem nos tipos de retorno; o método exchange fornece um ClientResponse junto com seu status, cabeçalhos, enquanto o método retrieve é o caminho mais curto para buscar um corpo diretamente:

String response2 = request1.exchange()
  .block()
  .bodyToMono(String.class)
  .block();
String response3 = request2
  .retrieve()
  .bodyToMono(String.class)
  .block();

Preste atenção ao método bodyToMono, que emitirá uma WebClientException se o código de status for 4xx (erro do cliente) ou 5xx (erro do servidor). Usamos o método block em Monos para assinar e recuperar dados reais que foram enviados com a resposta.

*5. Trabalhando com o WebTestClient *

O WebTestClient é o principal ponto de entrada para testar os pontos de extremidade do servidor WebFlux. Possui uma API muito semelhante ao WebClient e delega a maior parte do trabalho a uma instância interna de WebClient, concentrando-se principalmente em fornecer um contexto de teste. A classe DefaultWebTestClient é uma implementação de interface única.

O cliente para teste pode ser vinculado a um servidor real ou trabalhar com controladores ou funções específicas. Para concluir testes de integração de ponta a ponta com solicitações reais para um servidor em execução, podemos usar o método bindToServer:

WebTestClient testClient = WebTestClient
  .bindToServer()
  .baseUrl("http://localhost:8080")
  .build();

Podemos testar um RouterFunction específico, passando-o para o método bindToRouterFunction:

RouterFunction function = RouterFunctions.route(
  RequestPredicates.GET("/resource"),
  request -> ServerResponse.ok().build()
);

WebTestClient
  .bindToRouterFunction(function)
  .build().get().uri("/resource")
  .exchange()
  .expectStatus().isOk()
  .expectBody().isEmpty();

O mesmo comportamento pode ser alcançado com o método bindToWebHandler, que utiliza uma instância WebHandler:

WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();

Uma situação mais interessante ocorre quando estamos usando o método bindToApplicationContext. É necessário um ApplicationContext, analisa o contexto para beans do controlador e configurações _ @ EnableWebFlux_.

Se injetarmos uma instância do ApplicationContext, um trecho de código simples pode ser assim:

@Autowired
private ApplicationContext context;

WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
  .build();

Uma abordagem mais curta seria fornecer uma matriz de controladores que queremos testar pelo método bindToController. Supondo que temos uma classe Controller e a injetamos em uma classe necessária, podemos escrever:

@Autowired
private Controller controller;

WebTestClient testClient = WebTestClient.bindToController(controller).build();

Após criar um objeto WebTestClient, todas as operações a seguir na cadeia serão semelhantes ao método WebClient até o exchange (uma maneira de obter uma resposta), que fornece a interface WebTestClient.ResponseSpec para trabalhar com métodos úteis, como o expectStatus, expectBody, expectHeader:

WebTestClient
  .bindToServer()
    .baseUrl("http://localhost:8080")
    .build()
    .post()
    .uri("/resource")
  .exchange()
    .expectStatus().isCreated()
    .expectHeader().valueEquals("Content-Type", "application/json")
    .expectBody().isEmpty();

===* 6. Conclusão*

Neste tutorial, consideramos um novo mecanismo aprimorado do Spring para fazer solicitações no lado do cliente - a classe WebClient.

Além disso, analisamos os benefícios que ela oferece ao longo do processo de solicitação.

Todos os trechos de código mencionados no artigo podem ser encontrados em seu repositório do GitHub.