Spring MVC 如何在Spring中使用自定义验证器

2izufjch  于 2022-11-15  发布在  Spring
关注(0)|答案(4)|浏览(179)

我正在构建一个Sping Boot 应用程序,并尝试为一些DTO/实体实现自定义验证,我将在服务层中验证这些DTO/实体。基于有关此问题的Spring文档,我认为实现此目的的一种方法是实现org.springframework.validation.Validator接口。
作为一个最小、完整、可重现的示例,请考虑以下代码:
Spring初始化程序引导项目
src/main/java/com.example.usingvalidation中添加以下代码:
第一个
如果我点击端点触发验证,什么也不会发生。我看到calling validate日志消息。但是errors对象是空的。PersonValidater中没有任何日志消息被记录,所以很明显没有调用到达那里。
我的问题是:我如何向Spring * 注册 * 我的验证器,以便可以使用该验证器?
我已经浏览了多次文档和数百个SO问题(如X1 E0 F1 X),但无济于事。

附加信息

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

vhmi4jdf1#

这对您不起作用的主要原因是您尚未向DataBinder注册验证器。
对控制器进行一些更改。不是自动连接LocalValidatorFactoryBean,而是将验证器自动连接到控制器中,并将它们注册到DataBinder。

@Autowired
private PersonValidator personValidator;

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

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

@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在控制器端所做的方式自动完成这一工作,但我将留给您去发现。

svgewumm

svgewumm2#

你应该用@Validated注解你的控制器,别忘了把spring-boot-starter-validation添加到你的pom中。

de90aj5v

de90aj5v3#

经过一番幸运的搜索,从这篇文档中得到了启示https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/validation.html#validation-binder
我讨厌写自定义注解,所以我们来了。

验证器类

public class PersonValidator implements Validator的类保持原样,它是迄今为止封装“validator类”最好的类。

AOP类

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

@Aspect
@Component
public class RoAspect {

@Autowired
private PersonValidator validator;

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

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

    BindingResult bindingResult = dataBinder.getBindingResult();

    if(bindingResult.hasErrors()){
        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 {
        personService.save(new Person());
    } catch (Exception e) {
        // exception thrown by "AOP class" can be catch here
    }
vql8enpb

vql8enpb4#

如果你想以注解的形式创建你自己的验证器。下面的信息将帮助你。
我已经在我的项目中创建了我自己的验证器来验证用户给出的标记。它验证空格和#的标记。下面代码中的**@Hashtag**注解是我的自定义注解。
首先,您需要确定注解名称并声明一个接口,如下所示:(此接口将由java类“HashtagConstraintValidator”实现,您将在其中提供验证逻辑)

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = HashtagConstraintValidator.class)//says the logic of validation is in which class
@Target({ElementType.FIELD})//decides what level this annotation would work like on only fileds/ class level or both fields and class level.
@Retention(RetentionPolicy.RUNTIME)//decides up to which time this annotation should be valid like till compile time or runtime
public @interface Hashtag {

//This is kind of a method which returns # as a value to its caller and we use this value in our implementation class to build the logic 
    public String value() default "#";
//This is kind of a method which returns , as a value to its caller and we use in our implementation class to build the logic
    public String separator() default ",";

//This message will be shown on UI when the user input fails the validation
    public String message() default "[Tags should not start with #],[divide with , (comma)],[Do not give .(periods)]," +
            "[Should not have spaces]";

    //Groups-can group related constraints
    public Class<?>[] groups() default {};

    //Payload-Extra info about errors
    public Class<? extends Payload>[] payload() default {};
}

实作类别

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class HashtagConstraintValidator implements ConstraintValidator<Hashtag, String> {

    private String tagPrefix;//value will be #
    private String tagSuffix;//value will be ,

//Both initialize() and isValid() are mandatory

    @Override
    public void initialize(Hashtag hashtag) {
        tagPrefix = hashtag.value();
        tagSuffix = hashtag.separator();
    }

//Your validation logic
    @Override
    public boolean isValid(String theTags, ConstraintValidatorContext constraintValidatorContext) {
        boolean result = false;

        if(theTags == null) {
            return true;
        } else if(theTags.contains("#") || theTags.contains(" ") || theTags.contains(".")) {
            return false;
        }

        String[] tags = theTags.split(",");

        for(String tag : tags){
            if(tag.trim().equals("") || tag.trim().equals("#")){
                return false;
            }
        }

        if(tags.length ==1){
            result = true;
        } else {
            result = theTags.contains(tagSuffix) ? true : false;
        }

        return result;
    }
}
  • 现在,@标签可以应用于字段,因为我给出了@Target({ElementType.FIELD})*
@Hashtag
    private String tags;

您需要在控制器中使用@Valid和BindingResult来捕获验证错误并将错误发送回用户。

@PostMapping("/publish")
    public String saveTags(@Valid @ModelAttribute("tags") String tags, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "postTagsPage";
        }

如果您使用thymeleaf作为模板,下面的代码将在UI上向用户显示错误

<p th:if="${#fields.hasErrors('tags')}" th:errorclass="error" th:errors="*{tags}"/>

相关问题