Backend

99 Ngày Spring — Ngày 67: Custom validation

SSite Admin
2 tháng 10, 2026 2 phút đọc 6 lượt xem
99 Ngày Spring — Ngày 67: Custom validation

@NotNull và @Size xử lý nhiều quy tắc đơn giản. Khi điều kiện phụ thuộc hai field hoặc quy tắc miền, hãy viết constraint riêng để tái sử dụng và kiểm thử. Trong Spring Boot 3.x, Bean Validation dùng namespace jakarta.validation; controller cần @Valid hoặc @Validated ở điểm nhận dữ liệu.

Custom validation

1. Khai báo constraint

Annotation có @Constraint(validatedBy=...), @Target và @Retention(RUNTIME). Giữ message, groups và payload theo hợp đồng Bean Validation. Đặt annotation ở TYPE nếu cần xem nhiều field của cùng DTO; không ép một validator cấp field đọc ngầm field khác.

@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = DateRangeValidator.class)
public @interface ValidDateRange {
    String message() default "{booking.dateRange.invalid}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

2. Kiểm tra liên trường

ConstraintValidator<A,T> nhận annotation và giá trị DTO. Với khoảng ngày, hãy tách việc null cho @NotNull ở từng field, rồi chỉ so sánh khi cả hai có giá trị. Đưa lỗi về endDate để client biết sửa đâu; sau khi tắt default violation, phải tạo ít nhất một violation mới khi trả false.

public boolean isValid(Booking b, ConstraintValidatorContext c) {
    if (b == null || b.startDate() == null || b.endDate() == null)
        return true;
    if (b.endDate().isAfter(b.startDate())) return true;
    c.disableDefaultConstraintViolation();
    c.buildConstraintViolationWithTemplate("{booking.dateRange.invalid}")
        .addPropertyNode("endDate").addConstraintViolation();
    return false;
}

3. Kích hoạt tại API

Đặt @Valid trên @RequestBody để Spring MVC kiểm tra DTO. Lỗi validation mặc định thành phản hồi 400; khi chuẩn hóa lỗi, giữ field và mã thông điệp ổn định cho client. Không dùng validation như biện pháp duy nhất bảo vệ invariant trong database: ghi từ nguồn khác vẫn có thể bỏ qua controller.

@ValidDateRange
record Booking(@NotNull LocalDate startDate,
               @NotNull LocalDate endDate) {}
@PostMapping("/bookings")
ResponseEntity<?> create(@Valid @RequestBody Booking body) {
    return service.create(body);
}

4. Test cả biên và thông điệp

Thử ngày kết thúc bằng ngày bắt đầu, trước ngày bắt đầu, null và khoảng hợp lệ. Dùng Validator để unit test constraint, rồi gửi HTTP thật để xác nhận @Valid và dạng lỗi. Nếu rule cần truy vấn database, xử lý race condition bằng unique constraint hoặc transaction; validator chỉ cho phản hồi sớm.

  • Thông điệp nên là key trong messages.properties để ngày 68 dịch được.

  • Không đưa dữ liệu nhạy cảm vào nội dung lỗi.

Constraint tốt biểu diễn một quy tắc rõ ràng, trả lỗi tại đúng chỗ và được kiểm thử qua HTTP. Ngày 68 sẽ dịch các thông điệp ấy theo locale.

Tài liệu đối chiếu

  • https://docs.spring.io/spring-framework/reference/core/validation/beanvalidation.html

  • https://jakarta.ee/specifications/bean-validation/3.0/jakarta-bean-validation-spec-3.0.html

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 67: Concurrent collections

ConcurrentHashMap, CopyOnWriteArrayList và BlockingQueue: chọn theo kiểu truy cập và giới hạn tải.

2 thg 10, 20262 phút4
99 Ngày Spring — Ngày 66: Gọi API ngoài bằng RestClient và WebClient

Chọn client đồng bộ hoặc bất đồng bộ, đặt timeout, xử lý lỗi và retry có kiểm soát.

1 thg 10, 20262 phút2
99 Ngày Java — Ngày 66: Lock và deadlock

Dùng ReentrantLock, tryLock và thứ tự khóa cố định để tránh deadlock trên Java 21.

1 thg 10, 20262 phút1