Client Web Spring 5

1. Vue d'ensemble

Dans ce tutoriel, nous allons examiner le WebClient , qui est un client Web réactif introduit dans Spring 5.

Nous allons également examiner le WebTestClient, un WebClient conçu pour être utilisé dans les tests.

2. Qu'est-ce que le WebClient ?

En termes simples, WebClient est une interface représentant le principal point d'entrée pour effectuer des requêtes Web.

Il a été créé dans le cadre du module Spring Web Reactive et remplacera le RestTemplate classique dans ces scénarios. De plus, le nouveau client est une solution réactive et non bloquante qui fonctionne sur le protocole HTTP / 1.1.

Enfin, l'interface a une seule implémentation, la classe DefaultWebClient , avec laquelle nous allons travailler.

3. Dépendances

Puisque nous utilisons une application Spring Boot, nous avons besoin de la dépendance spring-boot-starter-webflux , ainsi que du projet Reactor.

3.1. Construire avec Maven

Ajoutons les dépendances suivantes au fichier pom.xml :

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

3.2. Construire avec Gradle

Avec Gradle, nous devons ajouter les entrées suivantes au fichier build.gradle :

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

4. Travailler avec le WebClient

Pour travailler correctement avec le client, nous devons savoir comment:

  • créer une instance
  • faire une demande
  • gérer la réponse

4.1. Créer une instance WebClient

Vous avez le choix entre trois options. La première consiste à créer un objet WebClient avec les paramètres par défaut:

WebClient client1 = WebClient.create(); 

La deuxième option consiste à lancer une instance WebClient avec un URI de base donné:

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

La troisième option (et la plus avancée) consiste à créer un client à l'aide de la classe DefaultWebClientBuilder , qui permet une personnalisation complète:

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. Création d'une instance WebClient avec des délais d'expiration

Souvent, les délais d'attente HTTP par défaut de 30 secondes sont trop lents pour nos besoins, voyons donc comment les configurer pour notre instance WebClient .

La classe principale que nous utilisons est TcpClient.

Là, nous pouvons définir le délai d'expiration de la connexion via la valeur ChannelOption.CONNECT_TIMEOUT_MILLIS . Nous pouvons également définir les délais de lecture et d'écriture à l'aide d'un ReadTimeoutHandler et d'un WriteTimeoutHandler , respectivement:

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();

Notez que bien que nous puissions également appeler timeout sur notre demande client, il s'agit d'un timeout de signal, pas d'une connexion HTTP ou d'un timeout de lecture / écriture; c'est un timeout pour l'éditeur Mono / Flux.

4.3. Préparer une demande

Nous devons d'abord spécifier une méthode HTTP d'une requête en appelant la méthode (méthode HttpMethod) ou en appelant ses méthodes de raccourci telles que get , post et delete :

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

The next step is to provide a URL. We can pass it to the uri API as a String or a java.net.URL instance:

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

Then we can set a request body, content type, length, cookies, or headers if we need to.

For example, if we want to set a request body, there are two available ways: filling it with a BodyInserter or delegating this work to a 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"));

The BodyInserter is an interface responsible for populating a ReactiveHttpOutputMessage body with a given output message and a context used during the insertion. A Publisher is a reactive component that is in charge of providing a potentially unbounded number of sequenced elements.

The second way is the body method, which is a shortcut for the original body(BodyInserter inserter) method.

To alleviate the process of filling a BodyInserter, there is a BodyInserters class with a number of useful utility methods:

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

It is also possible with a MultiValueMap:

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

Or by using a single object:

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

After we set the body, we can set headers, cookies, and acceptable media types. Values will be added to those that have already been set when instantiating the client.

Also, there is additional support for the most commonly used headers like “If-None-Match”, “If-Modified-Since”, “Accept”, and “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

Dans cet article, nous avons exploré le WebClient, un nouveau mécanisme Spring amélioré pour effectuer des demandes côté client.

Nous avons également examiné les avantages qu'il offre en procédant à la configuration du client, à la préparation de la demande et au traitement de la réponse.

Tous les extraits de code mentionnés dans l'article se trouvent dans notre référentiel GitHub.