Cách sử dụng gói Validation trong Spring Boot để kiểm tra tham số đầu vào? - win 911

| Apr 12, 2025 min read

Ngày 08 tháng 01 năm 2024 - Máy tính

Gói spring-boot-starter-validation của Spring Boot hỗ trợ việc kiểm tra tham số đầu vào bằng cách sử dụng các chú thích chuẩn. Gói spring-boot-starter-validation chủ yếu tham chiếu đến gói hibernate-validator, và chức năng kiểm tra tham số chính là do gói hibarnate-validator cung cấp.

Bài viết này sẽ tập trung vào việc sử dụng các chú thích chuẩn được bao gồm trong gói spring-boot-starter-validation, cách bắt lỗi kiểm tra và hiển thị chúng, cách sử dụng chức năng kiểm tra theo nhóm, cũng như cách sử dụng trình kiểm tra tùy chỉnh.

Dự án ví dụ trong bài viết này sử dụng Maven để quản lý phụ thuộc. Sau đây là phiên bản JDK, Maven và Spring Boot được sử dụng khi viết bài viết này:

JDK: Amazon Corretto 17.0.8
Maven: 3.9.2
Spring Boot: 3.2.1

Bài viết này sẽ minh họa cách sử dụng gói Validation thông qua việc phát triển một API RESTful cho User. Do đó, tệp pom.xml ngoài việc cần phải thêm phụ thuộc spring-boot-starter-validation:

<!-- pom.xml -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Cũng cần thêm phụ thuộc spring-boot-starter-web:

<!-- pom.xml -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Để tiết kiệm thời gian viết Getters và Setters cho lớp Model, bài viết này còn sử dụng phụ thuộc lombok:

<!-- pom.xml -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>

Sau khi đã chuẩn bị xong các phụ thuộc, chúng ta có thể bắt đầu sử dụng gói Validation.

1 Sử dụng các chú thích chuẩn của Validation

Dưới đây là danh sách một số chú thích thường dùng trong gói spring-boot-starter-validation.

Chú thích Loại trường Mô tả
@Null Bất kỳ loại nào Kiểm tra giá trị phần tử là null
@NotNull Bất kỳ loại nào Kiểm tra giá trị phần tử không là null, không thể kiểm tra chuỗi rỗng
@NotBlank Loại con của CharSequence Kiểm tra giá trị phần tử không trống (không là null và không là chuỗi rỗng)
@NotEmpty Loại con của CharSequence, Collection, Map, mảng Kiểm tra giá trị phần tử không là null và không trống (chiều dài chuỗi hoặc kích thước tập hợp không bằng 0)
@Min Bất kỳ loại Number nào Kiểm tra giá trị phần tử lớn hơn hoặc bằng giá trị được chỉ định bởi @Min
@Max Bất kỳ loại Number nào Kiểm tra giá trị phần tử nhỏ hơn hoặc bằng giá trị được chỉ định bởi @Max
@Digits Bất kỳ loại Number nào Kiểm tra giới hạn số chữ số nguyên và số thập phân của giá trị phần tử
@Size Chuỗi, Collection, Map, mảng, v.v. Kiểm tra giá trị phần tử nằm trong khoảng được chỉ định, chẳng hạn như chiều dài ký tự hoặc kích thước tập hợp
@Range Loại số Kiểm tra giá trị phần tử nằm giữa giá trị tối thiểu và tối đa
@Email Loại con của CharSequence Kiểm tra giá trị phần tử là định dạng email
@Pattern Loại con của CharSequence Kiểm tra giá trị phần tử khớp với biểu thức chính quy được chỉ định
@Valid Bất kỳ loại không nguyên tử nào Chỉ định kiểm tra đệ quy đối tượng liên quan

Tiếp theo, chúng ta sẽ xem xét cách sử dụng những chú thích này. Giả sử chúng ta muốn tạo một API RESTful để tạo User, và khi tạo User, một số trường có quy tắc kiểm tra (chẳng hạn như: bắt buộc, thỏa mãn yêu cầu về độ dài chuỗi, thỏa mãn định dạng email, thỏa mãn biểu thức chính quy, v.v.).

Dưới đây là mã nguồn của lớp User Model sử dụng các chú thích Validation:

// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class User {
    @NotNull(message = "name can not be null")
    @Size(min = 2, max = 20, message = "name length should be in the range [2, 20]")
    private String name;

    @NotNull(message = "age can not be null")
    @Range(min = 18, max = 100, message = "age should be in the range [18, 100]")
    private Integer age;

    @NotNull(message = "email can not be null")
    @Email(message = "email invalid")
    private String email;

    @NotNull(message = "phone can not be null")
    @Pattern(regexp = "^1[3-9][0-9]{9}$", message = "phone number invalid")
    private String phone;
}

Dưới đây là phân tích từng quy tắc kiểm tra cho mỗi trường trong mô hình User:

  • name: Là kiểu chuỗi, sử dụng các chú thích @NotNull@Size, nghĩa là trường này bắt buộc và độ dài chuỗi phải nằm trong khoảng [2, 20].
  • age: Là kiểu số nguyên, sử dụng các chú thích @NotNull@Range, nghĩa là trường này bắt buộc và giá trị phải nằm trong khoảng [18, 100].
  • email: Là kiểu chuỗi, sử dụng các chú thích @NotNull@Email, nghĩa là trường này bắt buộc và phải là định dạng email.
  • phone: Là kiểu chuỗi, sử dụng các chú thích @NotNull@Pattern, nghĩa là trường này bắt buộc và phải là định dạng số điện thoại hợp lệ trong nước.

Dưới đây là mã nguồn của lớp ErrorMessage trả về lỗi thống nhất:

// src/main/java/com/example/demo/model/ErrorMessage.java
package com.example.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ErrorMessage {
    private String code;
    private String description;
}

Cuối cùng, dưới đây là mã nguồn của lớp UserController:

// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
import com.example.demo.model.ErrorMessage;
import com.example.demo.model.User;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping("")
    public ResponseEntity<?> addUser(@RequestBody @Valid User user, BindingResult result) {
        if (result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if (!allErrors.isEmpty()) {
                ObjectError error = allErrors.get(0);
                String description = error.getDefaultMessage();
                return ResponseEntity.badRequest().body(new ErrorMessage("validation_failed", description));
            }
        }
        // userService.addUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

Như có thể thấy, phương thức addUser của UserController sử dụng lớp User Model để nhận dữ liệu từ yêu cầu, và trước lớp User Model có sử dụng chú thích @Valid, chú thích này sẽ tự động kiểm tra các trường trong User Model theo quy tắc được đặt ra bởi các chú thích. Ngoài ra, phương thức addUser còn có một tham số khác là BindingResult, tham số này sẽ thu thập tất cả các thông tin lỗi kiểm tra trường, bài viết này chỉ trả về lỗi đầu tiên theo định dạng ErrorMessage, nếu không có lỗi nào thì sẽ trả về trạng thái mã 201.

Dưới đây là thử nghiệm giao diện bằng lệnh CURL:

curl -L \
 -X POST \
 -H "Content-Type: application/json" \
 -d '{"name": "Larry", "age": 18, "email": "larry@qq.com"}'
// 400
{ "code": "validation_failed", "description": "phone can not be null" }

Như có thể thấy, nếu có trường không đáp ứng quy tắc kiểm tra, nó sẽ trả về thông báo lỗi được thiết lập.

Nếu lớp Model có chứa đối tượng lồng nhau, chúng ta cần làm gì để kiểm tra? Chỉ cần thêm chú thích @Valid vào trường tương ứng là đủ. Ví dụ, nếu trong mô hình User có một trường là address, đây là đối tượng Address, mã nguồn của lớp Address như sau:

// src/main/java/com/example/demo/model/Address.java
package com.example.demo.model;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class Address {
    @NotNull(message = "province can not be null")
    @Size(min = 2, max = 100, message = "province length should be in the range [10, 100]")
    private String province;

    @NotNull(message = "city can not be null")
    @Size(min = 2, max = 100, message = "city length should be in the range [10, 100]")
    private String city;

    @NotNull(message = "street can not be null")
    @Size(min = 10, max = 1000, message = "street length should be in the range [10, 1000]")
    private String street;
}

Trong mô hình User, nếu muốn áp dụng quy tắc kiểm tra cho trường address, cần thêm chú thích @Valid vào trường này:

// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class User {
    ...
    @Valid
    @NotNull(message = "address can not be null")
    private Address address;
}

Hiểu cách sử dụng các chú thích phổ biến của gói Validation, tiếp theo chúng ta sẽ xem xét cách xử lý và hiển thị ngoại lệ lỗi kiểm tra.

2 Xử lý và hiển thị ngoại lệ lỗi kiểm tra

Chúng ta lưu ý rằng, trong ví dụ trên, phương thức addUser của UserController sử dụng một tham số bổ sung là BindingResult để nhận thông tin lỗi kiểm tra, sau đó hiển thị cho người gọi theo nhu cầu. Tuy nhiên, cách xử lý này hơi dư thừa, mỗi phương thức yêu cầu đều cần thêm tham số này và viết lại logic trả về lỗi.

Thực tế là nếu không thêm tham số này, khi có lỗi kiểm tra, khung Spring Boot sẽ ném ra một ngoại lệ MethodArgumentNotValidException. Vì vậy, cách xử lý đơn giản hơn là sử dụng chú thích @RestControllerAdvice để đánh dấu một lớp là lớp xử lý ngoại lệ toàn cục, đối với MethodArgumentNotValidException, chỉ cần thực hiện xử lý thống nhất trong lớp xử lý ngoại lệ này là đủ.

Mã nguồn của lớp xử lý ngoại lệ MyExceptionHandler như sau:

// src/main/java/com/example/demo/exception/MyExceptionHandler.java
package com.example.demo.exception;
import com.example.demo.model.ErrorMessage;
import org.springframework.http.HttpStatus;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;

@RestControllerAdvice
public class MyExceptionHandler {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ErrorMessage handleValidationExceptions(MethodArgumentNotValidException ex) {
        List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
        if (!allErrors.isEmpty()) {
            ObjectError error = allErrors.get(0);
            String description = error.getDefaultMessage();
            return new ErrorMessage("validation_failed", description);
        }
        return new ErrorMessage("validation_failed", "validation failed");
    }
}

Sau khi có lớp xử lý ngoại lệ này, mã nguồn của UserController có thể trở nên rất gọn gàng:

// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
...
@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping("")
    public ResponseEntity<?> addUser(@RequestBody @Valid User user) {
        // userService.addUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

Sử dụng cách này, hiệu quả đối với phía gọi vẫn giống như trước đây:

# Sử dụng lệnh CURL để tạo một User mới (không cung cấp tham số phone)
curl -L \
 -X POST \
 -H "Content-Type: application/json" \
 -d '{"name": "Larry", "age": 18, "email": "larry@qq.com"}'
// Trả về mã trạng thái 400, cùng với thông báo lỗi sau
{ "code": "validation_failed", "description": "phone can not be null" }

Sau khi học cách xử lý lỗi kiểm tra bằng lớp xử lý ngoại lệ thống nhất, tiếp theo chúng ta sẽ xem xét cách sử dụng chức năng kiểm tra theo nhóm.

3 Sử dụng chức năng kiểm tra theo nhóm

Chức năng kiểm tra theo nhóm cho phép áp dụng các quy tắc kiểm tra khác nhau cho cùng một Model trong các tình huống khác nhau. Dưới đây chúng ta sẽ thử sử dụng cùng một mô hình User để nhận dữ liệu yêu cầu cho cả việc thêm mới và cập nhật, nhưng chỉ định các nhóm khác nhau cho các trường để phân biệt quy tắc kiểm tra giữa thêm mới và cập nhật.

Mã nguồn của mô hình User như sau:

// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.validation.groups.Default;
import lombok.Data;
import org.hibernate.validator.constraints.Range;

@Data
public class User {
    @NotNull(message = "id can not be null", groups = Update.class)
    private Long id;

    @NotNull(message = "name can not be null", groups = Add.class)
    @Size(min = 2, max = 20, message = "name length should be in the range [2, 20]")
    private String name;

    @NotNull(message = "age can not be null", groups = Add.class)
    @Range(min = 18, max = 100, message = "age should be in the range [18, 100]")
    private Integer age;

    @NotNull(message = "email can not be null", groups = Add.class)
    @Email(message = "email invalid")
    private String email;

    @NotNull(message = "phone can [win 911](/post/8207/)  not be null", groups = Add.class)
    @Pattern(regexp = "^1[3-9][0-9]{9}$", message = "phone number invalid")
    private String phone;

    public interface Add extends Default {}
    public interface Update extends Default {}
}

Như có thể thấy, chúng ta cwin222 đã định nghĩa hai nhóm trong mô hình User: AddUpdate. Mỗi trường đều có một chú thích @NotNull, nhưng trường id thuộc nhóm Update.class, các trường khác thuộc nhóm Add.class, các chú thích khác thì không chỉ định nhóm (có nghĩa là áp dụng cho tất cả). Nghĩa là yêu cầu: khi thêm mới, name, age, email, phone là các trường bắt buộc; khi cập nhật, id là trường bắt buộc; và bất kể thêm mới hay cập nhật, nếu cung cấp các trường tương ứng, cần phải thỏa mãn quy tắc kiểm tra của các trường đó.

Dưới đây là mã nguồn của UserController:

// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
...

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping("")
    public ResponseEntity<?> addUser(@RequestBody @Validated(User.Add.class) User user) {
        // userService.addUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    @PatchMapping("")
    public ResponseEntity<?> updateUser(@RequestBody @Validated(User.Update.class) User user) {
        // userService.updateUser(user);
        return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
    }
}

Như có thể thấy, giao diện thêm mới User và cập nhật User sử dụng cùng một mô hình User; nhưng giao diện thêm mới sử dụng nhóm User.Add.class, giao diện cập nhật sử dụng nhóm User.Update.class.

Lưu ý: Khi chỉ định nhóm ở đây sử dụng chú thích @Validated, trong khi trước đó sử dụng chú thích @Valid, tại đây chúng tôi sẽ giải thích sự khác biệt giữa hai chú thích này. Chú thích @Validated là của khung Spring, trong khi chú thích @Valid là của gói jakarta.validation, chú thích @Validated có thể chỉ định nhóm, trong khi chú thích @Valid thì không có chức năng này.

Dưới đây là thử nghiệm cập nhật User mà không cung cấp trường id:

curl -L \
 -X PATCH \
 -H "Content-Type: application/json" \
 -d '{"name": "Larry", "age": 18, "email": "larry@qq.com"}'

Sẽ trả về lỗi sau:

// 400
{ "code": "validation_failed", "description": "id can not be null" }

Sau khi giới thiệu xong cách sử dụng chức năng kiểm tra theo nhóm, tiếp theo chúng ta sẽ xem xét cách sử dụng trình kiểm tra tùy chỉnh.

4 Sử dụng trình kiểm tra tùy chỉnh

Nếu các chú thích sẵn có trong gói Validation không đáp ứng được nhu cầu kiểm tra của bạn, bạn có thể tự định nghĩa một chú thích và thực hiện logic kiểm tra tương ứng.

Dưới đây là chú thích tùy chỉnh CustomValidation, mã nguồn của nó như sau:

package com.example.demo.validation;
...
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomValidator.class)
public @interface CustomValidation {
    String message() default "Invalid value";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Như mã nguồn trên, @Target chỉ định phạm vi hoạt động của chú thích, trong ví dụ này, có nghĩa là chú thích này có thể áp dụng cho phương thức hoặc trường; @Retention chỉ định thời gian tồn tại của chú thích, trong ví dụ này, có nghĩa là chú thích này có thể sử dụng trong thời gian chạy; @Constraint chỉ định lớp xử lý của chú thích.

Lớp xử lý CustomValidator được sử dụng để viết logic kiểm tra tùy chỉnh, mã nguồn của nó như sau:

package com.example.demo.validation;
...
public class CustomValidator implements ConstraintValidator<CustomValidation, String> {

    @Override
    public void initialize(CustomValidation constraintAnnotation) {}

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return null != value && value.startsWith("ABC");
    }
}

Như có thể thấy, CustomValidator triển khai ConstraintValidator<CustomValidation, String>, nghĩa là trường được đánh dấu là kiểu String; phương thức initialize() được sử dụng để khởi tạo trình kiểm tra, có thể truy cập các thuộc tính khác nhau của chú thích theo nhu cầu; phương thức isValid() có thể lấy giá trị của trường được kiểm tra, để viết logic kiểm tra thực tế.

Dưới đây là cách sử dụng chú thích tùy chỉnh này trong mô hình User:

package com.example.demo.model;
...
@Data
public class User {
    @CustomValidation(message = "testField invalid")
    private String testField;
}

Như vậy, khi giá trị của trường này không đáp ứng quy tắc kiểm tra tùy chỉnh, sẽ nảy sinh lỗi tương ứng:

// 400
{ "code": "validation_failed", "description": "testField invalid" }

Tóm lại, bài viết này đã giới thiệu chi tiết cách sử dụng gói spring-boot-starter-validation thông qua mã nguồn ví dụ. Mã nguồn chính được đề cập trong bài viết đã được gửi lên GitHub cá nhân của tôi, chào đón mọi người theo dõi hoặc Fork.

[1] Kiểm tra Đầu vào Biểu mẫu | Spring - spring.io [2] Kiểm tra trong Spring Boot | Medium - medium.com [3] Kiểm tra Tham số Dự án Spring Boot (Validator) | Blog CSDN - blog.csdn.net [4] Sử dụng Spring Validation để Kiểm tra Tham số trong Spring Boot | Hiếm Đất Nép - juejin.cn [5] Sử dụng Validation để Kiểm tra Tham số trong Spring Boot | Blog Garden - www.cnblogs.com [6] Sự Khác Nhau Giữa @Valid và @Validated trong Spring | Stackoverflow - stackoverflow.com

#Spring #Java