Spring 5 WebClient

1. Ikhtisar

Dalam tutorial ini, kita akan memeriksa WebClient , yang merupakan klien web reaktif yang diperkenalkan di Spring 5.

Kami juga akan melihat WebTestClient, sebuah WebClient dirancang untuk digunakan dalam tes.

2. Apakah WebClient itu ?

Sederhananya, WebClient adalah antarmuka yang mewakili titik masuk utama untuk melakukan permintaan web.

Itu dibuat sebagai bagian dari modul Spring Web Reactive, dan akan menggantikan RestTemplate klasik dalam skenario ini. Selain itu, klien baru adalah solusi non-pemblokiran reaktif yang bekerja melalui protokol HTTP / 1.1.

Terakhir, antarmuka memiliki implementasi tunggal, kelas DefaultWebClient , yang akan kita kerjakan.

3. Ketergantungan

Karena kami menggunakan aplikasi Spring Boot, kami membutuhkan dependensi spring-boot-starter-webflux , serta proyek Reactor.

3.1. Membangun dengan Maven

Mari tambahkan dependensi berikut ke file pom.xml :

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

3.2. Membangun dengan Gradle

Dengan Gradle, kita perlu menambahkan entri berikut ke file build.gradle :

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

4. Bekerja dengan WebClient

Untuk bekerja dengan baik dengan klien, kita perlu mengetahui cara:

  • buat sebuah contoh
  • Buat sebuah permintaan
  • tangani tanggapannya

4.1. Membuat Instance WebClient

Ada tiga opsi untuk dipilih. Yang pertama adalah membuat objek WebClient dengan pengaturan default:

WebClient client1 = WebClient.create(); 

Opsi kedua adalah memulai instance WebClient dengan URI dasar tertentu:

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

Opsi ketiga (dan yang paling canggih) adalah membangun klien dengan menggunakan kelas DefaultWebClientBuilder , yang memungkinkan penyesuaian penuh:

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. Membuat Instance WebClient dengan Timeout

Seringkali, batas waktu HTTP default 30 detik terlalu lambat untuk kebutuhan kita, jadi mari kita lihat cara mengkonfigurasinya untuk instance WebClient kita .

Kelas inti yang kami gunakan adalah TcpClient.

Di sana kita dapat mengatur batas waktu koneksi melalui nilai ChannelOption.CONNECT_TIMEOUT_MILLIS . Kita juga dapat menyetel waktu tunggu baca dan tulis menggunakan ReadTimeoutHandler dan WriteTimeoutHandler , masing-masing:

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

Perhatikan bahwa meskipun kami juga dapat memanggil waktu tunggu atas permintaan klien kami, ini adalah batas waktu sinyal, bukan sambungan HTTP, atau batas waktu baca / tulis; ini adalah waktu tunggu untuk penerbit Mono / Flux.

4.3. Mempersiapkan Permintaan

Pertama kita perlu menentukan metode HTTP permintaan dengan memanggil metode (metode HttpMethod) atau memanggil metode pintasannya seperti get , post , dan delete :

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

Langkah selanjutnya adalah memberikan URL. Kita bisa meneruskannya ke uri API sebagai String atau instance java.net.URL :

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

Kemudian kita dapat mengatur isi permintaan, jenis konten, panjang, cookie, atau header jika kita perlu.

Misalnya, jika kita ingin menyetel isi permintaan, ada dua cara yang tersedia: mengisinya dengan BodyInserter atau mendelegasikan pekerjaan ini ke 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 adalah sebuah antarmuka bertanggung jawab untuk mengisi sebuah ReactiveHttpOutputMessage tubuh dengan pesan output tertentu dan konteks yang digunakan selama penyisipan. Sebuah Publisher adalah komponen reaktif yang bertugas menyediakan sejumlah berpotensi tak terbatas dari elemen sequencing.

Cara kedua adalah metode body , yang merupakan jalan pintas untuk metode body asli (inserter BodyInserter) .

Untuk meringankan proses pengisian BodyInserter, terdapat kelas BodyInserters dengan sejumlah metode utilitas yang berguna:

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

Ini juga dimungkinkan dengan MultiValueMap :

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

Atau dengan menggunakan satu objek:

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

Setelah kami mengatur badan, kami dapat mengatur header, cookie, dan jenis media yang dapat diterima. Nilai akan ditambahkan ke nilai yang telah ditetapkan saat membuat instance klien.

Selain itu, ada dukungan tambahan untuk header yang paling umum digunakan seperti "If-None-Match", "If-Modified-Because", "Accept", dan "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

Dalam artikel ini, kami menjelajahi WebClient, mekanisme Spring baru yang ditingkatkan untuk membuat permintaan di sisi klien.

Kami juga melihat manfaat yang diberikannya melalui konfigurasi klien, menyiapkan permintaan, dan memproses respons.

Semua cuplikan kode yang disebutkan dalam artikel dapat ditemukan di repositori GitHub kami.