【问题标题】:How to use custom validators in Spring如何在 Spring 中使用自定义验证器
【发布时间】:2022-07-13 17:38:48
【问题描述】:

我正在构建一个 Spring Boot 应用程序,并尝试为我将在服务层中验证的一些 DTO/实体实现自定义验证。基于Spring documentation on this matter,我认为一种方法是实现org.springframework.validation.Validator接口。

作为一个最小的、完整的、可重现的示例,请考虑以下代码:

Spring Initializr Bootstrapped Project

src/main/java/com.example.usingvalidation中添加以下代码:

// Person.java

package com.example.usingvalidation;

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private String gender;

    public Person() {
    }

    public Person(String firstName, String lastName, int age, String gender) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.gender = gender;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "Person{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                '}';
    }
}
// PersonValidator.java

package com.example.usingvalidation;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@Component
public class PersonValidator implements Validator {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public boolean supports(Class<?> clazz) {
        log.info("supports called");
       return Person.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        log.info("validate called");
        Person person = (Person) target;
        errors.reject("E00001", "This is the default error message, just to test.");
    }
}
// MyController.java
package com.example.usingvalidation;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.ConstraintViolation;
import java.util.Set;

@RestController
public class MyController {
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    private final LocalValidatorFactoryBean validatorFactory;

    @Autowired
    public MyController(LocalValidatorFactoryBean validatorFactory) {
        this.validatorFactory = validatorFactory;
    }

    @GetMapping("/")
    public Person getPerson(@RequestBody Person person) {
        log.info("calling validate");
        Set<ConstraintViolation<Person>> errors =  validatorFactory.validate(person);
        log.info("called validate, result: {}", errors);
        return null;
    }
}
// UsingValidationApplication.java  nothing changed here

package com.example.usingvalidation;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    private final LocalValidatorFactoryBean validatorFactory;

    @Autowired
    public MyController(LocalValidatorFactoryBean validatorFactory) {
        this.validatorFactory = validatorFactory;
    }

    @GetMapping("/")
    public Person getPerson(@RequestBody Person person) {
        log.info("calling validate");
        validatorFactory.validate(person);
        return null;
    }
}

如果我点击端点来触发验证,什么都不会发生。我看到了calling validate 日志消息。但是错误对象是空的。 PersonValidater 中的任何日志消息都没有被记录,因此显然没有呼叫到达那里。

我的问题是:我如何注册我的验证器到 Spring 以便我可以使用验证器?

我已多次阅读文档和数百个 SO 问题(如 java - Implementing custom validation logic for a spring boot endpoint using a combination of JSR-303 and Spring's Validator - Stack Overflow),但无济于事。

附加信息

  • 如果有任何 JSR-303 注释,如 @NotNull,则当前设置将拾取与 JSR-303 验证相关的错误。但这不是我需要的,我需要它来使用我的自定义验证器。
  • 我看到了其他 SO 问题,其中 InitBinder 在控制器中用于向 Spring 注册验证器。但我不想这样做,因为我计划在服务层中进行这些自定义验证。

【问题讨论】:

  • "var person" 是可怕的语法。
  • @K.Nicholas 感谢您的反馈。你能详细说明为什么它不好吗?

标签: java spring spring-boot spring-mvc


【解决方案1】:

这对您不起作用的主要原因是您尚未向 DataBinder 注册验证器。

对您的控制器进行一些更改。不是自动连接LocalValidatorFactoryBean,而是自动将您的验证器连接到控制器中并将它们注册到 DataBinder。

@Autowired
private PersonValidator personValidator;

@InitBinder
public void initBinder(WebDataBinder binder) {
  binder.addValidators(personValidator);
}

您的控制器方法也将更简单,因为您不再需要显式调用 ValidatorFactory,当您将 @Valid 注解添加到方法参数时,Spring 将自动调用验证器。将BindingResult 参数添加到方法中,来自验证器的所有错误都将出现在BindingResult 错误中,这包括由@Min、@Max、@NotNull 等javax 验证引起的错误。

@GetMapping("/")
public Person getPerson(@RequestBody @Valid Person person, BindingResult bindingResult) {
   if (bindingResult.hasErrors()) {
      log.info(bindingResult.getAllErrors());
   }
   return null;
}

当您想在服务层执行此操作时,您不得不编写自己的逻辑来处理此问题。就调用自定义验证而言,Spring 没有任何魔力。这是有意的,应用程序的入口是通过控制器进行的,这是您对输入数据的控制有限的地方,因此如果您想验证,应该在这里处理。您可以完全控制的控制器下游的 Person 对象的每个突变。如果您觉得您绝对必须在服务层进行验证,那么您将自己编写此代码,坦率地说,我不会为此使用 Spring 的 Validator 的实现。如果您对在服务层执行此操作一无所知,这里有一种方法可以实现。

创建注释以应用于您的 Person

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;

@Documented
@Constraint(validatedBy = PersonValidator.class)
@Target({TYPE})
@Retention(RUNTIME)
public @interface ValidPerson {

  String message() default "This isn't correct";

  Class[] groups() default {};

  Class[] payload() default {};
}

将上述注释添加到您的 Person 类中。

@ValidPerson
public class Person {

将您的 PersonValidator 修改为 ConstraintValidator,我已经将验证 Person 上的两个字段的实现组合在一起。

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.util.ObjectUtils;


public class PersonValidator implements ConstraintValidator<ValidPerson, Person> {

  @Override
  public void initialize(ValidPerson constraintAnnotation) {
    ConstraintValidator.super.initialize(constraintAnnotation);
  }

  @Override
  public boolean isValid(Person value, ConstraintValidatorContext context) {
     boolean isErrored = false;

     if (ObjectUtils.isEmpty(value.getLastName())) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate("lastName can't be empty").addConstraintViolation();
        isErrored = true;
     }
     if (value.getAge() < 0) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate("You're not old enough to be alive").addConstraintViolation();
        isErrored = true;
     }

    return !isErrored;
  }
}

在您的服务类中,注入一个 Validator 并在您的方法中调用它,这将调用您已定义并添加到您的 PersonConstraintValidator

import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PersonService {

  @Autowired
  private Validator validator;

  public Person updatePerson(Person person) {
    Set<ConstraintViolation<Person>> validate = validator.validate(person);

    return person;
  }
}

您可以使用 AOP 做一些花哨的事情,以像 Spring 在控制器方面所做的那样自动执行此操作,但我会让您自己去发现。

【讨论】:

  • 感谢您的回答!是的,我已经看到了在控制器中使用initBinder 的方法。我也看到了注释和ConstraintViolation 的东西。我只是想知道是否有一种更简洁的方法可以做到这一点,而不必为我想使用的每个验证器创建一个注释类(如果感觉会很混乱)。
  • 如果我想在服务中这样做,不使用 Spring Validator,你会推荐什么?
  • 就个人而言,我会为您想要验证的所有内容及其注释创建ConstraintViolations,您可以对其进行结构化,以便您的注释在他们自己的包中。如果您必须在服务层中进行验证,您将不得不手动进行,正如我上面所概述的那样。不幸的是,没有自动的方法来做到这一点。如果您想自动执行此操作,请查看 AOP,并创建包装您的服务并调用验证器的 Around 建议。否则,只需按照此处所述在每个服务调用中调用验证器。
  • @InitBinder 是天才,所有教程都应该提到这个功能。
  • @Johan 我完全同意你的看法@
【解决方案2】:

您应该使用@Validated 注释来注释您的控制器。不要忘记将 spring-boot-starter-validation 添加到您的 pom 中。

【讨论】:

  • 感谢您的回答,但我认为这不能解决问题。我认为只有在控制器方法中直接使用其他一些 JSR-303 约束注释时才需要 @Validated 注释。请参阅此question。我没有使用这样的注释,我只想使用我定义的Validator。是的,验证开始是 pom 的一部分,正常验证(使用注释)按预期工作。
【解决方案3】:

碰巧搜索后,从这个文档中得到启发 https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/validation.html#validation-binder

我讨厌编写自定义注释,所以我们来了。

验证器类

public class PersonValidator implements Validator的类保持原样,是目前最好的封装“验证器类”。

AOP 类

这个 AOP 类,在方法“保存”执行之前“神奇地”拦截。

@Aspect
@Component
public class RoAspect {

@Autowired
private MyEntityValidator validator;

/**
 * Validation before save
 *
 * @param joinPoint join point information
 * @param entity    passed entity
 */
@Before("execution(* go.to.your.service.MyService+.save(*)) && args(entity)")
public void beforeSave(JoinPoint joinPoint, MyEntity entity) throws MethodArgumentNotValidException {

    DataBinder dataBinder = new DataBinder(entity);
    dataBinder.setValidator(validator);
    dataBinder.validate();

    BindingResult bindingResult = dataBinder.getBindingResult();

    throw new MethodArgumentNotValidException(null, bindingResult);
    }

异常处理

处理“AOP类”抛出的异常

@RestControllerAdvice
public class ControllerExceptionHandler {

@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, List<String>> handleValidationError(MethodArgumentNotValidException exception) {

    Map<String, List<String>> errorMap = new HashMap<>();
    List<String> errors = new ArrayList<>();
    if (exception.hasErrors()) {
        for (ObjectError error : exception.getAllErrors()) {
            errors.add(error.getDefaultMessage());
        }
        errorMap.put("errors", errors);
    }
    return errorMap;
}
}

就是这样。您可能想知道“@RestControllerAdvice”的关键字。是的,它是专为控制器设计的(可能是!,如果我错了,请纠正)。

但是,不用担心它也可以使用本机 java “try catch”来处理。示例:

    try {
        myService.save(new Status());
    } catch (Exception e) {
        // exception thrown by "AOP class" can be catch here
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-02-26
    • 2022-07-21
    • 2013-11-15
    • 1970-01-01
    • 2014-09-24
    • 1970-01-01
    • 2014-09-24
    • 1970-01-01
    相关资源
    最近更新 更多