SpringBoot系列之数据验证

经典语录:SpringBoot 是为了简化 Spring 应用的创建、运行、调试、部署等一系列问题而诞生的产物,自动装配的特性让我们可以更好的关注业务本身而不是外部的XML配置,我们只需遵循规范,引入相关的依赖就可以轻易的搭建出一个 WEB 工程

主要内容:

javax.validation 包和 hibernate-validator 验证包中常用注解及作用

普通参数属性与对象参数属性的验证方法

自定义注解的编写方式

利用一个验证类实现多个接口之间不同规则的验证

一、数据验证概述

对于任何一个应用而言,客户端做的数据有效性验证都不是安全有效的数据验证又是一个企业级项目架构上最为基础的功能模块,这时候就要求我们在服务端接收到数据的时候也对数据的有效性进行验证。

以前的验证方式

public String test1(String name) {
    if (name == null) {
        throw new NullPointerException("name 不能为空");
    }
    if (name.length() < 2 || name.length() > 10) {
        throw new RuntimeException("name 长度必须在 2 - 10 之间");
    }
    return "success";
}

上面这段代码就是对参数进行有效性校验,但仔细观察的话就会发现;随着参数的增加,格式的变化,校验数据有效性的代码愈发的繁琐杂乱,一点都不轻松

二、SpringBoot中存在的数据验证

2.1 JSR-303 注释介绍

这里只列举了 javax.validation 包下的注解,同理在 spring-boot-starter-web 包中也存在 hibernate-validator 验证包,里面包含了一些 javax.validation 没有的注解

注解 说明
@NotNull 限制必须不为null
@NotEmpty 验证注解的元素值不为 null 且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在 min 到 max 之间(也可以用在集合上)
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Null 限制只能为null(很少用)
@AssertFalse 限制必须为false (很少用)
@AssertTrue 限制必须为true (很少用)
@Past 限制必须是一个过去的日期
@Future 限制必须是一个将来的日期
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过 integer,小数部分的位数不能超过 fraction (很少用)

2.2 普通参数属性与对象属性的验证

属性验证对象

import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;

/**
    对象属性类
 */
public class Book {

    private Integer id;
    @NotBlank(message = "name 不允许为空")
    @Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间")
    private String name;
    @NotNull(message = "price 不允许为空")
    @DecimalMin(value = "0.1", message = "价格不能低于 {value}")
    private BigDecimal price;

    // 省略 GET SET ...
}

控制层添加验证后的代码(同理可以应用在其它层,但是一般用不着)

import com.battcn.pojo.Book;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.NotBlank;

/**
 * 控制层参数校验
 */
@Validated
@RestController
public class ValidateController {

    @GetMapping("/test2")
    public String test2(@NotBlank(message = "name 不能为空") @Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间") String name) {
        return "success";
    }

    @GetMapping("/test3")
    public String test3(@Validated Book book) {
        return "success";
    }
}

注解解释:

  • @Validated: 开启数据有效性校验,添加在类上即为验证方法,添加在方法参数中即为验证参数对象。(添加在方法上无效)
  • @NotBlank: 被注释的字符串不允许为空(value.trim() > 0 ? true : false
  • @Length: 被注释的字符串的大小必须在指定的范围内
  • @NotNull: 被注释的字段不允许为空(value != null ? true : false)
  • @DecimalMin: 被注释的字段必须大于或等于指定的数值

开始测试

第一步:配置全局异常处理

SpringBoot系列之数据验证_第1张图片 配置全局异常处理

测试普通参数属性

SpringBoot系列之数据验证_第2张图片 测试普通参数异常结果

测试对象参数异常结果

SpringBoot系列之数据验证_第3张图片 测试对象异常返回参数

 对异常信息进行封装

SpringBoot系列之数据验证_第4张图片 处理后的对象属性异常拦截器

 

SpringBoot系列之数据验证_第5张图片 处理后的对象异常测试结果

2.3 自定义 Validator 注解

当系统自带的注解无法满足我们的要求时候应该咋办呢?这就需要我们的自定义 Validator 注解出现了。javax.validation 包与 hibernate-validator 包中存在的注解几乎可以满足大部分的要求,又拥有基于正则表达式的@Pattern,为什么还需要自己去定义呢?原因如下

  • 正则效率不高
  • 正则可读性不好
  • 正则门槛较高,很多开发者并不会编写正则表达式

2.3.1 自定义一个@DateTime 注解

这里定义了一个 @DateTime 注解,在该注解上标注了 @Constraint 注解,它的作用就是指定一个具体的校验器类

import com.battcn.validator.DateTimeValidator;

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

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
自定义时间检验的注解
*/
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = DateTimeValidator.class)
public @interface DateTime {

    String message() default "格式错误";

    String format() default "yyyy-MM-dd";

    Class[] groups() default {};

    Class[] payload() default {};
}

 其中

  • message: 验证失败提示的消息内容
  • groups: 为约束指定验证组

2.3.2  定义具体验证类DateTimeValidator

定义校验器类 DateTimeValidator 实现 ConstraintValidator 接口,实现接口后需要实现它里的 initialize: 与 isValid: 方法。

package com.battcn.validator;

import com.battcn.annotation.DateTime;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.text.ParseException;
import java.text.SimpleDateFormat;

/**
 * 日期格式验证
 */
public class DateTimeValidator implements ConstraintValidator {

    private DateTime dateTime;

    @Override
    public void initialize(DateTime dateTime) {
        this.dateTime = dateTime;
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 如果 value 为空则不进行格式验证,为空验证可以使用 @NotBlank @NotNull @NotEmpty 等注解来进行控制,职责分离
        if (value == null) {
            return true;
        }
        String format = dateTime.format();
        if (value.length() != format.length()) {
            return false;
        }
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
        try {
            simpleDateFormat.parse(value);
        } catch (ParseException e) {
            return false;
        }
        return true;
    }
}

其中

  • initialize: 主要用于初始化,它可以获得当前注解的所有属性
  • isValid: 进行约束验证的主体方法,其中 value 就是验证参数的具体实例,context 代表约束执行的上下文环境。

这里的验证方式虽然简单,但职责明确;为空验证可以使用 @NotBlank@NotNull@NotEmpty 等注解来进行控制,而不是在一个注解中做各种各样的规则判断,应该职责分离

2.3.3 在控制层测试

 控制层代码

import com.battcn.annotation.DateTime;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 参数校验
 */
@Validated
@RestController
public class ValidateController {

    @GetMapping("/test")
    public String test(@DateTime(message = "您输入的格式错误,正确的格式为:{format}", format = "yyyy-MM-dd HH:mm") String date) {
        return "success";
    }
}

三、数据验证之数据分组验证

有的时候,我们对一个实体类需要有多中验证方式,在不同的情况下使用不同验证方式,比如说对于一个实体类来的 id 来说,新增的时候是不需要的,对于更新时是必须的,这个时候你是选择写一个实体类呢还是写两个呢?

3.1 定义分组验证器

定义一个验证组,里面写上不同的空接口类即可

/**
 * 验证组
 */
public class Groups {

    public interface Update {

    }

    public interface Default {

    }
}

3.2 验证的实体类

groups 属性的作用就让 @Validated 注解只验证与自身 value 属性相匹配的字段,可多个,只要满足就会去纳入验证范围;我们都知道针对新增的数据我们并不需要验证 ID 是否存在,我们只在做修改操作的时候需要用到,因此这里将 ID 字段归纳到 Groups.Update.class 中去,而其它字段是不论新增还是修改都需要用到所以归纳到 Groups.Default.class 中…


import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;

/**
 * @author Levin
 * @since 2018/6/7 0005
 */
public class Book {

    @NotNull(message = "id 不能为空", groups = Groups.Update.class)
    private Integer id;
    @NotBlank(message = "name 不允许为空", groups = Groups.Default.class)
    private String name;
    @NotNull(message = "price 不允许为空", groups = Groups.Default.class)
    private BigDecimal price;

    // 省略 GET SET ...
}

3.3 控制层接口代码

创建一个 ValidateController 类,然后定义好 insertupdate 俩个方法,比由于 insert 方法并不关心 ID 字段,所以这里 @Validated 的 value 属性写成 Groups.Default.class 就可以了;而 update 方法需要去验证 ID 是否为空,所以此处 @Validated 注解的 value 属性值就要写成 Groups.Default.class, Groups.Update.class;代表只要是这分组下的都需要进行数据有效性校验操作

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 参数校验
 */

@RestController
public class ValidateController {

    @GetMapping("/insert")
    public String insert(@Validated(value = Groups.Default.class) Book book) {
        return "insert";
    }

    @GetMapping("/update")
    public String update(@Validated(value = {Groups.Default.class, Groups.Update.class}) Book book) {
        return "update";
    }
}

 

你可能感兴趣的:(SpringBoot系列)