Spring REST API & Data JPA
16/46
Bài 16 / 46~12 phútError, Validation & API DocsLộ trình · chặng 15/21Miễn phí… lượt xem

Jakarta Bean Validation: Kiểm Thực Khai Báo Với @Valid

Áp dụng 23 ràng buộc tích hợp của Jakarta Validation, phân biệt @Valid với @Validated và hiểu cơ chế cascade kiểm thực lồng nhau trong Spring MVC.

TL;DR: Jakarta Bean Validation 3.0 (JSR 380) cung cấp 23 constraint annotations tích hợp như @NotBlank, @Size, @Email, @Min, @Max, @Positive, @Future... Bằng cách đặt các annotation này trực tiếp lên field của DTO và gắn @Valid tại tham số controller, Spring MVC tự động kiểm thực dữ liệu trước khi method body được gọi. Nếu vi phạm ràng buộc, framework ném ra MethodArgumentNotValidException và trả về mã lỗi 400 Bad Request. Khi DTO chứa danh sách hoặc đối tượng lồng nhau, cần gắn @Valid trên thuộc tính đó để kích hoạt cơ chế cascade đệ quy. Để validate các tham số đơn lẻ như @RequestParam hay @PathVariable, controller class bắt buộc phải có annotation @Validated.

1. Tại sao kiểm thực khai báo (Declarative) vượt trội hơn kiểm tra thủ công?

Viết mã kiểm thực dữ liệu thủ công bằng các khối lệnh if/else dẫn đến nhiều bất cập:

Java
// Anti-pattern: Kiem tra thu cong lam ban business logic
@PostMapping("/orders")
public OrderDto createOrder(@RequestBody OrderRequest req) {
    if (req.customer() == null || req.customer().isBlank()) {
        throw new IllegalArgumentException("Customer name is required");
    }
    if (req.total() == null || req.total().compareTo(BigDecimal.ZERO) <= 0) {
        throw new IllegalArgumentException("Total must be positive");
    }
    if (req.items() == null || req.items().isEmpty()) {
        throw new IllegalArgumentException("Order must have at least one item");
    }
    return orderService.create(req);
}

Nhược điểm: Mã nguồn bị phân mảnh qua nhiều controller, lặp lại các quy tắc kiểm tra giống nhau và dễ bỏ sót khi thêm endpoint mới.

Ngược lại, mô hình Declarative Validation định nghĩa quy tắc ngay trên lớp DTO:

Java
public record OrderRequest(
    @NotBlank(message = "Customer name is required")
    String customer,

    @NotNull
    @Positive(message = "Total must be positive")
    BigDecimal total,

    @NotEmpty
    @Valid // Kich hoat cascade xuong tung phan tu trong list
    List<OrderItem> items
) {}

@PostMapping("/orders")
public OrderDto createOrder(@Valid @RequestBody OrderRequest req) {
    // Khong can if/else: Spring tu dong chan request truoc khi method thuc thi
    return orderService.create(req);
}

Ràng buộc trở thành một "nguồn sự thật duy nhất" (Single Source of Truth). Controller chỉ tiếp nhận những đối tượng đã được bảo đảm tính toàn vẹn dữ liệu.

2. 23 ràng buộc tích hợp trong Jakarta Validation được phân loại ra sao?

Jakarta Bean Validation 3.0 cung cấp hệ thống annotation đa dạng chia thành các nhóm chính:

Nhóm kiểm tra giá trị tồn tại (Presence & Nullability)

  • @NotNull: Giá trị không được phép là null (áp dụng cho mọi kiểu đối tượng).
  • @Null: Giá trị bắt buộc phải là null (thường kết hợp với validation groups).
  • @NotBlank: Chuỗi không được null, không rỗng và không chỉ chứa toàn ký tự khoảng trắng.
  • @NotEmpty: Áp dụng cho String, Collection, Map, Array: không được null và kích thước phải lớn hơn 0.

Nhóm kiểm tra miền số học (Numeric Ranges)

  • @Min(value) / @Max(value): Giới hạn giá trị số nguyên tối thiểu / tối đa.
  • @DecimalMin(value) / @DecimalMax(value): Giới hạn số thực hoặc BigDecimal.
  • @Positive / @PositiveOrZero: Yêu cầu số dương (> 0) hoặc không âm ($\ge 0$).
  • @Negative / @NegativeOrZero: Yêu cầu số âm (< 0) hoặc không dương ($\le 0$).
  • @Digits(integer, fraction): Giới hạn số lượng chữ số phần nguyên và phần thập phân.

Nhóm định dạng văn bản và thời gian

  • @Size(min, max): Độ dài chuỗi hoặc số phần tử trong danh sách phải nằm trong khoảng quy định.
  • @Email: Kiểm tra định dạng hòm thư điện tử hợp lệ.
  • @Pattern(regexp): Khớp với biểu thức chính quy (Regular Expression).
  • @Past / @PastOrPresent: Mốc thời gian phải ở trong quá khứ so với thời điểm hiện tại.
  • @Future / @FutureOrPresent: Mốc thời gian phải ở tương lai so với thời điểm hiện tại.
Java
public record UserRegisterRequest(
    @NotBlank @Size(min = 3, max = 50) String username,
    @NotBlank @Email String email,
    @NotNull @Past LocalDate dateOfBirth,
    @Pattern(regexp = "^\\+?[0-9]{10,12}$", message = "So dien thoai khong hop le") String phone
) {}

3. Cơ chế kích hoạt MethodArgumentNotValidException vận hành như thế nào?

Annotation @Valid tại controller method tham số là tín hiệu để HandlerMethodArgumentResolver kích hoạt chu trình kiểm thực:

Một request POST orders sau khi Jackson dựng object thì rẽ ở câu hỏi parameter có @Valid không: nhánh có thì DataBinder gọi Hibernate Validator và ném MethodArgumentNotValidException trả 400, nhánh không thì validate không bao giờ được gọi và method body chạy với object invalid mà không có exception hay log nào

Luồng xử lý 6 bước bên dưới:

  1. RequestResponseBodyMethodProcessor dùng Jackson deserialize JSON body thành đối tượng DTO.
  2. Phát hiện annotation @Valid, resolver lập tức gọi DataBinder.validate().
  3. Hibernate Validator quét qua các trường của DTO và thực thi logic kiểm tra tương ứng.
  4. Nếu có trường vi phạm, DataBinder thu thập toàn bộ các FieldError vào đối tượng BindingResult.
  5. Framework ném ra ngoại lệ MethodArgumentNotValidException.
  6. Tầng @RestControllerAdvice bắt ngoại lệ này và định dạng phản hồi HTTP 400 Bad Request chuẩn RFC 9457.

4. Làm sao để kích hoạt kiểm thực lồng nhau (Cascade Validation) với @Valid?

Khi một DTO chứa các DTO con (quan hệ 1-1 hoặc 1-N), mặc định Hibernate Validator sẽ không tự động kiểm tra các thuộc tính bên trong đối tượng con trừ khi ta gắn thêm @Valid:

Java
public record OrderRequest(
    @NotBlank String customer,

    @NotEmpty
    @Valid // BAT BUOC: Kich hoat cascade de validate tung phan tu trong list
    List<OrderItem> items
) {}

public record OrderItem(
    @NotBlank String sku,
    @Min(1) @Max(100) int quantity,
    @NotNull @Positive BigDecimal unitPrice
) {}

Nếu thiếu @Valid trước List<OrderItem>, người dùng gửi quantity = -5 hay sku = "" thì hệ thống vẫn bỏ qua kiểm tra, dẫn đến việc dữ liệu sai lọt vào database.

5. Điểm khác biệt cốt lõi giữa @Valid và @Validated là gì?

Đây là hai annotation đến từ hai hệ sinh thái khác nhau và có vai trò bổ trợ:

Đặc tính so sánh@Valid@Validated
Gói thư việnjakarta.validation.Validorg.springframework.validation.annotation.Validated
Cơ chế bên dướiDataBinder thông qua ArgumentResolverDynamic Proxy / AOP Interceptor (MethodValidationPostProcessor)
Phạm vi áp dụngTham số Controller method, Fields của DTOĐặt trên Controller Class, Service Class, Method
Hỗ trợ Validation GroupsKhông hỗ trợCó hỗ trợ (@Validated(OnCreate.class))
Ngoại lệ phát sinhMethodArgumentNotValidExceptionConstraintViolationException

Hai cột song song: cột trái @Valid trên @RequestBody đi qua ArgumentResolver gọi DataBinder rồi ném MethodArgumentNotValidException, cột phải @RequestParam và @PathVariable đi qua AOP proxy với MethodValidationInterceptor rồi ném ConstraintViolationException, cả hai cùng đổ vào một hộp nói advice phải có handler cho cả hai

Áp dụng @Validated cho Query Params và Path Variables

Để các ràng buộc trên @RequestParam hay @PathVariable có hiệu lực, bạn bắt buộc phải gắn @Validated tại cấp độ Class:

Java
@RestController
@Validated // Bat buoc de Spring tao AOP Proxy kiem thuc cac method arguments
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping
    public List<OrderDto> getOrders(
        @RequestParam @Min(0) int page,
        @RequestParam @Min(1) @Max(100) int size
    ) {
        return orderService.findOrders(page, size);
    }

    @GetMapping("/{id}")
    public OrderDto getOrderById(@PathVariable @NotNull @Positive Long id) {
        return orderService.findById(id);
    }
}

6. Những cạm bẫy âm thầm thường gặp khi áp dụng Bean Validation là gì?

Cạm bẫy 1: Quên khai báo dependency trong Spring Boot 3

Kể từ Spring Boot 3, mô-đun validation không còn nằm sẵn trong spring-boot-starter-web. Bạn phải thêm thủ công:

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

Nếu thiếu dependency này, các annotation validation vẫn biên dịch thành công nhưng lúc chạy thực tế sẽ bị bỏ qua hoàn toàn mà không hề có cảnh báo.

Cạm bẫy 2: Nhầm lẫn giữa @NotEmpty và @NotBlank

  • Chuỗi " " (khoảng trắng): Thoát khỏi @NotEmpty nhưng bị chặn lại bởi @NotBlank. Do đó, luôn ưu tiên sử dụng @NotBlank cho các trường dạng văn bản.

7. Bean Validation kết nối với các bài học khác trong hệ sinh thái ra sao?

  • Problem Details RFC 9457: Bắt MethodArgumentNotValidException và bóc tách các trường vi phạm để gắn vào thuộc tính violations trong tài liệu lỗi.
  • Validation Nâng Cao: Nghiên cứu Validation Groups để phân biệt quy tắc giữa tạo mới (POST) và cập nhật (PUT), đồng thời tự xây dựng Custom Constraint annotations.

8. Những điểm then chốt cần ghi nhớ là gì?

  • Khai báo ràng buộc tập trung trên DTO bằng các annotation chuẩn của Jakarta Validation.
  • Luôn gắn @Valid trước @RequestBody để framework kích hoạt kiểm tra tính hợp lệ.
  • Sử dụng @Valid trên các thuộc tính lồng nhau để đảm bảo cơ chế cascade đệ quy hoạt động.
  • Gắn @Validated tại controller class khi cần kiểm thực các tham số đơn lẻ như @RequestParam và @PathVariable.
  • Thêm spring-boot-starter-validation vào file build của dự án Spring Boot 3.

9. Bài tập tự kiểm tra mức độ hiểu sâu

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao trong Spring Boot 3, một DTO có gắn đầy đủ @NotBlank và @NotNull nhưng khi client gửi dữ liệu rỗng lên API vẫn không bị chặn lại?
  2. Q2
    Annotation nào sau đây có khả năng chặn được chuỗi chỉ chứa toàn ký tự khoảng trắng (ví dụ: ' ')?
  3. Q3
    Để kích hoạt việc kiểm thực các ràng buộc trên một danh sách các đối tượng con List<OrderItem> nằm trong OrderRequest DTO, lập trình viên cần làm gì?
  4. Q4
    Khi cần kiểm tra tham số @RequestParam @Min(1) int page trong một phương thức GET, annotation nào bắt buộc phải có trên Controller Class?
  5. Q5
    Khi validation thất bại trên tham số có gắn @Valid @RequestBody, Spring MVC sẽ ném ra ngoại lệ nào?

Bài tiếp theo: Validation groups, custom constraint & i18n

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 15/21 · REST API với Spring MVC — bài 16/21 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Validation Nâng Cao: Groups, Custom Constraint và i18n