Spring Validation 实践

参考文档:https://springboot.io/t/topic/2582

Java API 规范( JSR303 )定义了 Bean 校验的标准 validation-api ,但没有提供实现。 hibernate validation 是对这个规范的实现,并增加了校验注解如 @Email 、 @Length 等。 Spring Validation 是对 hibernate validation 的二次封装。以 spring-boot 项目为例,介绍 Spring Validation 的使用。

引入依赖:(如果 spring-boot 版本小于 2.3.x , spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于 2.3.x ,则需要手动引入依赖)

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>

对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

  1. POST 、 PUT 请求,使用 requestBody 传递参数;
  2. GET 请求,使用 requestParam/PathVariable 传递参数。

requestBody 参数校验

POST 、 PUT 请求一般会使用 requestBody 传递参数,这种情况下,后端使用 DTO对象(数据传输对象Data Transfer Object) 进行接收。 只要给DTO对象加上 @Validated 注解就能实现自动参数校验 。比如,有一个保存 User 的接口,要求 userName 长度是 2-10 , account 和 password 字段长度是 6-20 。如果校验失败,会抛出 MethodArgumentNotValidException 异常, Spring 默认会将其转为 400(Bad Request) 请求。

在 DTO 字段上声明约束注解

@Data
public class UserDTO {

    private Long userId;

    @NotNull(message = "名称不能为空")
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull(message = "账号不能为空")
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}

在方法参数上声明校验注解

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

注:这种情况下, 使用 @Valid 和 @Validated 都可以 。

requestParam/PathVariable 参数校验

GET 请求一般会使用 requestParam/PathVariable 传参。如果参数比较多(比如超过6个),还是推荐使用 DTO 对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下, 必须在 Controller 类上标注 @Validated 注解,并在入参上声明约束注解(如 @Min 等) 。如果校验失败,会抛出 ConstraintViolationException 异常。代码示例如下:

@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
    // 路径变量
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        // 校验通过,才会执行业务逻辑处理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(userId);
        userDTO.setAccount("11111111111111111");
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.ok(userDTO);
    }

    // 查询参数
    @GetMapping("getByAccount")
    public Result getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
        // 校验通过,才会执行业务逻辑处理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(10000000000000003L);
        userDTO.setAccount(account);
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.ok(userDTO);
    }
}

分组校验。

分组接口可以写到DTO里面。

约束注解上声明适用的分组信息 groups

@Data
public class UserDTO {

    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}

 @Validated 注解上指定校验分组

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}
复制代码

 


 

 既然Spring Validation 是对 hibernate validation 的二次封装,防止浅尝辄止,还是又有必要研究一下hibernate validation的使用:

参考文档:https://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html_single/#preface:此官方文档截稿时间:June 20, 2011。

此处重点列出实践中需要注意和使用的:

一、定义约束(Bean Validation 的约束是通过Java 注解(annotations)来标注的)

1、字段级(field level) 约束:前面Spring Validation说明的基本都是这种情况。注:静态字段或者属性是不会被校验的

2、属性级别约束。注:遵循JavaBeans规范的话,只能定义在访问器(getter)上面,不能定义在修改器(setter)上.

public class CarValidator {
    private String manufacturer;

    private boolean isRegistered;

    public CarValidator(String manufacturer, boolean isRegistered) {
        super();
        this.manufacturer = manufacturer;
        this.isRegistered = isRegistered;
    }

    @NotNull
    public String getManufacturer() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    @AssertTrue
    public boolean isRegistered() {
        return isRegistered;
    }

    public void setRegistered(boolean isRegistered) {
        this.isRegistered = isRegistered;
    }
}

3、类级别约束。

    当一个约束被标注在一个类上的时候,这个类的实例对象被传递给ConstraintValidator. 当需要同时校验多个属性来验证一个对象或者一个属性在验证的时候需要另外的属性的信息的时候, 类级别的约束会很有用。

4、约束继承

    如果要验证的对象继承于某个父类或者实现了某个接口,那么定义在父类或者接口中的约束会在验证这个对象的时候被自动加载,如同这些约束定义在这个对象所在的类中一样。

5、对象图

    Bean Validation API不仅能够用来校验单个的实例对象,还能够用来校验完整的对象图.要使用这个功能,只需要在一个有关联关系的字段或者属性上标注@Valid. 这样,如果一个对象被校验,那么它的所有的标注了@Valid的关联对象都会被校验。

    关联校验也适用于集合类型的字段, 也就是说,任何下列的类型:
    ①数组;
    ②实现了java.lang.Iterable接口( 例如Collection, List 和 Set);
    ③实现了java.util.Map接口;

public class Car {

    @NotNull
    @Valid
    private List<Person> passengers = new ArrayList<Person>();

    public Car(List<Person> passengers) {
        this.passengers = passengers;
    }

    //getters and setters ...
}

    如果标注了@Valid, 那么当主对象被校验的时候,这些集合对象中的元素都会被校验。当校验一个Car的实例的时候,如果passengers list中包含的任何一个Person对象没有名字的话,都会导致校验失败(a ConstraintValidation will be created)。注:对象图校验的时候是会被忽略null值的。

二、Bean Validation中最主要的接口Validator

    对一个实体对象验证之前首先需要有个Validator对象, 而这个对象是需要通过Validation 类和 ValidatorFactory来创建的. 最简单的方法是调用Validation.buildDefaultValidatorFactory() 这个静态方法.。

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

    Validator中有三个方法能够被用来校验整个实体对象或者实体对象中的属性。
    这三个方法都会返回一个Set<ConstraintViolation>对象, 如果整个验证过程没有发现问题的话,那么这个set是空的, 否则, 每个违反约束的地方都会被包装成一个ConstraintViolation的实例然后添加到set当中。
    所有的校验方法都接收零个或多个用来定义此次校验是基于哪个校验组的参数. 如果没有给出这个参数的话, 那么此次校验将会基于默认的校验组 (javax.validation.groups.Default)。

1、validate:使用validate()方法对一个给定的实体对象中定义的所有约束条件进行校验。

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Car car = new Car(null);
Set
<ConstraintViolation<Car>> constraintViolations = validator.validate(car); assertEquals(1, constraintViolations.size()); assertEquals("may not be null", constraintViolations.iterator().next().getMessage());// 英文模式;中文模式:不能为null

2、通过validateProperty()可以对一个给定实体对象的单个属性进行校验. 其中属性名称需要符合JavaBean规范中定义的属性名称.

Set<ConstraintViolation<Car>> constraintViolations = validator.validateProperty(car, "manufacturer");
assertEquals(1, constraintViolations.size());
assertEquals("may not be null", constraintViolations.iterator().next().getMessage());

3、validateValue:通过validateValue() 方法,你能够校验如果把一个特定的值赋给一个类的某一个属性的话,是否会违反此类中定义的约束条件.

Set<ConstraintViolation<Car>> constraintViolations = validator.validateValue(Car.class, "manufacturer", null);
assertEquals(1, constraintViolations.size());
assertEquals("may not be null", constraintViolations.iterator().next().getMessage());

注:validateProperty() 和 validateValue() 会忽略被验证属性上定义的@Valid.

三、ConstraintViolation 中的方法

1、只列出主要的:

    getMessage() ----获取(经过翻译的)校验错误信息 ----may not be null
    getMessageTemplate() ----获取错误信息模版 ----{javax.validation.constraints.NotNull.message}

2、验证失败提示信息解析

     每个约束定义中都包含有一个用于提示验证结果的消息模版, 并且在声明一个约束条件的时候,你可以通过这个约束中的message属性来重写默认的消息模版.

    如果在校验的时候,这个约束条件没有通过,那么你配置的MessageInterpolator会被用来当成解析器来解析这个约束中定义的消息模版, 从而得到最终的验证失败提示信息. 这个解析器会尝试解析模版中的占位符( 大括号括起来的字符串 )。

    其中, Hibernate Validator中默认的解析器 (MessageInterpolator) 会先在类路径下找名称为ValidationMessages.properties的ResourceBundle(如在Springboot项目中src目录下main\resources\ValidationMessages.properties), 然后将占位符和这个文件中定义的resource进行匹配,如果匹配不成功的话,那么它会继续匹配Hibernate Validator自带的位于/org/hibernate/validator/ValidationMessages.properties的ResourceBundle, 依次类推,递归的匹配所有的占位符。

四、校验组

1、校验组能够让你在验证的时候选择应用哪些约束条件. 这样在某些情况下( 例如向导 ) 就可以对每一步进行校验的时候, 选取对应这步的那些约束条件进行验证了。没有明确指定这个约束条件属于哪个组,所以它被归类到默认组 (javax.validation.groups.Default)。具体实践待见上面的分组校验。

    注:使用接口( 而不是字符串) 可以做到类型安全,并且接口比字符串更加对重构友好, 另外, 接口还意味着一个组可以继承别的组.

2、 校验组序列:一个属性属于多个校验组,校验的顺序是不确定的。如果需要指定顺序,就该校验组序列

@GroupSequence({Default.class, First.class, Second.class})
public interface OrderedChecks {
}

    注: 如果这个校验组序列中有一个约束条件没有通过验证的话, 那么此约束条件后面的都不会再继续被校验了。

@Test
public void testOrderedChecks() {
    Car car = new Car( "Morris", "DD-AB-123", 2 );
    car.setPassedVehicleInspection( true );

    Driver john = new Driver( "John Doe" );
    john.setAge( 18 );
    john.passedDrivingTest( true );
    car.setDriver( john );

    assertEquals( 0, validator.validate( car, OrderedChecks.class ).size() );
}

五、创建自己的约束规则

三步走:

    ①创建约束标注
    ②实现一个验证器
    ③定义默认的验证错误信息

1、约束标注

public enum CaseMode {
    UPPER, 
    LOWER;
}

   真正约束标注:

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {

    String message() default "{com.mycompany.constraints.checkcase}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
    
    CaseMode value();

}

    message属性, 这个属性被用来定义默认得消息模版, 当这个约束条件被验证失败的时候,通过此属性来输出错误信息.
    groups 属性, 用于指定这个约束条件属于哪(些)个校验组.这个的默认值必须是Class<?>类型到空到数组.
    payload 属性, Bean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用.

    元标注
   @Target({ METHOD, FIELD, ANNOTATION_TYPE }): 表示@CheckCase 可以被用在方法, 字段或者annotation声明上.
   @Retention(RUNTIME): 表示这个标注信息是在运行期通过反射被读取的.
   @Constraint(validatedBy = CheckCaseValidator.class): 指明使用那个校验器(类) 去校验使用了此标注的元素.
   @Documented: 表示在对使用了@CheckCase的类进行javadoc操作到时候, 这个标注会被添加到javadoc当中.

2、验证器(约束校验器):需要implement a constraint validator;此处implement the interface ConstraintValidator。

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {

        if (object == null)
            return true;

        if (caseMode == CaseMode.UPPER)
            return object.equals(object.toUpperCase());
        else
            return object.equals(object.toLowerCase());
    }

}

3、校验错误信息,详细见参考文档3.13

4、约束条件组合:在某些复杂的场景中, 可能还会有更多的约束条件被定义到同一个元素上面, 这可能会让代码看起来有些复杂。如:licensePlate上面又三个约束注解。

public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    @CheckCase(CaseMode.UPPER)
    private String licensePlate;

    @Min(2)
    private int seatCount;
    
    public Car(String manufacturer, String licencePlate, int seatCount) {

        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.seatCount = seatCount;
    }

    //getters and setters ...

}

    可以通过使用组合约束条件来解决. 创建一个新的约束标注@ValidLicensePlate, 它组合了@NotNull, @Size 和 @CheckCase:

@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface ValidLicensePlate {

    String message() default "{com.mycompany.constraints.validlicenseplate}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

    把要组合的约束标注在这个新的类型上加以声明 (注: 这正是我们为什么把annotation types作为了@CheckCase的一个target). 因为这个组合不需要额外的校验器, 所以不需要声明validator属性.
现在, 在licensePlate属性上使用这个新定义的"约束条件" (其实是个组合) 和之前在其上声明那三个约束条件是一样的效果了.

public class Car {

    @ValidLicensePlate
    private String licensePlate;

    //...

}

六、快速失败模型:Fail fast mode,出现第一个约束校验失败就返回,效率高(特别是很多约束时)

提供三种方式:

1、Enabling failFast via a property

HibernateValidatorConfiguration configuration = Validation.byProvider( HibernateValidator.class ).configure();
ValidatorFactory factory = configuration.addProperty( "hibernate.validator.fail_fast", "true" ).buildValidatorFactory();
Validator validator = factory.getValidator();

// do some actual fail fast validation
...

2、Enabling failFast at the Configuration level(推荐使用)

HibernateValidatorConfiguration configuration = Validation.byProvider( HibernateValidator.class ).configure();
ValidatorFactory factory = configuration.failFast( true ).buildValidatorFactory();
Validator validator = factory.getValidator();

// do some actual fail fast validation
...

3、Enabling failFast at the ValidatorFactory level

HibernateValidatorConfiguration configuration = Validation.byProvider( HibernateValidator.class ).configure();
ValidatorFactory factory = configuration.buildValidatorFactory();

Validator validator = factory.getValidator();

// do some non fail fast validation
...

validator = factory.unwrap( HibernateValidatorFactory.class )
            .usingContext()
            .failFast( true )
            .getValidator();

// do fail fast validation
...

 

posted on 2020-09-21 14:33  betterLearing  阅读(354)  评论(0编辑  收藏  举报