Spring 5 WebClient

1. Información general

En este tutorial, examinaremos el WebClient , que es un cliente web reactivo introducido en Spring 5.

También veremos WebTestClient, un WebClient diseñado para usarse en pruebas.

2. ¿Qué es WebClient ?

En pocas palabras, WebClient es una interfaz que representa el principal punto de entrada para realizar solicitudes web.

Fue creado como parte del módulo Spring Web Reactive y reemplazará al clásico RestTemplate en estos escenarios. Además, el nuevo cliente es una solución reactiva sin bloqueo que funciona sobre el protocolo HTTP / 1.1.

Finalmente, la interfaz tiene una única implementación, la clase DefaultWebClient , con la que trabajaremos.

3. Dependencias

Dado que estamos utilizando una aplicación Spring Boot, necesitamos la dependencia spring-boot-starter-webflux , así como el proyecto Reactor.

3.1. Construyendo con Maven

Agreguemos las siguientes dependencias al archivo pom.xml :

 org.springframework.boot spring-boot-starter-webflux   org.projectreactor reactor-spring 1.0.1.RELEASE 

3.2. Construyendo con Gradle

Con Gradle, necesitamos agregar las siguientes entradas al archivo build.gradle :

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

4. Trabajar con WebClient

Para trabajar correctamente con el cliente, necesitamos saber cómo:

  • crear una instancia
  • hacer una solicitud
  • manejar la respuesta

4.1. Crear una instancia de WebClient

Hay tres opciones para elegir. El primero es crear un objeto WebClient con la configuración predeterminada:

WebClient client1 = WebClient.create(); 

La segunda opción es iniciar una instancia de WebClient con un URI base determinado:

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

La tercera opción (y la más avanzada) es construir un cliente usando la clase DefaultWebClientBuilder , que permite una personalización completa:

WebClient client3 = WebClient .builder() .baseUrl("//localhost:8080") .defaultCookie("cookieKey", "cookieValue") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .defaultUriVariables(Collections.singletonMap("url", "//localhost:8080")) .build();

4.2. Creación de una instancia de WebClient con tiempos de espera

A menudo, los tiempos de espera HTTP predeterminados de 30 segundos son demasiado lentos para nuestras necesidades, así que veamos cómo configurarlos para nuestra instancia WebClient .

La clase principal que usamos es TcpClient.

Allí podemos establecer el tiempo de espera de la conexión a través del valor ChannelOption.CONNECT_TIMEOUT_MILLIS . También podemos establecer los tiempos de espera de lectura y escritura usando ReadTimeoutHandler y WriteTimeoutHandler , respectivamente:

TcpClient tcpClient = TcpClient .create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) .doOnConnected(connection -> { connection.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS)); connection.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)); }); WebClient client = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))) .build();

Tenga en cuenta que, si bien también podemos llamar al tiempo de espera en la solicitud de nuestro cliente, este es un tiempo de espera de señal, no una conexión HTTP o un tiempo de espera de lectura / escritura; es un tiempo de espera para el editor Mono / Flux.

4.3. Preparando una solicitud

Primero, necesitamos especificar un método HTTP de una solicitud invocando el método (método HttpMethod) o llamando a sus métodos de acceso directo, como get , post y delete :

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

El siguiente paso es proporcionar una URL. Podemos pasarlo a la API uri como una cadena o una instancia de java.net.URL :

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

Luego, podemos establecer un cuerpo de solicitud, tipo de contenido, longitud, cookies o encabezados si es necesario.

Por ejemplo, si queremos establecer un cuerpo de solicitud, hay dos formas disponibles: llenándolo con un BodyInserter o delegando este trabajo a un Publisher :

WebClient.RequestHeadersSpec requestSpec1 = WebClient .create() .method(HttpMethod.POST) .uri("/resource") .body(BodyInserters.fromPublisher(Mono.just("data")), String.class); WebClient.RequestHeadersSpec requestSpec2 = WebClient .create("//localhost:8080") .post() .uri(URI.create("/resource")) .body(BodyInserters.fromObject("data"));

El BodyInserter es una interfaz responsable de llenar una ReactiveHttpOutputMessage cuerpo con un mensaje de salida dado y un contexto utilizado durante la inserción. Un Publisher es un componente reactivo que se encarga de proporcionar un número potencialmente ilimitado de elementos secuenciados.

La segunda forma es el método body , que es un atajo para el método body original (insertador BodyInserter) .

Para aliviar el proceso de llenado de BodyInserter, existe una clase BodyInserters con varios métodos de utilidad útiles:

BodyInserter
    
      inserter1 = BodyInserters .fromPublisher(Subscriber::onComplete, String.class); 
    

También es posible con un MultiValueMap :

LinkedMultiValueMap map = new LinkedMultiValueMap(); map.add("key1", "value1"); map.add("key2", "value2"); BodyInserter inserter2 = BodyInserters.fromMultipartData(map); 

O usando un solo objeto:

BodyInserter inserter3 = BodyInserters.fromObject(new Object()); 

Después de configurar el cuerpo, podemos configurar encabezados, cookies y tipos de medios aceptables. Los valores se agregarán a los que ya se establecieron al crear una instancia del cliente.

Además, hay soporte adicional para los encabezados más utilizados, como "If-None-Match", "If-Modified-Since", "Accept" y "Accept-Charset".

Here's an example of how these values can be used:

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.4. Getting a Response

The final stage is sending the request and receiving a response. This can be done with either the exchange or the retrieve method.

These methods differ in return types; the exchange method provides a ClientResponse along with its status and headers, while the retrieve method is the shortest path to fetching a body directly:

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

It's important to pay attention to the bodyToMono method, which will throw a WebClientException if the status code is 4xx (client error) or 5xx (server error). We use the block method on Monos to subscribe and retrieve actual data that was sent with the response.

5. Working with the WebTestClient

The WebTestClient is the main entry point for testing WebFlux server endpoints. It has a very similar API to the WebClient, and it delegates most of the work to an internal WebClient instance focusing mainly on providing a test context. The DefaultWebTestClient class is a single interface implementation.

The client for testing can be bound to a real server or work with specific controllers or functions.

5.1. Binding to a Server

To complete end-to-end integration tests with actual requests to a running server, we can use the bindToServer method:

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

5.2. Binding to a Router

We can test a particular RouterFunction by passing it to the bindToRouterFunction method:

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. Binding to a Web Handler

The same behavior can be achieved with the bindToWebHandler method, which takes a WebHandler instance:

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

5.4. Binding to an Application Context

A more interesting situation occurs when we're using the bindToApplicationContext method. It takes an ApplicationContext and analyses the context for controller beans and @EnableWebFlux configurations.

If we inject an instance of the ApplicationContext, a simple code snippet may look like this:

@Autowired private ApplicationContext context; WebTestClient testClient = WebTestClient.bindToApplicationContext(context) .build(); 

5.5. Binding to a Controller

A shorter approach would be providing an array of controllers we want to test by the bindToController method. Assuming we've got a Controller class and we injected it into a needed class, we can write:

@Autowired private Controller controller; WebTestClient testClient = WebTestClient.bindToController(controller).build(); 

5.6. Making a Request

After building a WebTestClient object, all following operations in the chain are going to be similar to the WebClient until the exchange method (one way to get a response), which provides the WebTestClient.ResponseSpec interface to work with useful methods like the expectStatus, expectBody, and expectHeader:

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

6. Conclusion

En este artículo, exploramos WebClient, un nuevo mecanismo Spring mejorado para realizar solicitudes en el lado del cliente.

También analizamos los beneficios que proporciona al configurar el cliente, preparar la solicitud y procesar la respuesta.

Todos los fragmentos de código mencionados en el artículo se pueden encontrar en nuestro repositorio de GitHub.