TungDaDev's Blog

tạm biệt resttemplate: kỷ nguyên restclient & fluent api trong spring boot 3

Restclient fluent api.jpg
Published on
/6 mins read/

Một API tốt không chỉ làm được việc, mà phải mang lại cảm giác mượt mà và trực quan cho người viết code. Cú pháp Fluent API chính là cách ngôn ngữ lập trình trò chuyện tự nhiên với tư duy con người.

Trong suốt hơn một thập kỷ, bất kỳ kỹ sư backend Java nào khi cần gọi một REST API từ bên ngoài cũng đều gõ:

RestTemplate restTemplate = new RestTemplate();

RestTemplate là "người lính già cần mẫn" đã cõng hàng tỷ request trong các hệ thống Spring trên toàn cầu.

Thế nhưng, từ phiên bản Spring Framework 6 và Spring Boot 3.2, đội ngũ Spring chính thức đưa ra khuyến cáo: RestTemplate đã bước vào chế độ bảo trì (Maintenance Mode) và khuyên các nhà phát triển nên chuyển sang công cụ mới: RestClient.

Cùng với JdbcClient, JmsClient và ChatClient (trong Spring AI), hệ sinh thái Spring đang chứng kiến một cuộc cách mạng đồng bộ: Rũ bỏ hoàn toàn các class dạng *Template cồng kềnh để chuyển sang phong cách Fluent API hiện đại.

Bài viết này sẽ giải thích tại sao RestTemplate lỗi thời, phân tích kiến trúc của RestClient và hướng dẫn bạn di chuyển mã nguồn một cách chuyên nghiệp.


# tại sao resttemplate lại trở thành gánh nặng?

RestTemplate được thiết kế từ thời Java 5 (năm 2009). Vào thời điểm đó, nó là một bước tiến lớn, nhưng sau 15 năm, nó bộc lộ những điểm yếu cố hữu:

  1. Quá tải phương thức (Method Overload Explosion): RestTemplate có hơn 40 phương thức nạp chồng khác nhau. Để thực hiện một request với custom header và query params, bạn phải dùng hàm exchange() dài dằng dặc với hàng đống tham số null hoặc HttpEntity rườm rà.
  2. Xử lý lỗi ngoại lệ thô kệch: Mặc định, bất kỳ response nào trả về mã 4xx hoặc 5xx đều ném ra HttpClientErrorException hoặc HttpServerErrorException. Để can thiệp, bạn buộc phải viết một ResponseErrorHandler toàn cục phức tạp.
  3. Nghịch lý WebClient: Khi WebFlux ra đời, Spring giới thiệu WebClient với cú pháp Fluent API tuyệt đẹp. Nhưng muốn dùng WebClient, bạn buộc phải kéo toàn bộ dependency nặng nề của spring-boot-starter-webflux và Netty vào một ứng dụng Spring MVC đồng bộ truyền thống.

Và RestClient ra đời để giải quyết trọn vẹn nghịch lý đó: Mang cú pháp Fluent API của WebClient vào mô hình lập trình đồng bộ (Synchronous / Blocking) mà không cần WebFlux!


# giải phẫu restclient: sức mạnh của fluent api

Dưới đây là một ví dụ so sánh trực quan giữa hai cách viết:

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

Từng dòng code đọc lên tự nhiên như một câu văn tiếng Anh: "Tôi muốn GET tới URI này, gắn Header này, chấp nhận JSON, lấy về và map vào UserDto".


# xử lý lỗi thanh lịch với onstatus()

Thay vì phải try-catch RestClientResponseException khắp mọi nơi, RestClient cho phép bạn định nghĩa các hàm xử lý lỗi theo từng dải mã HTTP Status ngay trên 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);

# bước chuyển mình tương tự: jdbcclient thay thế jdbctemplate

Không dừng lại ở HTTP client, Spring Boot 3.2 còn giới thiệu JdbcClient để thay thế cho JdbcTemplate.

Trước đây, khi dùng JdbcTemplate, bạn phải tự viết RowMapper hoặc dùng BeanPropertyRowMapper chậm chạp qua Reflection:

// 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, việc truy vấn cơ sở dữ liệu và ánh xạ vào Java 21 record trở nên gọn gàng chưa từng thấy:

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

# best practices cấu hình restclient chuẩn enterprise

Để RestClient hoạt động bền bỉ trong môi trường Production, đừng bao giờ dùng RestClient.create() mặc định. Hãy cấu hình một Bean có Connection Pool và Timeout thông qua HttpComponentsClientHttpRequestFactory (Apache HttpClient 5):

package com.tungdadev.config;
 
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.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
 
import java.util.concurrent.TimeUnit;
 
@Configuration
public class RestClientConfig {
 
    @Bean
    public RestClient partnerApiClient(RestClient.Builder builder) {
        // 1. Cấu hình Connection Pool
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(200);             // Tối đa 200 connection cho toàn bộ app
        connectionManager.setDefaultMaxPerRoute(50);    // Tối đa 50 connection cho 1 host
 
        // 2. Cấu hình Timeout (Connect & Read Timeout)
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(3, TimeUnit.SECONDS)
                .setResponseTimeout(5, TimeUnit.SECONDS)
                .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();
    }
}

# tổng kết

Sự chuyển dịch từ RestTemplate sang RestClient, từ JdbcTemplate sang JdbcClient phản ánh triết lý hiện đại của hệ sinh thái Java: Đơn giản hóa trải nghiệm lập trình viên (Developer Experience), loại bỏ boilerplate code và hướng tới sự nhất quán trong toàn bộ framework.

Nếu bạn đang khởi tạo một dự án mới trên Spring Boot 3 hoặc đang tái cấu trúc các service cũ, hãy mạnh dạn gạch bỏ RestTemplate và đón nhận RestClient. Bạn sẽ nhận ra code của mình sạch sẽ, dễ bảo trì và dễ test hơn gấp nhiều lần.


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 😎 👍🏻 🚀 🔥.