Thay thế RestTemplate bằng WebClient trong Spring Boot

Spring Framework phiên bản 5 đã giới thiệu WebClient, một trong những thành phần quan trọng của Web Reactive framework giúp chúng ta triển khai reactive và non-blocking web application.

Trong các ứng dụng web một trong những công việc thường xuyên phải thực hiện đó là thực hiện một HTTP Request đến các server khác, kể cả các ứng dụng RestAPI Spring Boot cũng có nhu cầu này. Các phiên bản trước Spring 5, chúng ta có RestTemplate để làm công việc này. Tuy nhiên với nhiều hạn chế của RestTemplate nên nó đã được thay thế bởi WebClient, từ các phiên bản sau RestTemplate bị đánh dấu deprecated và không còn được cập nhật các tính năng mới.

WebClient là gì?

WebClient là một interface cung cấp các API dùng để thực thi các HTTP request. Nó được tạo ra như một phần của module Spring Web Reactive và sẽ thay thế RestTemplate trong tương lai. Ngoài ra, để triển khai cơ chế reactive và non-blocking nên WebClient sẽ không hoạt động trên phiên bảng HTTP/1.1 protocol.

Dependency

Vì chúng ta đang sử dụng ứng dụng Spring Boot, tất cả những gì chúng ta cần là spring-boot-starter-webflux.

Maven dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Gradle

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-webflux'
}

Thao tác cơ bản trên WebClient

Khởi tạo WebClient

Để tạo một WebClient object chúng ta có thể sử dụng create() method với Base URL.

WebClient webClient1 = WebClient.create();
 
WebClient webClient2 = WebClient.create("http://localhost:8080");

Hoặc sử dụng WebClient.Builder AP

WebClient webClient2 = WebClient.builder()
        .baseUrl("http://localhost:8080")
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .build();

Gửi HTTP request

Để gửi một HTTP request trong WebClient chúng ta phải đi qua các bước sau:

  • Tạo WebClient.UriSpec thông qua hàm method(HttpMethod) hoặc sử dụng các hàm dựng sẵn như get(), post(), put() và delete().
  • Tạo URI.
  • Tạo các thông tin header, các thông tin xác thực nếu được yêu cầu.
  • Tạo body request nếu cần thiết.
  • Gọi retrieve() hoặc exchange() method để gửi request đi và nhận kết quả trả về. Lưu ý exchange() method trả về ClientResponse chứa status và header response. Chúng ta có thể lấy response body từ ClientResponse.
  • Xử lý response trả về.
WebClient webClient = WebClient.create("http://localhost:3000");
 
Employee createdEmployee = webClient.post()
        .uri("/employees")
        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .body(Mono.just(empl), Employee.class)
        .retrieve()
        .bodyToMono(Employee.class);

Xử lý response trả về

Nếu chỉ quan tâm đến response body thì chúng ta chỉ cần sử dụng bodyToFlux()bodyToMono(). 

Ngoài ra, nếu sử dụng exchange() method, nó sẽ trả về ClientResponse chứa tất cả các thông tin về header, status, và response body.

Xin lưu ý rằng các phương thức bodyToMono()bodyToFlux() chỉ quan tâm đến response. Nếu mã trạng thái phản hồi là 4xx (lỗi máy khách) hoặc 5xx (lỗi máy chủ) thì các phương thức này sẽ ném ra WebClientException

Project Setup

 

WebClient có thể sử dụng để request đến một Rest API bất kỳ, tuy nhiên để đảm bảo ví dụ có thể hoạt động trong khuôn khổ bài viết này, mình sẽ sử dụng WebClient request đến chính server hiện tại.

package com.deft.webclient.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {

    private long id;
    private String firstName;
    private String lastName;
    private long yearlyIncome;
}

Trong đó sử dụng mình sử dụng  Lombok để giảm bớt các các bước triển khai constructor, getter, setter v.v

Tiếp theo định nghĩa một MockRestAPI định nghĩa các API mà sau đó chúng ta sẽ dùng WebClient để request đến.

package com.deft.webclient.mockapi;

import com.deft.webclient.model.Employee;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.*;
import java.util.stream.Collectors;


@RestController
@RequestMapping("/mock")
public class MockRestAPI {

    private static final Map<Long, Employee> employees;
    private static Long nextID = 1L;

    static {
        employees = new HashMap<>();
        employees.put(nextID, new Employee(nextID++, "John", "Doe", 80000));
        employees.put(nextID, new Employee(nextID++, "Mary", "Jackson", 75000));
        employees.put(nextID, new Employee(nextID++, "Peter", "Grey", 60000));
        employees.put(nextID, new Employee(nextID++, "Max", "Simpson", 67000));
        employees.put(nextID, new Employee(nextID++, "Lisa", "O'Melly", 45000));
        employees.put(nextID, new Employee(nextID++, "Josephine", "Rose", 52000));
    }

    @GetMapping("/{id}")
    public Employee getEmployee(@PathVariable Long id) {
        Optional<Employee> employee = Optional.ofNullable(employees.get(id));
        return employee.orElse(null);
    }

    @GetMapping
    public List<Employee> getAllEmployee() {
        return new ArrayList<>(employees.values());
    }

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Employee> create(@RequestBody Employee newEmployee) {
        newEmployee.setId(nextID++);
        employees.put(newEmployee.getId(), newEmployee);

        return ResponseEntity.status(HttpStatus.CREATED)
                .header("Location", "/rest/employees/" + newEmployee.getId())
                .body(newEmployee);
    }

    @PutMapping(path = "/{id}",
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Employee> update(@PathVariable long id, @RequestBody Employee request) {
        if (!employees.containsKey(id)) {
            return ResponseEntity.notFound().build();
        } else {
            Employee employee = employees.get(id);
            employee.setFirstName(request.getFirstName());
            employee.setLastName(request.getLastName());
            employee.setYearlyIncome(request.getYearlyIncome());

            return ResponseEntity.ok(employee);
        }
    }

    @DeleteMapping(path = "/{id}")
    public ResponseEntity<Void> delete(@PathVariable long id) {
        Employee removedEmployee = employees.remove(id);

        return removedEmployee != null ? ResponseEntity.ok().build() : ResponseEntity.notFound().build();
    }
}

Có nhiều cách để khởi tạo một WebClient object, tuy nhiên mình sẽ khởi tạo WebClient bean và sử dụng nó trong xuyên suốt ứng dụng.

package com.deft.webclient.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .baseUrl("http://localhost:8080/mock")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }
}

Ví  dụ sử dụng WebTemplate

GET Request

Thông thường GET API dùng để lấy dữ liệu từ server, trong ví dụ này chúng ta sẽ dùng GET để lấy danh sách tất các employee hoặc một employee theo ID duy nhất.

  • HTTP GET /: Lấy danh sách tất cả các Employee 
  • HTTP GET /{id} : Lấy một employee theo ID.
package com.deft.webclient.controller;

import com.deft.webclient.model.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;


@RestController
@RequestMapping("/")
public class ControllerExample {

    @Autowired
    private WebClient webClient;

    @GetMapping("/{id}")
    public Mono<Employee> findById(@PathVariable String id) {
        return webClient.get()
                .uri("/" + id)
                .retrieve()
                .bodyToMono(Employee.class);
    }

    @GetMapping()
    public Flux<Employee> findAll() {
        return webClient.get()
                .uri("/")
                .retrieve()
                .bodyToFlux(Employee.class);
    }

}

Post request

Post API thường được dùng để tạo mới một resource. Trong ví dụ này chúng ta sẽ dùng post() để tạo mới một employee.

@PostMapping
public Mono<Employee> create() {
    Employee employee = new Employee();
    employee.setFirstName("deft");
    employee.setLastName("blog");
    employee.setYearlyIncome(2021);
    return webClient.post()
            .uri("/")
            .body(Mono.just(employee), Employee.class)
            .retrieve()
            .bodyToMono(Employee.class);
}

PUT request

Sử dụng PUT request để cập nhật một employee theo ID,

@PutMapping("/{id}")
public Mono<Employee> update(@PathVariable String id) {
    Employee employee = new Employee();
    employee.setFirstName("deft");
    employee.setLastName("blog");
    employee.setYearlyIncome(2021);
    return webClient.put()
            .uri("/" + id)
            .body(Mono.just(employee), Employee.class)
            .retrieve()
            .bodyToMono(Employee.class);
}

DELETE request

DELETE được sử dụng phổ biến trong trường hợp chúng ta muốn xóa tài nguyên.

@DeleteMapping
public Mono<Void> delete(@PathVariable String id) {
    return webClient.delete()
            .uri("/" + id)
            .retrieve()
            .bodyToMono(Void.class);
}

Một số WebClient config

Memory limit

Spring WebFlux cấu hình mặc định cho giới hạn bộ nhớ đệm trên memory là 256KB. Nếu vượt quá giới hạn này trong bất kỳ trường hợp nào thì chúng ta sẽ gặp phải lỗi DataBufferLimitException.

Để đặt lại giới hạn bộ nhớ, hãy định cấu hình thuộc tính dưới đây trong tệp application.properties.

spring.codec.max-in-memory-size=1MB

 Connection Timeout

Chúng ta có thể sử dụng HttpClient để đặt connection timeout cũng như read timeout và write timeout.

package com.deft.webclient.config;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {

        HttpClient httpClient = HttpClient.create()
                .tcpConfiguration(client ->
                        client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
                                .doOnConnected(conn -> conn
                                        .addHandlerLast(new ReadTimeoutHandler(10))
                                        .addHandlerLast(new WriteTimeoutHandler(10))));

        ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);

        return WebClient.builder()
                .baseUrl("http://localhost:8080/mock")
                .clientConnector(connector)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }
}

Kết bài

Như vậy là chúng ta đã có một công cụ mới thay thế RestTemplate để thực thi các HTTP request trong các ứng dụng Spring với nhiều tính năng và hiệu xuất tốt hơn. Nếu bạn vừa mới bắt đầu dự án Spring gần đây, thì hãy sử dụng WebClient liền nhé.

Mã nguồn được mình công khai trên gitlab để các bạn tiện tham khảo: webclient

Nguồn

https://howtodoinjava.com/spring-webflux/webclient-get-post-example/#create-instance

Spring 5 WebClient

https://www.baeldung.com/spring-5-webclient

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x