Spring Boot 技术探索

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".

9、Spring Boot的数据校验

平台环境:

名称

版本号

Mac OS X

10.14.5

JDK

1.8.0_201

Apache Maven

3.6.0

IntelliJ IDEA

2019.1 (Ultimate Edition)

Spring Boot

2.1.6.RELEASE

 

一、数据校验

  任何一个程序都需要对输入的数据进行合法性校验,Spring Boot为我们提供了数据校验的解决方案。

在spring-boot-starter-web依赖中,默认引用了hibernate-validator依赖。而hibernate-validator的由来则要从JSR讲起。

  JSR是Java Specification Requests Java的缩写,意思是Java规范提案。是指向JCP(Java Community Process是为Java技术开发标准技术规范的机制)提出新增一个标准化技术规范的正式提案。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

  Bean Validation是Java定义的一套基于注解的数据校验规范,目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成于2017.08),已经经历了三个版本。

 

Bean Validation的实现

  目前,Bean Validation 2.0有一系列的官方认证的实现。其中Hibernate Validator应用最广泛,这里只讲Hibernate Validator。

 

二、Hibernate Validator Demo

1、新建一个User类,增加校验标签

package com.example.demo.model;

import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;


public class User
{
    @NotEmpty(message = "姓名不能为空")
    private String userName;

    @Max(value = 100, message = "年龄不能大于100岁")
    @Min(value = 18, message = "必须年满18岁!")
    private int age;

    @NotEmpty(message = "备注不能为空")
    @Length(min = 6, message = "备注长度不能小于6位")
    private String remark;

    public String getUserName()
    {
        return userName;
    }

    public void setUserName(String userName)
    {
        this.userName = userName;
    }

    public int getAge()
    {
        return age;
    }

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

    public String getRemark()
    {
        return remark;
    }

    public void setRemark(String remark)
    {
        this.remark = remark;
    }

    @Override
    public String toString()
    {
        return "userName:" + userName + ",age=" + age + ",remark=" + remark;
    }
}

 

2、新建一个WebController,测试用

package com.example.demo.controller;

import com.example.demo.model.User;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
public class WebController
{
    @RequestMapping("/saveUser")
    public void saveUser(@Valid User user, BindingResult result)
    {
        System.out.println("user:" + user);
        if (result.hasErrors())
        {
            for (ObjectError error : result.getAllErrors())
            {
                System.out.println(error.getCode() + "-" + error.getDefaultMessage());
            }
        }
    }
}

代码解释:

  • 方法参数前面添加 @Valid 注解,表示此字段启用参数校验

  • BindingResult是用于显示绑定结果的通用接口

 

3、运行项目,打开浏览器访问http://localhost:8080/saveUser?userName=xiaoming&age=30&remark=123456

输出:

user:userName:xiaoming,age=30,remark=123456

 

打开浏览器访问http://localhost:8080/saveUser?userName=&age=999&remark=123

输出:

Max-年龄不能大于100岁

Length-备注长度不能小于6位

NotEmpty-姓名不能为空

 

三、Hibernate Validator的验证模式

  上面的例子中可以看出,控制台一次输出了所有字段的校验信息,而实际上当有第一个校验失败的情况发生时,校验工作就可以结束了。这里就引出了Hibernate Validator的两种验证模式:

  1、普通模式(默认会校验完所有的属性,然后返回所有的验证失败信息)

  2、快速失败返回模式(只要有一个验证失败,则返回)

 

修改验证模式的方法:

方法1、failFast参数方式:true快速失败返回模式,false普通模式。

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

 

方法2、addProperty方式:hibernate.validator.fail_fast:true快速失败返回模式,false普通模式。

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

 

Demo:

package com.example.demo.config;

import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

@Configuration
public class ValidatorConfiguration
{
    @Bean
    public Validator validator()
    {
        ValidatorFactory validatorFactory = Validation.byProvider(
                HibernateValidator.class)
                .configure()
                .failFast(true)
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        return validator;
    }
}

 

四、Hibernate Validator的校验应用方法

1、Model类参数校验(之前的Demo就是用的这种方式)

  在方法的参数前增加注解 @Valid,然后后面加BindindResult即可。

  多个参数的,可以加多个@Valid和BindingResult,如:

public void saveUser(@Valid User user, BindingResult result)
public void saveUser(@Valid User user, BindingResult result, @Valid User user2, BindingResult result2)

 

2、普通参数校验

  如果在一个@RequestMapping方法中没有使用Model类参数,而是直接获取参数,则使用@Valid注解是无效的。例如:

@RequestMapping("/getParam")
public void getParam(@NotEmpty(message = "姓名不能为空") String userName, @Max(value = 100, message = "年龄不能大于100岁")int age)
{
    System.out.println("userName:" + userName);
    System.out.println("age:" + age);
}

 

  这时,需要在Controller上添加@Validated注解,启用验证。

package com.example.demo.controller;

import com.example.demo.model.User;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotEmpty;

@RestController
@Validated
public class WebController
{
    @RequestMapping("/getParam")
    public void saveUser(@NotEmpty(message = "姓名不能为空") String userName, @Max(value = 100, message = "年龄不能大于100岁")int age)
    {
        System.out.println("userName:" + userName);
        System.out.println("age:" + age);
    }
}

 

浏览器访问:http://localhost:8080/getParam?userName=&age=999

控制台输出:

javax.validation.ConstraintViolationException: saveUser.userName: 姓名不能为空, saveUser.age: 年龄不能大于100岁

 

3、Model类级联校验

  现在假设小伙伴们买了车,因此在User类中需要增加一个Car属性。

  这种情况需要在验证User类属性的同时也要验证Car类的属性。通过在User类中需要级联验证的属性前加@Valid,就可以对属性的对象的内部属性进行验证。

 

User类

package com.example.demo.model;

import org.hibernate.validator.constraints.Length;

import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;


public class User
{
    @NotEmpty(message = "姓名不能为空")
    private String userName;

    @Max(value = 100, message = "年龄不能大于100岁")
    @Min(value = 18, message = "必须年满18岁!")
    private int age;

    @NotEmpty(message = "备注不能为空")
    @Length(min = 6, message = "备注长度不能小于6位")
    private String remark;

    @Valid
    private Car car;

    public Car getCar()
    {
        return car;
    }

    public void setCar(Car car)
    {
        this.car = car;
    }

    public String getUserName()
    {
        return userName;
    }

    public void setUserName(String userName)
    {
        this.userName = userName;
    }

    public int getAge()
    {
        return age;
    }

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

    public String getRemark()
    {
        return remark;
    }

    public void setRemark(String remark)
    {
        this.remark = remark;
    }

    @Override
    public String toString()
    {
        return "userName:" + userName + ",age=" + age + ",remark=" + remark;
    }
}

 

Car类

package com.example.demo.model;

import javax.validation.constraints.NotEmpty;

public class Car
{
    @NotEmpty(message = "carName不能为空")
    private String carName;

    public String getCarName()
    {
        return carName;
    }

    public void setCarName(String carName)
    {
        this.carName = carName;
    }
}

 

测试Controller

package com.example.demo.controller;

import com.example.demo.model.Car;
import com.example.demo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validator;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotEmpty;
import java.util.Set;

@RestController
//@Validated
public class WebController
{
    /**前面配置了快速失败返回的Bean*/
    @Autowired
    private Validator validator;

    @RequestMapping("/saveUser2")
    public void saveUser2()
    {
        Car car = new Car();
//        car.setCarName("abc");

        User user = new User();
        user.setUserName("111");
        user.setAge(30);
        user.setRemark("rmkrmkrmk");
        user.setCar(car);

        Set<ConstraintViolation<User>> violationSet = validator.validate(user);
        for (ConstraintViolation<User> model : violationSet) {
            System.out.println(model.getMessage());
        }
    }
}

 

浏览器访问http://localhost:8080/saveUser2

控制台输出:

carName不能为空

 

4、分组校验

  有的时候,不需要验证所有的字段,也不能按照快速失败返回模式遇到一个不合法的字段就停止验证,这个时候就需要分组校验。

  例如,当新增信息的时候,不需要验证主键ID。但是在修改的时候就必须验证主键ID。

 

为了清楚的展示验证效果,可以先关闭快速失败返回模式。

failFast(false)
或者
("hibernate.validator.fail_fast", "false")

 

定义验证组接口:

public interface GroupA 
{
}
public interface GroupB 
{
}

 

验证model:Role

package com.example.demo.model;

import com.example.demo.model.group.GroupA;
import com.example.demo.model.group.GroupB;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.NotBlank;
import javax.validation.groups.Default;

public class Role
{
    @Range(min = 1,max = Integer.MAX_VALUE,message = "角色ID必须大于等于0",groups = {GroupA.class})
    /**角色id*/
    private Integer roleID;

    @NotBlank
    @Length(min = 4,max = 20,message = "角色名长度必须在[4,20]内",groups = {GroupB.class})
    /**角色名*/
    private String roleName;

    @Range(min = 0,max = 100,message = "角色编号必须是[0,100]",groups={Default.class})
    /**角色编号*/
    private Integer roleNO;

    @Range(min = 0,max = 2,message = "启用状态必须是[0,1]",groups = {GroupB.class})
    /**启用状态 0:未启用;1:已启用*/
    private Integer IsActive;

    public Integer getRoleID()
    {
        return roleID;
    }

    public void setRoleID(Integer roleID)
    {
        this.roleID = roleID;
    }

    public String getRoleName()
    {
        return roleName;
    }

    public void setRoleName(String roleName)
    {
        this.roleName = roleName;
    }

    public Integer getRoleNO()
    {
        return roleNO;
    }

    public void setRoleNO(Integer roleNO)
    {
        this.roleNO = roleNO;
    }

    public Integer getIsActive()
    {
        return IsActive;
    }

    public void setIsActive(Integer isActive)
    {
        IsActive = isActive;
    }
}

 

如上Role所示,3个分组分别验证字段如下:

  • GroupA验证字段roleID
  • GroupB验证字段roleName、IsActive
  • Default验证字段roleNO(Default是Validator自带的默认分组)

 

测试代码,验证GroupA、GroupB标记的分组:

@RequestMapping("/test3")
public void test3(@Validated({GroupA.class, GroupB.class}) Role role, BindingResult result)
{
    if (result.hasErrors())
    {
        for (ObjectError error : result.getAllErrors())
        {
            System.out.println(error.getCode() + "-" + error.getDefaultMessage());
        }
    }
}

浏览器访问:http://localhost:8080/test3?roleID=-12&roleName=a&roleNO=110&IsActive=5

输出:

Length-角色名长度必须在[4,20]内

Range-角色ID必须大于等于0

Range-启用状态必须是[0,1]

 

或者这样写测试代码:

@RequestMapping("/test4")
public void test4()
{
    Role role = new Role();

    /**GroupA验证不通过*/
    role.setRoleID(-12);

    /**GroupA验证通过*/
    //role.setUserId(12);

    role.setRoleName("a");
    role.setRoleNO(110);
    role.setIsActive(5);

    Set<ConstraintViolation<Role>> validate = validator.validate(role, GroupA.class, GroupB.class);
    for (ConstraintViolation<Role> item : validate)
    {
        System.out.println(item.getMessage());
    }
}

浏览器访问:http://localhost:8080/test4

输出:

启用状态必须是[0,1]
角色名长度必须在[4,20]内

 

5、分组顺序校验

  通过上的例子可以看出分组校验的灵活性,但是有些情况既想有分组校验的灵活性,又想拥有快速失败返回模式的验证失败立即返回的形式。这里就需要用到分组顺序校验。

  分组顺序校验时,按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不进行验证。

 

定义组序列(GroupA > GroupB > Default):

package com.example.demo.model.group;

import javax.validation.GroupSequence;
import javax.validation.groups.Default;

@GroupSequence({GroupA.class, GroupB.class, Default.class})
public interface GroupOrder
{
}

 

测试代码:

@RequestMapping("/test5")
public void test5(@Validated({GroupOrder.class}) Role role, BindingResult result)
{
    if (result.hasErrors())
    {
        for (ObjectError error : result.getAllErrors())
        {
            System.out.println(error.getCode() + "-" + error.getDefaultMessage());
        }
    }
}

 

浏览器访问http://localhost:8080/test5?roleID=-12&roleName=a&roleNO=110&IsActive=5

输出:

Range-角色ID必须大于等于0

 

浏览器访问http://localhost:8080/test5?roleID=2&roleName=a&roleNO=110&IsActive=5

输出:

Length-角色名长度必须在[4,20]内

Range-启用状态必须是[0,1]

 

浏览器访问http://localhost:8080/test5?roleID=2&roleName=abcde&roleNO=110&IsActive=1

输出:

Range-编号必须是[0,100]

 

五、自定义验证器

  有时候会遇到框架提供的验证标签无法满足情况的时候,此时,我们可以验证标签。

  如下所示,实现了一个自定义的大小写验证标签:

 

CaseMode.java

package com.example.demo.util.CheckCase;

public enum CaseMode
{
    UPPER,
    LOWER;
}

 

CheckCase.java

package com.example.demo.util.CheckCase;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;


@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase
{
    String message() default "";

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

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

    CaseMode value();
}

 

CheckCaseValidator.java

package com.example.demo.util.CheckCase;

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


public class CheckCaseValidator implements ConstraintValidator<CheckCase, String>
{
    private CaseMode caseMode;

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

    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext)
    {
        if (s == null)
        {
            return true;
        }

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

 

测试Model类:

package com.example.demo.model;

import com.example.demo.util.CheckCase.CaseMode;
import com.example.demo.util.CheckCase.CheckCase;

public class Book
{
    @CheckCase(value = CaseMode.UPPER, message = "书名必须大写")
    String bookName;

    public String getBookName()
    {
        return bookName;
    }

    public void setBookName(String bookName)
    {
        this.bookName = bookName;
    }
}

 

测试Demo

    @RequestMapping("/test6")
    public void test6()
    {
        Book book = new Book();
        book.setBookName("a tale of two cities");
//        book.setBookName("A TALE OF TWO CITIES");

        Set<ConstraintViolation<Book>> validate = validator.validate(book);
        for (ConstraintViolation<Book> item : validate)
        {
            System.out.println(item.getMessage());
        }
    }

 

浏览器访问http://localhost:8080/test6

输出:

书名必须大写

 

 

附:校验标签列表

meta-data

comment

version

@Null

对象,为空

Bean Validation 1.0

@NotNull

对象,不为空

Bean Validation 1.0

@AssertTrue

布尔,为True

Bean Validation 1.0

@AssertFalse

布尔,为False

Bean Validation 1.0

@Min(value)

数字,最小为value

Bean Validation 1.0

@Max(value)

数字,最大为value

Bean Validation 1.0

@DecimalMin(value)

数字,最小为value

Bean Validation 1.0

@DecimalMax(value)

数字,最大为value

Bean Validation 1.0

@Size(max, min)

min<=value<=max

Bean Validation 1.0

@Digits (integer, fraction)

数字,某个范围内

Bean Validation 1.0

@Past

日期,过去的日期

Bean Validation 1.0

@Future

日期,将来的日期

Bean Validation 1.0

@Pattern(value)

字符串,正则校验

Bean Validation 1.0

@Email

字符串,邮箱类型

Bean Validation 2.0

@NotEmpty

集合,不为空

Bean Validation 2.0

@NotBlank

字符串,不为空字符串

Bean Validation 2.0

@Positive

数字,正数

Bean Validation 2.0

@PositiveOrZero

数字,正数或0

Bean Validation 2.0

@Negative

数字,负数

Bean Validation 2.0

@NegativeOrZero

数字,负数或0

Bean Validation 2.0

@PastOrPresent

过去或者现在

Bean Validation 2.0

@FutureOrPresent

将来或者现在

Bean Validation 2.0

 

 

标签文档:

https://beanvalidation.org/2.0/spec/#builtinconstraints

 

参考资料:

https://www.jcp.org/en/home/index

https://beanvalidation.org

https://www.jianshu.com/p/2a495bf5504e

https://www.cnblogs.com/mr-yang-localhost/p/7812038.html

 

Bootstrap Thumbnail Second
MySQL

MySQL is the world's most popular open source database.

GO

Bootstrap Thumbnail Third
算法基础

本书介绍了什么是计算机算法,如何描述它们,以及如何来评估它们。

GO