只会注解校验?Spring Boot 编程式验证应对各种复杂场景

153 阅读4分钟

点击上方“程序员蜗牛g”,选择“设为星标”

跟蜗牛哥一起,每天进步一点点

程序员蜗牛g

大厂程序员一枚 跟蜗牛一起 每天进步一点点

33篇原创内容

**

公众号

通过简单配置即可自动校验数据,同时还提供了一套完善且灵活的编程式验证接口,允许开发者根据实际需求手动编写验证逻辑,实现更复杂、更精细化的数据校验。

如下编程式验证的简单示例:

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;    // 自定义验证逻辑  }}

接下来,我们以案例的方式详细介绍基于编程式的验证。

2.实战案例

2.1 API介绍

以下是创建自定义验证器对象的一般步骤:

  • 创建一个类,实现org.springframework.validation.Validator接口
  • 重写supports()方法,以指定此验证器支持验证的类。
  • 实现validate()或validateObject()方法,以定义实际的验证逻辑。
  • 使用ValidationUtils.rejectIfEmpty()等工具方法,以给定的错误代码拒绝给定字段。
  • 我们也可以直接调用Errors.rejectValue()方法,以添加其他类型的错误。

如下示例:

import org.springframework.validation.Errors;import org.springframework.validation.ValidationUtils;import org.springframework.validation.Validator;
@Componentpublic 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.");    // 你也可以自定义自己的验证逻辑  }}

接下来,如何使用呢?我们可以new每次创建实例,也可以通过注入的方式直接使用,如下示例:

public class UserService {  private final UserValidator userValidator ;  public UserService(UserValidator userValidator) {    this.userValidator = userValidator;  }  public void someServiceMethod(User user) {    Errors errors = new BeanPropertyBindingResult(user, "user");    userValidator.validate(user, errors);    if (errors.hasErrors()) {      // 如果存在错误,则进行错误处理    }  }}

2.2 编程式验证示例

首先,我们定义如下实体对象:

public class Employee {  private Long id;  private String name;  private String email;  private Department department;  // getters, setters}public class Department {  private Long id;  private String name;  // getters, setters}

定义验证器

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""id.empty");    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name""姓名不能为空");    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email""邮件不能为空");    Employee employee = (Employee) target;    if (employee.getName() != null && employee.getName().length() < 2) {      errors.rejectValue("name""姓名必须大于等于2个字符");    }  }}

同样地,我们也为 Department 类定义了相应的验证器。如有需要,你可以添加更复杂的验证规则。

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", , "id.empty");    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name""部门名称不能为空");    Department department = (Department) target;    if(department.getName() != null && department.getName().length() < 2) {      errors.rejectValue("name""部门名称必须大于等于2个字符");    }  }}

现在,我们可以按照如下方式对 Employee 和 Department 类的实例进行验证:

Employee employee = new Employee();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());  }}

输出结果

id.empty姓名不能为空邮件不能为空

2.3 链式多验证器

如果使用上述自定义验证器时,仅对 Employee 对象进行验证,则 API 不会自动验证与之关联的 Department 对象。正常情况下,在验证某个特定对象时,应同时对其所有关联对象执行验证。

编程式验证 API 支持通过调用其他验证器来聚合所有错误,并最终返回综合结果。这可通过 ValidationUtils.invokeValidator() 方法实现,如下示例:

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() 方法允许为子对象设置临时嵌套路径。在上述示例中,当对 department 对象进行验证时,路径会被设置为 'employee.department'。
  • popNestedPath() 方法则会在调用 pushNestedPath() 方法后,将路径重置回原始路径。在上述示例中,它会将路径重新恢复为 'employee'。

修改上面的代码如下:

public class EmployeeValidator implements Validator {
  private final DepartmentValidator departmentValidator ;  @Override  public void validate(Object target, Errors errors) {    // ...    try {      errors.pushNestedPath("department");      ValidationUtils.invokeValidator(this.departmentValidator, employee.getDepartment(), errors);    } finally {      errors.popNestedPath();    }  }}

修改测试类

Department department = new Department() ;Employee employee = new Employee(nullnullnull, department);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() + "," + error.getCode()) ;  }}

输出结果

id,id.emptyname,姓名不能为空email,邮件不能为空department.id,id.emptydepartment.name,部门名称不能为空

2.4 与Controller接口结合

将编程式验证器(Programmatic Validators)与Spring MVC控制器集成,需要将编程式验证器注入到控制器中、在Spring上下文中进行配置,并利用 @Valid 注解和 BindingResult 对象实现简洁的验证流程。

@RestController@RequestMapping("/employees")public class EmployeeController {  @InitBinder  protected void initBinder(WebDataBinder binder) {    // 设置我们自定义的验证器    binder.setValidator(new EmployeeValidator(new DepartmentValidator()));  }  @PostMapping("/create")  public ResponseEntity<?> create(    @Validated @RequestBody Employee employee, BindingResult error) {    if (error.hasErrors()) {      List<String> errors = error.getFieldErrors()          .stream()          .map(err -> err.getField() + ", " + err.getCode())          .toList() ;      return ResponseEntity.ok(errors) ;    }    return ResponseEntity.ok("success") ;  }}

如上示例,验证时,将会应用我们自定义的验证器。

图片

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!