[Translation] Spring 6 input parameter data validation: a comprehensive guide

原文地址:Spring 6 Programmatic Validator: A Comprehensive Guide

I. Introduction

In Spring 6.1, there is an important improvement that is very noteworthy - programmatic validator implementation. Spring has long supported declarative validation via annotations, and Spring 6.1 introduces this powerful enhancement by providing a dedicated programmatic validation method.

Programmatic verification allows developers to have fine-grained control over the verification process, enabling dynamic and conditional verification scenarios beyond the capabilities of declarative approaches. In this tutorial, we'll dive into the details of implementing programmatic validation and seamlessly integrating it with Spring MVC controllers.

2. The difference between declarative verification and programmatic verification

For data validation, the Spring Framework has two main approaches: declarative validation and programmatic validation.

"Declarative validation(Declarative validation)"Specify validation rules through metadata or annotations on domain objects. Spring leverages JavaBean Validation (JSR 380) annotations such as @NotNull, @Size, and @Pattern to declare validation constraints directly in the class definition.

Spring automatically triggers validation during data binding (for example, during Spring MVC form submission). Developers do not need to explicitly call validation logic in their code.

public class User {
    
    

  @NotNull
  private String username;
  
  @Size(min = 6, max = 20)
  private String password;
  // ...
}

On the other hand, "Programmatic validation (Programmatic validation)" writes custom validation logic in code, usually using Spring's Validator interface. This approach enables more dynamic and complex verification scenarios.

The developer is responsible for explicitly calling the validation logic, usually in the service layer or controller.

public class UserValidator implements Validator {
    
    

  @Override
  public boolean supports(Class<?> clazz) {
    
    
    return User.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    
    
    User user = (User) target;
    // 自定义验证逻辑, 可以读取多个字段进行混合校验,编程的方式灵活性大大增加
  }
}

3. When to use programmatic verification

The choice between declarative and programmatic validation depends on the specific requirements of the use case.

Declarative validation is usually suitable for simpler scenarios, and validation rules can be clearly expressed through comments. Declarative verification is convenient and conforms to the convention of over-configuration.

Programmatic verification provides greater flexibility and control for complex verification scenarios that go beyond the scope of declarative expressions. Programmatic validation is especially useful when validation logic depends on dynamic conditions or involves interactions between multiple fields.

We can combine these two methods. We can leverage the simplicity of declarative validation to handle common situations, and use programmatic validation when facing more complex requirements.

4. Introduction to Programmatic Validator API

The core of the programmatic validator API in Spring is to allow the creation of custom validator classes and define validation rules that may not be easily captured by annotations alone.

The following are the general steps for creating a custom validator object.

  • Create a class that implements the org.springframework.validation.Validator interface.
  • Overload the supports() method to specify which classes this validator supports.
  • Implements the validate() or validateObject() method to define the actual validation logic.
  • Reject the given field with the given error code using the ValidationUtils.rejectIfEmpty() or ValidationUtils.rejectIfEmptyOrWhitespace() method.
  • We can directly call the Errors.rejectValue() method to add other types of errors.
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

@Component
public class UserValidator implements Validator {
    
    

  @Override
  public boolean supports(Class<?> clazz) {
    
    
      return User.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    
    

    User user = (User) target;

    // 例如: 校验 username 不能为空
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, 
      "username", "field.required", "Username must not be empty.");

    // 添加更多的自定义验证逻辑
  }
}

To use a custom validator, we can inject it into a Spring component like @Controller or @Service, or instantiate it directly. We then call the validation method, passing the object to validate and the Errors object to collect validation errors.

public class UserService {
    
    

  private Validator userValidator;

  public UserService(Validator userValidator) {
    
    
    this.userValidator = userValidator;
  }

  public void someServiceMethod(User user) {
    
    

    Errors errors = new BeanPropertyBindingResult(user, "user");
    userValidator.validate(user, errors);

    if (errors.hasErrors()) {
    
    
      // 处理数据校验错误
    }
  }
}

5. Initial installation

5.1. Maven configuration

To use the programmatic validator we need Spring Framework 6.1 or Spring Boot 3.2 as these are the minimum supported versions.

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.0</version>
  <relativePath/>
</parent>

5.2. Domain objects

The domain objects for this tutorial are the Employee and Department objects. We won't create complex structures so we can focus on core concepts.

Employee.java

package demo.customValidator.model;

@Data
@Builder
public class Employee {
    
    

  Long id;
  String firstName;
  String lastName;
  String email;
  boolean active;

  Department department;
}

Department.java

package demo.customValidator.model;

@Data
@Builder
public class Department {
    
    

  Long id;
  String name;
  boolean active;
}

6. Implement programmatic validator

The following EmployeeValidator class implements the org.springframework.validation.Validator interface and implements the necessary methods. It will add validation rules in the Employee fields as needed.

package demo.customValidator.validator;

import demo.customValidator.model.Employee;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class EmployeeValidator implements Validator {
    
    

  @Override
  public boolean supports(Class<?> clazz) {
    
    
    return Employee.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    
    

    ValidationUtils.rejectIfEmpty(errors, "id", ValidationErrorCodes.ERROR_CODE_EMPTY);
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "First name cannot be empty");
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "Last name cannot be empty");
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "Email cannot be empty");

    Employee employee = (Employee) target;

    if (employee.getFirstName() != null && employee.getFirstName().length() < 3) {
    
    
      errors.rejectValue("firstName", "First name must be greater than 2 characters");
    }

    if (employee.getLastName() != null && employee.getLastName().length() < 3) {
    
    
      errors.rejectValue("lastName", "Last name must be greater than 3 characters");
    }
  }
}

Likewise, we have defined validators for the Department class. If necessary, you can add more complex validation rules.

package demo.customValidator.model.validation;

import demo.customValidator.model.Department;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class DepartmentValidator implements Validator {
    
    

  @Override
  public boolean supports(Class<?> clazz) {
    
    
    return Department.class.equals(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    
    

    ValidationUtils.rejectIfEmpty(errors, "id", ValidationErrorCodes.ERROR_CODE_EMPTY);
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "Department name cannot be empty");

    Department department = (Department) target;

    if(department.getName() != null && department.getName().length() < 3) {
    
    
      errors.rejectValue("name", "Department name must be greater than 3 characters");
    }
  }
}

Now we can verify instances of the Employee and Department objects as follows:

Employee employee = Employee.builder().id(2L).build();
//Aurowire if needed
EmployeeValidator employeeValidator = new EmployeeValidator();

Errors errors = new BeanPropertyBindingResult(employee, "employee");
employeeValidator.validate(employee, errors);

if (!errors.hasErrors()) {
    
    
  System.out.println("Object is valid");
} else {
    
    
  for (FieldError error : errors.getFieldErrors()) {
    
    
    System.out.println(error.getCode());
  }
}

Program output:

First name cannot be empty
Last name cannot be empty
Email cannot be empty

DepartmentObjects can be similarly validated.

7. Chain multiple validators

In the above custom validator, if we validate the employee object, then the API will not validate the department object. Ideally, when validating a specific object, validation should be performed against all associated objects.

The programmatic validation API allows calling other validators, summarizing all errors, and finally returning the results. This functionality is achieved using the ValidationUtils.invokeValidator() method as follows:

public class EmployeeValidator implements Validator {
    
    

  DepartmentValidator departmentValidator;

  public EmployeeValidator(DepartmentValidator departmentValidator) {
    
    
    if (departmentValidator == null) {
    
    
      throw new IllegalArgumentException("The supplied Validator is null.");
    }
    if (!departmentValidator.supports(Department.class)) {
    
    
      throw new IllegalArgumentException("The supplied Validator must support the Department instances.");
    }
    this.departmentValidator = departmentValidator;
  }

  @Override
  public void validate(Object target, Errors errors) {
    
    

    //...

    try {
    
    
      errors.pushNestedPath("department");
      ValidationUtils.invokeValidator(this.departmentValidator, employee.getDepartment(), errors);
    } finally {
    
    
      errors.popNestedPath();
    }
  }
}
  • pushNestedPath()The method allows setting temporary nested paths for child objects. In the above example, when validating the department object, the path is set to employee.department.
  • The method resets the path to the original path before calling the pushNestedPath() method. In the above example, it resets the path to again. popNestedPath()employee

Now, when we validate the Employee object, we can also see the validation errors for the Department object.

Department department = Department.builder().id(1L).build();
Employee employee = Employee.builder().id(2L).department(department).build();

EmployeeValidator employeeValidator = new EmployeeValidator(new DepartmentValidator());

Errors errors = new BeanPropertyBindingResult(employee, "employee");
employeeValidator.validate(employee, errors);

if (!errors.hasErrors()) {
    
    
  System.out.println("Object is valid");
} else {
    
    
  for (FieldError error : errors.getFieldErrors()) {
    
    
    System.out.println(error.getField());
    System.out.println(error.getCode());
  }
}

Program output:

firstName
First name cannot be empty

lastName
Last name cannot be empty

email
Email cannot be empty

department.name
Department name cannot be empty

Note that the printed field name is department.name. Since the pushNestedPath() method is used, the department. prefix is ​​added.

8. Use MessageSource with message parsing function

Using hardcoded messages is not a good idea, so we can add the message to a resource file (such as messages.properties) and then use MessageSource.getMessage() Improve this code further by parsing the message into the desired local language.

For example, let's add the following message to the resource file:

error.field.empty={
    
    0} cannot be empty
error.field.size={
    
    0} must be between 3 and 20

For unified access, please add the following code in the constant file. Note that these error codes are added in the custom validator implementation.

public class ValidationErrorCodes {
    
    
  public static String ERROR_CODE_EMPTY = "error.field.empty";
  public static String ERROR_CODE_SIZE = "error.field.size";
}

Now, when weparse the information, we will get the information of the properties file.

MessageSource messageSource;

//...

if (!errors.hasErrors()) {
    
    
  System.out.println("Object is valid");
} else {
    
    
  for (FieldError error : errors.getFieldErrors()) {
    
    

    System.out.println(error.getCode());
    System.out.println(messageSource.getMessage(
      error.getCode(), new Object[]{
    
    error.getField()}, Locale.ENGLISH));
  }
}

Program output:

error.field.empty
firstName cannot be empty

error.field.empty
lastName cannot be empty

error.field.empty
email cannot be empty

error.field.empty
department.name cannot be empty

9. Integrate programmatic validators with Spring MVC/WebFlux controllers

Integrate programmatic validators withSpring MVC controller, including injecting programmatic validators into controllers and configuring them in the Spring context They, as well as simplify validation with annotations such as @Valid and BindingResult.

Happily, this integration also solves Ajax form submission and controller unit testing issues.

Below is a simplified example of a Spring MVC controller using the EmployeeValidator object we created in the previous chapter.

import demo.app.customValidator.model.Employee;
import demo.app.customValidator.model.validation.DepartmentValidator;
import demo.app.customValidator.model.validation.EmployeeValidator;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/employees")
public class EmployeeController {
    
    

  @InitBinder
  protected void initBinder(WebDataBinder binder) {
    
    
    // 注入 编程式验证器
    binder.setValidator(new EmployeeValidator(new DepartmentValidator()));
  }

  @GetMapping("/registration")
  public String showRegistrationForm(Model model) {
    
    
    model.addAttribute("employee", Employee.builder().build());
    return "employee-registration-form";
  }

  @PostMapping("/processRegistration")
  public String processRegistration(
    @Validated @ModelAttribute("employee") Employee employee,
    BindingResult bindingResult) {
    
    

    if (bindingResult.hasErrors()) {
    
    
      return "employee-registration-form";
    }

    // 处理成功通过数据校验后表单的逻辑
    // 通常涉及数据库操作、身份验证等。

    return "employee-registration-confirmation"; // 重定向至成功页面
  }
}

After , when the form is submitted, you can use the ${#fields.hasErrors('*')} expression to display validation errors in the view.

In the example below, we display validation errors in two places, i.e. displaying all errors in a list at the top of the form and then displaying errors for individual fields. Please customize the code according to your requirements.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Employee Registration</title>
</head>
<body>

<h2>Employee Registration Form</h2>

<!-- Employee Registration Form -->
<form action="./processRegistration" method="post" th:object="${employee}">

    <!-- Display validation errors, if any -->
    <div th:if="${#fields.hasErrors('*')}">
        <div style="color: red;">
            <p th:each="error : ${#fields.errors('*')}" th:text="${error}"></p>
        </div>
    </div>

    <!-- Employee ID (assuming it's a hidden field for registration) -->
    <input type="hidden" th:field="*{id}" />

    <!-- Employee First Name -->
    <label for="firstName">First Name:</label>
    <input type="text" id="firstName" th:field="*{firstName}" required />
    <span th:if="${#fields.hasErrors('firstName')}" th:text="#{error.field.size}"></span>
    <br/>

    <!-- Employee Last Name -->
    <label for="lastName">Last Name:</label>
    <input type="text" id="lastName" th:field="*{lastName}" required />
    <span th:if="${#fields.hasErrors('lastName')}" th:text="#{error.field.size}"></span>
    <br/>

    <!-- Employee Email -->
    <label for="email">Email:</label>
    <input type="email" id="email" th:field="*{email}" required />
    <span th:if="${#fields.hasErrors('email')}" th:text="#{error.field.size}"></span>
    <br/>

    <!-- Employee Active Status -->
    <label for="active">Active:</label>
    <input type="checkbox" id="active" th:field="*{active}" />
    <br/>

    <!-- Department Information -->
    <h3>Department:</h3>
    <label for="department.name">Department Name:</label>
    <input type="text" id="department.name" th:field="*{department.name}" required />
    <span th:if="${#fields.hasErrors('department.name')}" th:text="#{error.field.size}"></span>

    <br/>

    <!-- Submit Button -->
    <button type="submit">Register</button>

</form>

</body>
</html>

When we run the application and submit an invalid form, an error like the one shown in the picture appears:

Insert image description here

10. Unit test programmatic validator

We can test custom validators as mock dependencies or as individual test objects. The following JUnit test case will test EmployeeValidator.

We've written two very simple basic tests for quick reference, but you can write more tests to suit your needs.

import demo.app.customValidator.model.Department;
import demo.app.customValidator.model.Employee;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;

public class TestEmployeeValidator {
    
    

  static EmployeeValidator employeeValidator;

  @BeforeAll
  static void setup() {
    
    
    employeeValidator = new EmployeeValidator(new DepartmentValidator());
  }

  @Test
  void validate_ValidInput_NoErrors() {
    
    
    // Set up a valid user
    Employee employee = Employee.builder().id(1L)
      .firstName("Lokesh").lastName("Gupta").email("[email protected]")
      .department(Department.builder().id(2L).name("Finance").build()).build();

    Errors errors = new BeanPropertyBindingResult(employee, "employee");
    employeeValidator.validate(employee, errors);

    Assertions.assertFalse(errors.hasErrors());
  }

  @Test
  void validate_InvalidInput_HasErrors() {
    
    
    // Set up a valid user
    Employee employee = Employee.builder().id(1L)
      .firstName("A").lastName("B").email("C")
      .department(Department.builder().id(2L).name("HR").build()).build();

    Errors errors = new BeanPropertyBindingResult(employee, "employee");
    employeeValidator.validate(employee, errors);

    Assertions.assertTrue(errors.hasErrors());
    Assertions.assertEquals(3, errors.getErrorCount());
  }
}

Best practice is to ensure that your tests cover edge cases and boundary conditions. This includes situations where the input is at the minimum or maximum value allowed.

11. Conclusion

In this tutorial, we explore the Spring 6.1 Programmatic Validator API and its implementation guidelines with examples. Programmatic verification allows developers to have fine-grained control over the verification process.

We discussed how to create and use a custom validator class and integrate it with a Spring MVC controller. We learned how to use message parsing and then discussed how to test these validators to enable stronger coding practices.

Code address:programmatic-validator

Guess you like

Origin blog.csdn.net/xieshaohu/article/details/134590314