Spring 5 WebClient

1. Panoramica

In questo tutorial, esamineremo WebClient , che è un client Web reattivo introdotto nella primavera 5.

Vedremo anche WebTestClient, un WebClient progettato per essere utilizzato nei test.

2. Cos'è il WebClient ?

In poche parole, WebClient è un'interfaccia che rappresenta il punto di ingresso principale per l'esecuzione delle richieste web.

È stato creato come parte del modulo Spring Web Reactive e sostituirà il classico RestTemplate in questi scenari. Inoltre, il nuovo client è una soluzione reattiva e non bloccante che funziona sul protocollo HTTP / 1.1.

Infine, l'interfaccia ha un'unica implementazione, la classe DefaultWebClient , con la quale lavoreremo.

3. Dipendenze

Dato che stiamo usando un'applicazione Spring Boot, abbiamo bisogno della dipendenza spring-boot-starter-webflux , così come del progetto Reactor.

3.1. Costruire con Maven

Aggiungiamo le seguenti dipendenze al file pom.xml :

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

3.2. Costruire con Gradle

Con Gradle, dobbiamo aggiungere le seguenti voci al file build.gradle :

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

4. Lavorare con il WebClient

Per lavorare correttamente con il cliente, dobbiamo sapere come:

  • crea un'istanza
  • fare una richiesta
  • gestire la risposta

4.1. Creazione di un WebClient istanza

Ci sono tre opzioni tra cui scegliere. Il primo è creare un oggetto WebClient con le impostazioni predefinite:

WebClient client1 = WebClient.create(); 

La seconda opzione è avviare un'istanza WebClient con un dato URI di base:

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

La terza opzione (e la più avanzata) è la creazione di un client utilizzando la classe DefaultWebClientBuilder , che consente una personalizzazione 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. Creazione di un'istanza WebClient con timeout

Spesso, i timeout HTTP predefiniti di 30 secondi sono troppo lenti per le nostre esigenze, quindi vediamo come configurarli per la nostra istanza WebClient .

La classe principale che usiamo è TcpClient.

Lì possiamo impostare il timeout della connessione tramite il valore ChannelOption.CONNECT_TIMEOUT_MILLIS . Possiamo anche impostare i timeout di lettura e scrittura utilizzando un ReadTimeoutHandler e un WriteTimeoutHandler , rispettivamente:

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

Nota che mentre possiamo chiamare timeout anche sulla nostra richiesta del client, questo è un timeout del segnale, non una connessione HTTP o un timeout di lettura / scrittura; è un timeout per l'editore Mono / Flux.

4.3. Preparazione di una richiesta

Per prima cosa dobbiamo specificare un metodo HTTP di una richiesta invocando il metodo (metodo HttpMethod) o chiamando i suoi metodi di scelta rapida come get , post e delete :

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

Il passaggio successivo consiste nel fornire un URL. Possiamo passarlo all'API uri come istanza String o java.net.URL :

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

Quindi possiamo impostare il corpo della richiesta, il tipo di contenuto, la lunghezza, i cookie o le intestazioni se necessario.

Ad esempio, se vogliamo impostare un corpo della richiesta, ci sono due modi disponibili: riempirlo con un BodyInserter o delegare questo lavoro a un editore :

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

Il BodyInserter è un'interfaccia responsabile della compilazione un ReactiveHttpOutputMessage corpo con un dato messaggio di uscita ed un contesto utilizzato durante l'inserimento. Un editore è un componente reattivo incaricato di fornire un numero potenzialmente illimitato di elementi in sequenza.

Il secondo è il metodo body , che è una scorciatoia per il metodo body originale (BodyInserter inserter) .

Per alleviare il processo di riempimento di un BodyInserter, esiste una classe BodyInserters con una serie di utili metodi di utilità:

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

È anche possibile con una MultiValueMap :

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

Oppure utilizzando un singolo oggetto:

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

Dopo aver impostato il corpo, possiamo impostare intestazioni, cookie e tipi di media accettabili. I valori verranno aggiunti a quelli che sono già stati impostati durante l'istanza del client.

Inoltre, è disponibile un supporto aggiuntivo per le intestazioni più comunemente utilizzate come "If-None-Match", "If-Modified-Since", "Accept" e "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

In questo articolo, abbiamo esplorato WebClient, un nuovo meccanismo Spring migliorato per effettuare richieste sul lato client.

Abbiamo anche esaminato i vantaggi che offre durante la configurazione del client, la preparazione della richiesta e l'elaborazione della risposta.

Tutti gli snippet di codice menzionati nell'articolo possono essere trovati nel nostro repository GitHub.