Validator使用总结

1. 前言

本周写了一个自定义的认证、鉴权功能,发现以前SpringBoot的使用记录对于查找问题并不友好,这里对这几篇文章重新进行整理。分成以下几大部分:

  1. 存储
  2. 传输
  3. Spring机制
  4. 缓存组件

本篇是传输方面的第一篇,主要是Validator内容。

ps:这里的传输并不是网络方面更的内容,数据到来之后的处理方面。

以前在Nest.js中,也用过Validate功能,相比而言,Nest.js的validate是对dto属性合法性的验证,由于js语言的特性,会对每一种类型进行判断。

SpringBoot的validate更加丰富,即可以写一个validator类对整个Bean进行验证,又可以在Bean用Contraint的注解进行验证,既可以对controller层进行验证,又可以对service层进行验证。

2. Customize Validator验证方式

  1. 实现一个Validator
 @Component()
 public class PersonValidator implements Validator {

    @Override
    public boolean supports(Class clazz) {
        return PersonDto.class.equals(clazz);
    }

    @Override
    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        PersonDto p = (PersonDto) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 150) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}
  1. 将Validator加入DataBinder
@RestController
@RequestMapping("/person")
public class PersonController {

    @Autowired
    private PersonValidator personValidator;

   @InitBinder("personDto")
    public void initBinder(WebDataBinder binder) {
    binder.addValidators(personValidator);
    }
}
  1. 在controller中用@Valid对PersonDto进行验证
@PostMapping("/")
public Project create(@Valid @RequestBody PersonDto personDto) throws Exception{
    return this.personService.save(personDto);
}

说明:

  • 在完成第1步,validator的实现,可通过注入的方式在Controller或者Service中通过调用validate函数的方式对PerstonDto进行验证,这种写法麻烦一些。
  • 在validator中,可以嵌套其他validator对属性进行验证,示例

下边简单来看看Databinder:

先看一下类的定义

public class DataBinder extends Object implements PropertyEditorRegistry, TypeConverter{
    //...省略...
}

从中可以猜测DataBinder通过属性的编辑以实现类型的转换。

再来看一下它的说明文档:DataBinder 允许将属性值设置到目标对象上,包括支持验证和绑定结果分析。

最后来看几个示例:

  • 转换示例

    package com.logicbig.example;
    
    public class TestBean {
        private int anInt;
    
        public int getAnInt () {
            return anInt;
        }
    
        public void setAnInt (int anInt) {
            this.anInt = anInt;
        }
    
        @Override
        public String toString () {
            return "TestBean{anInt=" + anInt + '}';
        }
    }
    
    package com.logicbig.example;
    
    import org.springframework.beans.MutablePropertyValues;
    import org.springframework.validation.DataBinder;
    
    public class DataBinderExample {
        public static void main (String[] args) {
    
            MutablePropertyValues mpv = new MutablePropertyValues();
            mpv.add("anInt", "10");
    
            TestBean testBean = new TestBean();
            DataBinder db = new DataBinder(testBean);
    
            db.bind(mpv);
            System.out.println(testBean);
    
        }
    }
    
  • 验证示例

    Foo target = new Foo();
    DataBinder binder = new DataBinder(target);
    binder.setValidator(new FooValidator());
    
    // bind to the target object
    binder.bind(propertyValues);
    
    // validate the target object
    binder.validate();
    
    // get BindingResult that includes any validation errors
    BindingResult results = binder.getBindingResult();
    

3. Annotation验证方式

Annotation验证方式是最常用的验证方式,它应该是通过将annotation与validator进行了结合,使验证使用更简单。

Annotation的方式官方叫法是Java Bean Validation,使用时比较简单,但有一个大坑。

先在属性上增加约束

public class PersonDto {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    @Max(150)
    private int age;
}

然后在Controller上用@Valid验证即可

@PostMapping("/")
public Project create(@Valid @RequestBody PersonDto personDto) throws Exception{
    return this.personService.save(personDto);
}

来说说这个大坑

官方文档上说,Spring Framework支持Jakata Bean Validation。我们在使用这些@Null、@NotNull、@Size时候,需要引入依赖,而这时候有个jakata.validation..的包就很容易被引用进来。这时候无论如何测试,这个约束都不成功,找文档也很难找到相同的问题,最主要是这个Jakata官方文档资料真少。

我在偶然的情况下,修改了依赖为spring-boot-starter-validation才解决了这个问题,然后才发现spring-boot-starter-validation封装了Hibernate ValidatorHibernate Validator是Jakata的一个实现。

4. 自定义Constraint

它是由2部分组成,进行注解的部分成文Constraint(约束),进行验证的部分就是validator

  1. Constraint部分
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
  1. Validator部分
import javax.validation.ConstraintValidator;
public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    // ...
}

具体的例子见后边Enum部分

5. Update、Create同属性不同验证

create是对所有属性进行创建,而update时,是对其中一部分的属性进行修改。
在nest.js中一般使用@ValidateIf来进行装饰,spring提供了groups属性来支持。

  1. 创建2个marker interface

    interface OnCreate {}
    interface OnUpdate {}
    
  2. 在DTO需要不同验证的属性上进行注解

    class InputWithGroups {
        @Null(groups = OnCreate.class)
        @NotNull(groups = OnUpdate.class)
        private Long id;
    // ...
    }
    
  3. 在Controller的路由上指明是哪种group

    @Service
    @Validated
    class ValidatingServiceWithGroups {
    
        @Validated(OnCreate.class)
        void validateForCreate(@Valid InputWithGroups input){
        // do something
        }
    
        @Validated(OnUpdate.class)
        void validateForUpdate(@Valid InputWithGroups input){
        // do something
        }
    
    }
    

6. 验证返回处理

这里主要是在异常处理中完成

public class ValidationErrorResponse {
  private List<Violation> violations = new ArrayList<>();
  // ...
}

public class Violation {
  private final String fieldName;
  private final String message;
  // ...
}
@RestControllerAdvice
class ErrorHandlingControllerAdvice {

  @ExceptionHandler(ConstraintViolationException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  ValidationErrorResponse onConstraintValidationException(ConstraintViolationException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (ConstraintViolation violation : e.getConstraintViolations()) {
      error.getViolations().add(
        new Violation(violation.getPropertyPath().toString(), violation.getMessage()));
    }
    return error;
  }

  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  ValidationErrorResponse onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
      error.getViolations().add(
        new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
    }
    return error;
  }

}

7. 参考

参考1:官方文档

参考2:Validation with Spring Boot - the Complete Guide

参考3:Spring Boot - 数据校验

# spring 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×