Spring 5 WebClient

1. Übersicht

In diesem Tutorial untersuchen wir den WebClient , einen reaktiven Webclient, der im Frühjahr 5 eingeführt wurde.

Wir werden uns auch den WebTestClient ansehen, einen WebClient , der für Tests verwendet werden soll.

2. Was ist der WebClient ?

Einfach ausgedrückt ist WebClient eine Schnittstelle, die den Haupteinstiegspunkt für die Ausführung von Webanfragen darstellt.

Es wurde als Teil des Spring Web Reactive-Moduls erstellt und ersetzt in diesen Szenarien das klassische RestTemplate . Darüber hinaus ist der neue Client eine reaktive, nicht blockierende Lösung, die über das HTTP / 1.1-Protokoll funktioniert.

Schließlich hat die Schnittstelle eine einzige Implementierung, die DefaultWebClient- Klasse, mit der wir arbeiten werden.

3. Abhängigkeiten

Da wir eine Spring Boot-Anwendung verwenden, benötigen wir die Spring-Boot-Starter-Webflux- Abhängigkeit sowie das Reactor-Projekt.

3.1. Bauen mit Maven

Fügen wir der Datei pom.xml die folgenden Abhängigkeiten hinzu :

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

3.2. Bauen mit Gradle

Bei Gradle müssen wir der Datei build.gradle die folgenden Einträge hinzufügen :

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

4. Arbeiten mit dem WebClient

Um richtig mit dem Kunden zu arbeiten, müssen wir wissen, wie man:

  • Erstellen Sie eine Instanz
  • eine Anfrage stellen
  • Behandle die Antwort

4.1. Erstellen einer WebClient- Instanz

Es stehen drei Optionen zur Auswahl. Das erste ist das Erstellen eines WebClient- Objekts mit Standardeinstellungen:

WebClient client1 = WebClient.create(); 

Die zweite Option besteht darin, eine WebClient- Instanz mit einem bestimmten Basis-URI zu initiieren :

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

Die dritte Option (und die am weitesten fortgeschrittene) ist das Erstellen eines Clients mithilfe der DefaultWebClientBuilder- Klasse, die eine vollständige Anpassung ermöglicht:

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. Erstellen einer WebClient- Instanz mit Zeitüberschreitungen

Häufig sind die Standard-HTTP-Zeitüberschreitungen von 30 Sekunden für unsere Anforderungen zu langsam. Schauen wir uns also an, wie Sie sie für unsere WebClient- Instanz konfigurieren .

Die Kernklasse, die wir verwenden, ist TcpClient.

Dort können wir das Verbindungszeitlimit über den Wert ChannelOption.CONNECT_TIMEOUT_MILLIS einstellen . Wir können die Lese- und Schreibzeitlimits auch mit einem ReadTimeoutHandler bzw. einem WriteTimeoutHandler festlegen :

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

Beachten Sie, dass , während wir anrufen können Timeout auf unserer Client - Anforderung als auch, das ist ein Signal Timeout ist, nicht eine HTTP - Verbindung oder Lese / Schreib - Timeout; Es ist eine Auszeit für den Mono / Flux-Verlag.

4.3. Anfrage vorbereiten

Zuerst müssen wir eine HTTP-Methode einer Anforderung angeben, indem wir die Methode (HttpMethod-Methode) aufrufen oder ihre Verknüpfungsmethoden wie get , post und delete aufrufen :

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

Der nächste Schritt ist die Angabe einer URL. Wir können es als String oder als java.net.URL- Instanz an die uri- API übergeben :

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

Dann können wir bei Bedarf einen Anfragetext, einen Inhaltstyp, eine Länge, Cookies oder Header festlegen.

Wenn Sie beispielsweise einen Anforderungshauptteil festlegen möchten, stehen zwei Möglichkeiten zur Verfügung: Füllen Sie ihn mit einem BodyInserter oder delegieren Sie diese Arbeit an einen 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"));

Der BodyInserter ist eine Schnittstelle, die für das Auffüllen eines ReactiveHttpOutputMessage- Körpers mit einer bestimmten Ausgabenachricht und einem Kontext, der beim Einfügen verwendet wird, verantwortlich ist. Ein Publisher ist eine reaktive Komponente, die für die Bereitstellung einer möglicherweise unbegrenzten Anzahl sequenzierter Elemente verantwortlich ist.

Der zweite Weg ist die Body- Methode, eine Abkürzung für die ursprüngliche Body- Methode (BodyInserter Inserter) .

Um das Befüllen eines BodyInserter zu vereinfachen, gibt es eine BodyInserters- Klasse mit einer Reihe nützlicher Dienstprogrammmethoden:

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

Mit einer MultiValueMap ist es auch möglich :

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

Oder indem Sie ein einzelnes Objekt verwenden:

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

Nachdem wir den Body festgelegt haben, können wir Header, Cookies und akzeptable Medientypen festlegen. Werte werden zu denen hinzugefügt, die bereits beim Instanziieren des Clients festgelegt wurden.

Außerdem werden die am häufigsten verwendeten Header wie "If-None-Match", "If-Modified-Since", "Accept" und "Accept-Charset" zusätzlich unterstützt.

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 diesem Artikel haben wir den WebClient untersucht, einen neuen erweiterten Spring-Mechanismus zum Erstellen von Anforderungen auf der Clientseite.

Wir haben uns auch die Vorteile angesehen, die sich aus der Konfiguration des Clients, der Vorbereitung der Anforderung und der Verarbeitung der Antwort ergeben.

Alle im Artikel erwähnten Codefragmente finden Sie in unserem GitHub-Repository.