Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog

RestClient & Fluent API trong Spring Boot 3

Tung Bui Thanh (TungDaDev)8 mins read

Restclient fluent api.webp

API tốt thì ngoài việc chạy đúng, người viết code còn phải thấy dễ chịu khi dùng. Fluent API là kiểu cú pháp đọc lên gần với cách mình nghĩ nhất.

Hơn chục năm nay, dev Java backend nào cần gọi một REST API bên ngoài cũng gõ:

RestTemplate restTemplate = new RestTemplate();

RestTemplate là "người lính già" đã cõng hàng tỷ request trong các hệ thống Spring khắp nơi.

Thật ra RestTemplate đã bị coi là "feature complete" (không thêm tính năng mới) từ Spring Framework 5.0, lúc WebClient ra đời. Tới Spring Framework 6.1 (đi cùng Spring Boot 3.2), team Spring ra mắt RestClient làm lựa chọn đồng bộ thay thế. Và theo bài The state of HTTP clients in Spring, lộ trình đã rõ: Spring Framework 7.0 (11/2025) thông báo ý định deprecate, 7.1 (11/2026) gắn @Deprecated chính thức, 8.0 thì xoá hẳn. Bản OSS vẫn được hỗ trợ ít nhất tới 2029, nên không phải vội, nhưng code mới thì nên dùng RestClient.

Cùng với JdbcClient (6.1), JmsClient (Spring Framework 7.0) và ChatClient (trong Spring AI), hệ sinh thái Spring đang có thêm nhiều API kiểu Fluent API đặt cạnh các class *Template quen thuộc. Riêng RestTemplate là bị thay thế hẳn; JdbcTemplate hay JmsTemplate thì vẫn được giữ, các *Client mới chỉ là lớp API gọn hơn ở bên trên.

Bài này sẽ nói vì sao RestTemplate đã lỗi thời, RestClient được thiết kế thế nào, và cách chuyển code cũ sang cho gọn.


# vì sao resttemplate thành gánh nặng?

RestTemplate ra đời từ Spring 3.0 (năm 2009), thời Java 5. Hồi đó nó là bước tiến lớn, nhưng 15 năm sau thì lộ ra kha khá điểm yếu:

  1. Quá nhiều hàm nạp chồng (Method Overload Explosion): RestTemplate có hơn 40 phương thức nạp chồng. Muốn gửi một request có custom header và query params, bạn phải dùng exchange() dài dằng dặc, kèm một đống tham số null hoặc HttpEntity rườm rà.
  2. Xử lý lỗi thô: Mặc định, response nào trả mã 4xx hoặc 5xx cũng ném HttpClientErrorException hoặc HttpServerErrorException. Muốn can thiệp thì phải viết một ResponseErrorHandler toàn cục khá phức tạp.
  3. Cái khó của WebClient: Lúc WebFlux ra đời, Spring giới thiệu WebClient với Fluent API rất đẹp. Có điều muốn dùng WebClient thì phải kéo spring-webflux, Reactor và một HTTP client reactive (thường là Reactor Netty) vào một app Spring MVC đồng bộ bình thường, rồi gọi .block() khắp nơi.

RestClient ra đời để gỡ đúng chỗ đó: đem cú pháp Fluent API của WebClient vào mô hình đồng bộ (Synchronous / Blocking), không cần WebFlux.


# cấu trúc RestClient & Fluent API

Đặt hai cách viết cạnh nhau cho dễ so:

1. Lấy dữ liệu (GET Request)

Cách cũ với RestTemplate:

// Khó nhớ thứ tự tham số, cần bọc UriComponentsBuilder
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
HttpEntity<Void> entity = new HttpEntity<>(headers);
 
ResponseEntity<UserDto> response = restTemplate.exchange(
    "https://api.example.com/users/{id}",
    HttpMethod.GET,
    entity,
    UserDto.class,
    userId
);
UserDto user = response.getBody();

Cách mới với RestClient:

UserDto user = restClient.get()
    .uri("https://api.example.com/users/{id}", userId)
    .header("Authorization", "Bearer " + token)
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .body(UserDto.class);

Đọc từng dòng như đọc một câu tiếng Anh: "GET tới URI này, gắn header này, nhận JSON, lấy kết quả rồi map vào UserDto".

Cần cả status và header thì đổi .body(UserDto.class) thành .toEntity(UserDto.class), sẽ nhận về ResponseEntity<UserDto> y như exchange() cũ. Chuyển dần cũng dễ: RestClient.create(restTemplate) (hoặc RestClient.builder(restTemplate)) tạo một RestClient dùng lại request factory, interceptor và message converter của RestTemplate đang có.


# xử lý lỗi gọn gàng với onstatus()

Mặc định retrieve() của RestClient cũng ném HttpClientErrorException / HttpServerErrorException (đều là con của RestClientResponseException) như RestTemplate. Khác ở chỗ, thay vì try-catch khắp nơi, bạn khai báo cách xử lý lỗi theo từng dải HTTP Status ngay trong chuỗi gọi:

OrderResult result = restClient.post()
    .uri("/api/v1/checkout")
    .contentType(MediaType.APPLICATION_JSON)
    .body(checkoutRequest)
    .retrieve()
    // Bắt lỗi 400 Bad Request
    .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
        String errorMsg = new String(response.getBody().readAllBytes());
        log.error("Lỗi từ phía Client: {} - {}", response.getStatusCode(), errorMsg);
        throw new InvalidOrderException("Thông tin đơn hàng không hợp lệ: " + errorMsg);
    })
    // Bắt lỗi 500 Server Error
    .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
        log.error("Dịch vụ thanh toán bị lỗi: {}", response.getStatusCode());
        throw new PaymentGatewayException("Cổng thanh toán đang bảo trì, vui lòng thử lại sau!");
    })
    .body(OrderResult.class);

Handler nào không ném exception thì response được coi là "đã xử lý" và chuỗi gọi chạy tiếp tới .body(). Muốn áp cùng một cách xử lý cho mọi request của một client thì khai báo một lần bằng defaultStatusHandler(...) trên builder.


# chuyện tương tự: jdbcclient thay jdbctemplate

Không chỉ HTTP client, Spring Framework 6.1 (Spring Boot 3.2) còn có JdbcClient, một lớp API gọn hơn đặt trên JdbcTemplate và NamedParameterJdbcTemplate (bên dưới nó vẫn gọi hai class này). Spring Boot tự tạo sẵn bean JdbcClient khi có DataSource.

Trước đây dùng JdbcTemplate thì bạn phải tự viết RowMapper, hoặc dùng BeanPropertyRowMapper (cần constructor rỗng và setter, không hợp với record):

// Cách cũ với JdbcTemplate:
List<Customer> customers = jdbcTemplate.query(
    "SELECT id, name, email FROM customers WHERE active = ?",
    (rs, rowNum) -> new Customer(rs.getLong("id"), rs.getString("name"), rs.getString("email")),
    true
);

Với JdbcClient, query database rồi map vào record (có từ Java 16) gọn hơn hẳn:

// Cách mới với JdbcClient:
List<Customer> customers = jdbcClient.sql("SELECT id, name, email FROM customers WHERE active = :active")
    .param("active", true)
    .query(Customer.class) // Tự động map vào Java Record bằng tên thuộc tính!
    .list();

query(Customer.class) dùng SimplePropertyRowMapper: khớp tên cột với tham số constructor của record (có hiểu snake_case sang camelCase). Bên dưới vẫn là reflection, nên đây là cải thiện về độ gọn chứ không phải về tốc độ. Ở đường code nóng mà cần tối ưu từng chút thì RowMapper viết tay vẫn nhanh nhất, và JdbcClient nhận luôn nó: .query((rs, rowNum) -> new Customer(...)).


# best practices cấu hình restclient cho enterprise

Muốn RestClient chạy ổn trên production thì đừng dùng RestClient.create() mặc định: không có timeout rõ ràng, và không có observation/metrics mà Spring Boot gắn sẵn vào RestClient.Builder. Nên khai báo một bean từ builder do Spring Boot inject, có connection pool và timeout qua HttpComponentsClientHttpRequestFactory (Apache HttpClient 5, nhớ thêm dependency org.apache.httpcomponents.client5:httpclient5):

package com.tungdadev.config;
 
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.core5.util.Timeout;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
 
@Configuration
public class RestClientConfig {
 
    @Bean
    public RestClient partnerApiClient(RestClient.Builder builder) {
        // 1. Cấu hình Connection Pool + Connect Timeout (từ HttpClient 5.2 đặt ở ConnectionConfig)
        PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
                .setMaxConnTotal(200)       // Tối đa 200 connection cho toàn bộ app
                .setMaxConnPerRoute(50)     // Tối đa 50 connection cho 1 host
                .setDefaultConnectionConfig(ConnectionConfig.custom()
                        .setConnectTimeout(Timeout.ofSeconds(3))
                        .build())
                .build();
 
        // 2. Timeout chờ lấy connection từ pool (mặc định 3 phút!) và Read Timeout
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(Timeout.ofSeconds(2))
                .setResponseTimeout(Timeout.ofSeconds(5))
                .build();
 
        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .build();
 
        // 3. Khởi tạo RestClient
        return builder
                .baseUrl("https://api.partner.com")
                .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient))
                .defaultHeader("User-Agent", "TungDaDev-Backend/1.0")
                .build();
    }
}

Ba loại timeout ở đây dễ nhầm: connect timeout là thời gian bắt tay TCP/TLS, response timeout là thời gian chờ dữ liệu trả về, còn connection request timeout là thời gian chờ mượn một connection trong pool. Cái thứ ba mặc định tới 3 phút trong HttpClient 5, nên khi đối tác chậm và pool cạn, request sẽ treo rất lâu mà không báo lỗi gì. Bản cũ hơn của đoạn code này dùng RequestConfig.setConnectTimeout(...), cách đó đã bị deprecated từ HttpClient 5.2.

Nếu chỉ cần timeout mà không cần tinh chỉnh pool, từ Spring Boot 3.4 có sẵn ClientHttpRequestFactoryBuilder và nhóm property spring.http.client.* (Boot 4 có đổi tên một số property, xem tài liệu của bản đang dùng) để cấu hình mà không phải tự dựng HttpClient. Còn muốn bỏ hẳn code gọi HTTP thủ công thì xem thêm HTTP Interface (@HttpExchange): khai báo một interface Java, Spring sinh client chạy trên RestClient.


# tổng kết

Chuyện RestTemplate nhường chỗ cho RestClient, và JdbcClient xuất hiện cạnh JdbcTemplate, cho thấy hướng đi chung của Spring: làm cho dev viết code dễ chịu hơn (Developer Experience), bớt boilerplate, và cả framework dùng chung một kiểu API.

Nếu bạn đang mở dự án mới trên Spring Boot 3 hay 4, cứ dùng RestClient ngay từ đầu. Service cũ thì không cần viết lại một lượt: bắt đầu bằng RestClient.create(restTemplate) cho những chỗ code mới, rồi chuyển dần trước khi Spring Framework 8 xoá hẳn RestTemplate. Khi viết test, MockRestServiceServer.bindTo(restClientBuilder) dùng được cho RestClient giống hệt như với RestTemplate.


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.