SpringBoot2.x学习-数据校验

一、为什么要数据效验

​ 开发系统的时候,为了保证数据完整性、合法性、有效性,一般都会对进入系统的业务数据进行效验。除了前端必须要进行效验之外,后端也要对提交的数据进行效验。其他场景如:开发对外的API、编写一个工具类等更要做好数据效验工作。

二、Bean Validation介绍

​ 以前在后端编写数据效验代码我都是用IF-ELSE来判断,什么字段不能为空啦、长度不能超过啦、值不在字典范围里面啦…等等。代码里面充斥着大量IF-ELSE语句,非常的不好维护。

​ Java 规范提案(JSR) 提交了JSR-303、JSR-349以及JSR-380等来规范数据效验机制。这3个JSR统一被称为Bean Validation。Bean Validation为Java数据校验提供了更加规范化、通用化、灵活度更高的校验方法。

​ JSR只是制定了一个规范,具体的数据效验功能是由各种技术框架实现的。其中Hibernate Validator就是根据JSR规范实现校验功能的一个框架。Spring Boot2(2.3以前的版本)中的 spring-boot-starter-web启动器 已经默认关联依赖了Hibernate Validator 6.X支持,本文例子就是基于这个效验框架来编写。如果是Spring Boot2.3及以后的版本,则需要单独引入依赖

 <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
      </dependency>

Java数据效验主要是用注解在JavaBean的属性字段上声明数据效验准则,数据效验主要包括注解、效验器以及效验工厂。

Bean Validation:https://beanvalidation.org/

三、基本数据效验

1.简单效验

在Spring Boot2项目中编写简单测试用例,数据效验主要是围绕实体类展开的,示例代码如下:

**
 * 雇员信息对象Vo
 *
 * @author David Lin
 * @version: 1.0
 * @date 2020-03-21 17:03
 */
@Getter
@Setter
public class EmpVo {
    /**
     * 员工编号
     */
    @NotNull(message = "用来区分员工的标识不能为空!!")
    private Long empno;
    /**
     * 员工姓名
     */
    @NotBlank
    @Length(min = 3,max = 10)
    private String empname;
    /**
     * 工作岗位
     */
    private String job;
    /**
     * 领导者编号
     */
    private int mgr;
    /**
     * 入职时间
     * 自定义输出格式
     */
    @Past
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Timestamp hiredate;
    /**
     * 薪水
     */
    @Min(0)
    private double sal;
    /**
     * 奖金
     */
    private double comm;
    /**
     * 所在部门编号
     */
    private int deptno;
    /**
     * 年龄
     */
    @Min(1)
    @Max(value = 120 ,message = "大于120岁不存在的!!")
    private int age;
    /**
     * 部门名称
     */
    private String dname;

    /**
     * 创建时间
     */
    private Timestamp createTime;
    /**
     * 更新时间
     */
    private Timestamp updateTime;
}

​ 使用效验器进行效验 代码如下:

    @Test
    public void testEmpvo() {
        EmpVo empVo = new EmpVo();
        empVo.setEmpname("smith");
        empVo.setAge(200);
        empVo.setSal(20000);

        //引入校验工具
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        //获取校验器
        Validator validator = factory.getValidator();
        //执行校验 获取效验的结果
        Set<ConstraintViolation<EmpVo>> validateResult = validator.validate(empVo);
        //如果校验通过则返回的Set 对象集合长度为0
        if (validateResult.size() == 0) {
            log.info("效验通过啦!!!");
        } else {
            //遍历效验结果
            Iterator<ConstraintViolation<EmpVo>> iterator = validateResult.iterator();
            while (iterator.hasNext()) {
                ConstraintViolation<EmpVo> next = iterator.next();
                log.info("验证失败的字段是:{}, 错误信息为:{}", next.getPropertyPath(), next.getMessage());
            }
        }
    }

输出如下:

验证失败的字段是:empno, 错误信息为:用来区分员工的标识不能为空!!
验证失败的字段是:age, 错误信息为:大于120岁不存在的!!

执行结束之后 ,validateResult就是效验结果,如果效验通过这个Set集合对象就是空的。

遍历效验结果也可以使用Java8的 Lambda 表达式进行简化,输出结果都是一样的,代码如下:

 if (validateResult.size() == 0) {
            log.info("效验通过啦!!!");
    }else{
            //使用Java8的  Lambda 表达式 简化
            validateResult.forEach(validate -> {
                log.info("验证失败的字段是:{}, 错误信息为:{}", validate.getPropertyPath(), validate.getMessage());
            });
        }

2.Hibernate Validator内置效验注解(Constraint )

Hibernate Validator内置效验注解如下( Built-in Constraint definitions):

@Null被注释的元素必须为 null
@NotNull被注释的元素必须不为 null
@NotEmpty被注释的字符串的必须非空 must not be {@code null} nor empty.
@NotBlank验证字符串非null,且长度必须大于0
@AssertTrue被注释的元素必须为 true
@AssertFalse被注释的元素必须为 false
@Min被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Negative被注释的元素必须是一个严格意义上的负数
@NegativeOrZero被注释的元素必须是一个负数或者0
@Positive被注释的元素必须是一个严格意义上正数
@PositiveOrZero被注释的元素必须是一个正数或者0
@Size被注释的元素的大小必须在指定的范围内(included)
@Digits被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past被注释的元素必须是一个过去的日期时间
@PastOrPresent被注释的元素必须是一个过去的日期时间或者是当前日期世纪
@Future被注释的元素必须是一个将来的日期时间
@FutureOrPresent被注释的元素必须是一个将来的日期时间或者当前日期时间
@Pattern被注释的元素必须符合指定的正则表达式
@Email被注释的元素必须是电子邮箱地址
@Length被注释的字符串的大小必须在指定的范围内
@Range(min=,max=,message=)被注释的元素必须在合适的范围内

四、自定义校验规则

1.组合已有注解校验

可以通过组合已有的效验规则来实现新的效验规则

例如 定义新的效验注解 @MyAge

@Min(value = 1,message = "年龄最小不能小于1")
@Max(value = 120,message = "年龄最大不能超过120")
@Constraint(validatedBy = {}) //不指定效验器
@Documented
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAge {
    String message() default "年龄大小必须大于1并且小于120";

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

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

在实体类属性字段上使用 自定义组合注解

@Getter
@Setter
public class EmpVo {
    /**
     * 年龄
     */
   @MyAge
    private int age;

2.自定义校验器

Bean Validation支持自定义效验规则,对属性字段的一个效验被称为Constraint,一个Constraint由一个Annotation(注解)绑定1~N个Validator(效验器)组成。因此通过新增注解和效验器来定义新的效验规则。

(1)声明一个自定义效验注解

@Constraint(validatedBy = { DeptNoTypeValidator.class }) //指定校验器
@Documented
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD ,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyDeptNo {
    //自定义提示信息
    String message() default "部分编号只能是10、20、30、40等几个编号!!";

    //自定义分组 将 validator 进行分类,不同的类 group 中会执行不同的 validator 操作
    Class<?>[] groups() default {};

    //指定效验问题的级别
    Class<? extends Payload>[] payload() default {};
}

这个注解是作用在 Field 字段、Method方法、类型声明、注解声明上,运行时生效,触发的是DeptNoTypeValidator 这个效验器。

(2)自定义 Validator(效验器)

自定义效验器是真正进行验证的逻辑代码

/**
 * 部门编号效验器
 *
 * @author David Lin
 * @version: 1.0
 * @date 2020-03-22 10:21
 */
public class DeptNoTypeValidator implements ConstraintValidator<MyDeptNo, EmpVo> {
    /**
     * 部门编号字典 ,可以改成枚举
     */
    private final List<Integer> deptNoList = Arrays.asList(new Integer[]{10, 20, 30, 40});

    @Override
    public void initialize(MyDeptNo constraintAnnotation) {
    }

    @Override
    public boolean isValid(EmpVo empVo, ConstraintValidatorContext constraintValidatorContext) {
        return deptNoList.contains(empVo.getDeptno());
    }
}

自定义的效验器必须实现ConstraintValidator这个接口,并在范型中声明对应的自定义校验注解和数据类型(ConstraintValidator<A extends Annotation, T>,A是绑定的注解类型、T是数据类型)。本文的@MyDeptNo注解是作用在类声明上,所以数据类型是实体类。

(3)在实体类上使用自定义注解

@Getter
@Setter
@MyDeptNo
public class EmpVo {
    /**
     * 员工编号
     */
    @NotNull(message = "用来区分员工的标识不能为空!!")
    private Long empno;
    /**
     * 员工姓名
     */
    @NotBlank
    @Length(min = 3,max = 10)
    private String empname;
    /**
     * 薪水  查询的时候不返回这个字段值
     */
    @Range(min = 0,max = 20000,message = "薪水不在范围内的啊")
    private double sal;
    /**
     * 所在部门编号
     */
    private int deptno;
    /**
     * 年龄
     */
   @MyAge
    private int age;
    ......
}

3.分组效验

​ 在实际业务场景中数据效验规则并不是一成不变的,有时候需要根据某些状态来对单个或者一组属性字段进行效验。这个时候就可以用到分组效验功能,即 根据状态启用一组约束。注解里面有个groups参数,就是用来指定分组的,如果groups没有指定,则效验都属于javax.validation.groups.Default默认分组。

​ 假设有这样的场景:保存和更新的API共用一个VO/DTO,保存的API需要效验empname(姓名),createTime(创建时间),更新的API需要效验empno(主键标识)、empname(姓名)、updateTime(更新时间)。也就是说保存的API不需要效验empno、和updateTime,更新的API不需要效验createTime字段,保存和更新同时都要效验empname。

(1).定义 groups 的分组接口

这里用没有任何功能的类或者接口定义分组都可以的

import javax.validation.groups.Default;
/**
 * 保存效验分组
 * @author David Lin
 * @version: 1.0
 * @date 2020-03-22 14:38
 */
public interface CreateGroup  extends Default {}
import javax.validation.groups.Default;
/**
 * 更新效验分组
 * @author David Lin
 * @version: 1.0
 * @date 2020-03-22 14:40
 */
public interface UpdateGroup extends Default {}

(2).在校验的注解上通过groups指定分组

@Getter
@Setter
@MyDeptNo
public class EmpVo {
    /**
     * 员工编号
     */
    @NotNull(groups = {UpdateGroup.class}, message = "用来区分员工的标识不能为空!!")
    private Long empno;
    /**
     * 员工姓名
     */
    @NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
    @Length(groups = {CreateGroup.class, UpdateGroup.class}, min = 3, max = 10)
    private String empname;
    /**
     * 创建时间
     */
    @NotNull(groups = {CreateGroup.class})
    @FutureOrPresent(groups = {CreateGroup.class}, message = "创建时间必须是当前或者将来时间!!")
    private Timestamp createTime;
    /**
     * 更新时间
     */
    @NotNull(groups = {UpdateGroup.class})
    @FutureOrPresent(groups = {UpdateGroup.class}, message = "更新时间必须是当前或者将来时间!!")
    private Timestamp updateTime;
    .......
}

(3).执行分组校验

EmpVo empVo = new EmpVo();
empVo.setEmpname("sm");
empVo.setAge(29);
empVo.setSal(20000);
empVo.setDeptno(20);
empVo.setCreateTime(new Timestamp(1314));
.......
//指定分组效验
Set<ConstraintViolation<EmpVo>> validateResult = validator.validate(empVo,CreateGroup.class);
 validateResult.forEach(validate -> {
            log.info("验证失败的字段是:{}, 错误信息为:{}", validate.getPropertyPath(), validate.getMessage());
        });

输出结果为:

验证失败的字段是:createTime, 错误信息为:创建时间必须是当前或者将来时间!!
验证失败的字段是:empname, 错误信息为:长度需要在3和10之间

这里指定分组效验之后,只会执行groups = {CreateGroup.class}注解的校验。

如果想使用默认分组效验,则需要在效验注解上指定默认分组:

 /**
     * 创建时间
     */
    @NotNull(groups = {CreateGroup.class,Default.class})
    @FutureOrPresent(groups = {CreateGroup.class,Default.class}, message = "创建时间必须是当前或者将来时间!!")
    private Timestamp createTime;
  //使用默认分组校验
  Set<ConstraintViolation<EmpVo>> validateResult = validator.validate(empVo);

4.效验错误级别

注解里面的payload参数是用来标识 "效验问题"级别的,就是在效验数据时对"效验问题"进行分类。

(1).声明自定义问题级别接口

**
 * 效验问题级别
 *
 * @author David Lin
 * @version: 1.0
 * @date 2020-03-22 15:27
 */
public class PayLoadLevel {
     //信息提示级别
     public static interface INFO extends Payload {}
     // 警告级别
     public static interface WARN extends  Payload{}
}

(2).在JavaBean上指定效验问题级别

 /**
     * 薪水
     */
    @Range(min = 0, max = 20000, message = "薪水不在范围内的啊",payload = PayLoadLevel.WARN.class)
    private double sal;

(3).从效验结果里面获取问题级别

  log.info("问题级别为:{}",validate.getConstraintDescriptor().getPayload().toString());

5.顺序执行校验

五、Spring Boot中各种使用方法

Spring MVC对Hibernate Validation进行了二次封装,添加了自动效验功能,并将效验结果信息封装进了BindingResult类中。

1.简单使用

(1).@Validated注解对Controller方法的参数进行效验

 /**
     * 保存雇员信息
     * @param empVo
     * @return
     */
    @PostMapping("/saveemp")
    public Integer saveEmp(@RequestBody  @Validated EmpVo empVo) {
        Emp emp = new Emp();
        BeanUtils.copyProperties(empVo,emp);
        return empService.saveEmp(emp);
    }

用Postman工具模拟请求,Headers设置Content-Type为application/json
在这里插入图片描述
可以看到效验不通过自动抛出了MethodArgumentNotValidException异常,返回的结果不太友好,直接将整个错误对象相关信息都响应给客户端了,可以采用全局异常处理器来统一处理效验异常和业务异常,然后统一返回特定格式数据给客户端。

(2).单个参数效验

@RestController
@Validated
public class EmpController {

    /**
     * 日志操作对象
     */
    Logger logger = LoggerFactory.getLogger(EmpController.class);
    /**
     * 员工业务操作对象
     */
    @Resource
    private EmpService empService;

   @RequestMapping("/auth")
    public String authorization(@Length(min = 11,message = "Clientid长度至少11位") @RequestParam("clientid") String clientid,
                                @NotBlank(message = "口令不能为空") @RequestParam("pass") String pass) {
        if ("admin".equals(clientid) && "11111111".equals(pass)) {
            return "success";
        }
        return "error";
    }

在这里插入图片描述
可以看到自动抛出了ConstraintViolationException。@RequestParam注解有一个required参数,默认值为true,指示参数是否必须绑定,如果参数少了其中一个或者都没带参数请求,则抛出MissingServletRequestParameterException。

注意点:单个参数效验是在Controller类上面增加@Validated注解,而不是在方法参数上加。

(3).全局异常处理+定制消息返回

参数校验失败会自动抛出异常,推荐使用全局异常拦截处理的方式去处理效验失败后的处理流程,这样能能减少Controller层或Services层的代码逻辑处理,然后统一响应格式给客户端。

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 全局处理 MethodArgumentNotValidException  异常
     * @param methodArguException
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map handleMethodArgumentNotValidException(MethodArgumentNotValidException methodArguException) {
        BindingResult bindingResult = methodArguException.getBindingResult();
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        StringBuilder messBuilder = new StringBuilder(128);
        for (FieldError fieldError : fieldErrors) {
            messBuilder.append(fieldError.getDefaultMessage()).append(";");
        }
        //响应格式用Map, 比较推荐的是定义一个统一响应对象 
        Map result = new HashMap();
        result.put("code", "10001");
        result.put("msg", "参数效验失败!" + messBuilder.toString());
        return result;

    }

    /**
     * 全局处理ConstraintViolationException异常
     *
     * @param constraintViolationEx
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Map handleConstraintViolationException(ConstraintViolationException constraintViolationEx) {
        StringBuilder messBuilder = new StringBuilder(64);
        Set<ConstraintViolation<?>> constraintViolations = constraintViolationEx.getConstraintViolations();

        //使用Java8的  Lambda 表达式 简化
        constraintViolations.forEach(validate -> {
            messBuilder.append(validate.getMessage());
        });

        Map result = new HashMap();
        result.put("code", "10001");
        result.put("msg", "参数效验失败!" + messBuilder.toString());
        return result;
    }

    /**
     * 全局处理MissingServletRequestParameterException异常
     *
     * @param missEx
     * @return
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public Map handleMissingServletRequestParameterException(MissingServletRequestParameterException missEx) {
        StringBuilder messBuilder = new StringBuilder(64);
        messBuilder.append(missEx.getParameterName()).append(missEx.getMessage());
        Map result = new HashMap();
        result.put("code", "10001");
        result.put("msg", messBuilder.toString());
        return result;
    }
}

现在再次请求 结果如下
在这里插入图片描述

2.效验模式

数据校验不会在第一次碰到参数错误时就返回,而是会校验完成所有的参数。

3.级联效验(嵌套效验)

使用@Valid注解进行级联效验…

4.@Valid与@Validated

1.@Valid:标准JSR-303规范的标记型注解,用来标记验证属性和方法返回值,进行级联和递归校验

2.@Validated:是Spring提供的注解,是标准JSR-303的一个变种,提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。

不同点:

  • @Validated只能用在类、方法和参数上,而@Valid可用于方法、字段、构造器和参数上
  • @Validated支持分组,而@Valid不支持

在校验数据时,使用@Valid@Validated注解并没有特别的差异,@Validated注解可以用于类级别,而且支持分组,而@Valid注解可以用在属性级别约束,用来表示级联校验。关于@Valid@Validated的区别可以参考别人的博客

5.常用效验规则

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
您好!对于使用Spring Boot 2.x和MyBatis集成MySQL,并实现微信授权登录的问题,我可以给您一些指导。 首先,您可以按照以下步骤进行操作: 1. 配置MySQL数据库:在`application.properties`或`application.yml`文件中设置MySQL数据库的连接信息,包括数据库URL、用户名和密码等。 2. 引入依赖:在您的项目的`pom.xml`文件中添加Spring Boot、MyBatis和MySQL的相关依赖。例如: ```xml <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!-- MySQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 其他依赖... --> </dependencies> ``` 3. 创建实体类和Mapper:创建与数据库表对应的实体类,并使用MyBatis的注解或XML配置文件来定义Mapper接口和SQL语句。 4. 配置MyBatis:在`application.properties`或`application.yml`文件中配置MyBatis相关的属性,如Mapper接口的扫描路径、XML配置文件的位置等。 5. 编写业务逻辑:根据您的需求,编写相应的业务逻辑代码,包括微信授权登录的逻辑处理。 6. 实现微信授权登录:使用微信开放平台提供的API,获取用户的授权信息,并将相关信息保存到数据库中。您可以使用第三方开源库(如unapp)来简化微信授权登录的过程。 需要注意的是,以上只是一个大致的步骤,具体实现还需根据您的项目需求进行调整。同时,为了保证代码的安全性和可靠性,建议您进行适当的异常处理、参数校验等。 希望以上内容对您有所帮助!如果您有任何疑问,请随时提问。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值