更优雅的Java参数验证,告别冗长的if...else式参数校验

3,921 阅读5分钟

验证框架主要分为两种,即分层验证与JavaBean验证

分层验证模型

传统的校验模式,即每一层都添加数据验证。但是其验证逻辑重复性大,会出现冗余代码过多的情况

JavaBean验证

JavaBean验证模式指向前端接收数据时,在JavaBean上做数据校验。其校验逻辑无需写在其他层中

Bean Validation

Bean Validation 为 JavaBean 验证定义了相应的元数据模型和API。

---维基百科

| 分类 | 限制 | 说明
| ------------------------- | ---------- ------------------ | | 空/非空检查 | @NULL | 限制只能为NULL | || @NotNull | 限制必须不为NULL | | || @NotNull | 验证注解的元素值不为Null且不为空(字符串长度不为0,集合大小不为0) | | || @NotBlack | 验证注解的元素值不为空(不为Null,去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 | | | Boolean值检查 | @AssertFalse | 限制必须为False | || @AssertTrue | 限制必须为True | | | 长度检查 | @Size(max,min) | 限制字符长度必须在min到max之间 | || @Leanth | 限制字符长度必须在min到max之间 | | | 日期检查 | @Future | 限制日期为当前时间之后 | || @FutureOrPresent | 限制日期为当前时间或之后 | | || @Past | 限制日期为当前时间之前 | | || @PastOrPresent | 限制日期为当前时间或之前 | | | 数值检查 | @Max(Value) | 限制必须为一个不大于指定值的数字 | || @Min(Value) | 限制必须为一个不小于指定值的数字 | | || @DecimalMin(value) | 限制必须为一个不小于指定值的数字 | | || @DecimalMax(value) | 限制必须为一个不小于指定值的数字 | | || @Digits(integer,fraction) | 限制必须为小数,且整数部分的位数不能超过Integer,小数部分的位数不能超过fraction | | || @Negative | 限制必须为负整数 | | || @NegativeOrZero(value) | 限制必须为负整数或零 | | || @Positive(value) | 限制必须为正整数 | | || @PositiveOrZero(value) | 限制必须为正整数或零 | | | 其他检查 | @Pattern(Value) | 限制必须符合指定的正则表达式 | || @Email | 限制必须为email格式 | |

@Validated用于Controller层向前端接收参数时,对参数合法性进行校验的开启

    @PostMapping("/testMethod")
	@ApiOperation(value = "testMethod")
	public RespResult testMethod(@RequestBody @Validated TestParam testParam){
        ... ...
    }

@Validated只用于对功能的开启,代表本次开启参数校验

真正的参数校验注解在要进行参数合法性校验的类中进行书写

/**
 * 测试类
 *
 * @author WeiYL
 * @date 2022/7/27 16:15
 */
@Data
@ApiModel(value = "测试处理类")
public class TestParamDTO{
	/**
	 * 名称
	 */
	@ApiModelProperty(value = "隐患描述", example = "隐患描述编辑测试")
	@Length(max = 512, message = "长度非法,超过{max}字符!")
	String testName;

	/**
	 * 描述
	 */
	@ApiModelProperty(value = "描述", example = "描述编辑测试")
	@NotBlank(message = "描述不能为空")
	@Length(max = 512, message = "长度非法,超过{max}字符!")
	String testDescribe;

	/**
	 * 等级
	 */
	@ApiModelProperty(value = "等级", example = "1")
	@NotBlank(message = "等级不能为空")
	@Length(min = 1, max = 1, message = "等级字符长度不为{max}")
	String testGrade;
}

@Length 长度校验

@Length(min = 1, max = 10, message = "字符长度为{min}-{max}")

@NotBlank 字符非空判断

只用于String,不能为null且trim()之后size>0

验证注释的String字符串不为NULL或为空,纯空格的String也是不符合规则的

@NotBlank(message = "等级不能为空")

@NotEmpty

不能为NULL,且Size>0。

@NotEmpty注解的String、Collection、Map、数组是不能为NULL或长度为0的(不能为 “ ” )

@NotEmpty(message = "等级不能为空")

@NotNull

不能为NULL,但是可以为Empty,没有Size的约束

带注解的元素不能为NULL,但可以为“”。接受任何类型。

@NotNull(message = "等级不能为空")

级联验证

@Valid 与 @Validated

@Valid: 没有分组的功能。

@Valid: 可以用在方法、构造函数、方法参数和成员属性(字段)上

@Validated: 提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制

@Validated: 可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上

两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能

分组校验功能

需求: 新增和修改功能使用同一个DTO,新增时对ID字段无要求,修改时校验ID不为空

  1. 创建分组接口
/**
 * <h1>参数验证分组</h1>
 * @date 2022/7/23 10:18
 */
public class GroupType{

	/**
	 * <h2>修改</h2>
	 */
	public interface UpdateGroup{ };

	/**
	 * <h2>新增</h2>
	 */
	public interface CreateGroup{ };
}
  1. 在DTO的参数校验注解中设置groups属性

/**
 * <h1>...操作参数</h1>
 *
 * @author WeiYL
 * @date 2022/7/21 9:56
 */
@Data
public class PeccancyStandardDTO{

	/**
	 * 主键
	 */
	@NotBlank(groups = {GroupType.UpdateGroup.class})
	@ApiModelProperty(value = "主键,修改时必填", required = true, example = "11111")
	private String id;
}
  1. 在Controller层的参数列表中设定分组
/**
 * ...页面
 * @author ...
 * @date 2022/7/23 19:33
 */
@Api(tags = "...页面")
@Slf4j
@RestController
@RequestMapping("/check")
@AllArgsConstructor
public class controller{


	@PostMapping("/update")
	@ApiOperation(value = "修改作业隐患记录")
	public String update(@RequestBody @Validated(value = GroupType.UpdateGroup.class) ...DTO DTO){
		... ...
	}
    
}

组序列

对几个分组同时验证

1. 对分组进行追加

缺点:没有对一个组的验证顺序做明确定义

2. 使用@GroupSequence注解进行排序

/**
 * <h1>参数验证分组</h1>
 * @date 2022/7/23 10:18
 */
public class GroupType{

	/**
	 * <h2>修改</h2>
	 */
	public interface UpdateGroup{ };

	/**
	 * <h2>新增</h2>
	 */
	public interface CreateGroup{ };
    
    @GroupSequence({
    LoginGroup.class,
    RegisterGroup.class
    })
}

输入输出参数校验

    // 验证器对象
    private Validator validator;
    // 待验证对象
    private UserInfo userInfo;
    // 验证结果集合
    private Set<ConstraintViolation<UserInfo>> set;
    // 验证结果集合
    private Set<ConstraintViolation<UserInfoService>> otherSet;


    /**
     * 对方法输入参数进行约束注解校验
     */
    @Test
    public void paramValidation() throws NoSuchMethodException {
        // 获取校验执行器
        ExecutableValidator executableValidator =
                validator.forExecutables();

        // 待验证对象
        UserInfoService service = new UserInfoService();
        // 待验证方法
        Method method = service.getClass()
                .getMethod("setUserInfo", UserInfo.class);
        // 方法输入参数
        Object[] paramObjects = new Object[]{new UserInfo()};

        // 对方法的输入参数进行校验
        otherSet = executableValidator.validateParameters(
                service,
                method,
                paramObjects);
    }

对返回值进行约束校验

    // 验证器对象
    private Validator validator;
    // 待验证对象
    private UserInfo userInfo;
    // 验证结果集合
    private Set<ConstraintViolation<UserInfo>> set;
    // 验证结果集合
    private Set<ConstraintViolation<UserInfoService>> otherSet;



    /**
     * 对方法返回值进行约束校验
     */
    @Test
    public void returnValueValidation()
            throws NoSuchMethodException,
            InvocationTargetException, IllegalAccessException {

        // 获取校验执行器
        ExecutableValidator executableValidator =
                validator.forExecutables();

        // 构造要验证的方法对象
        UserInfoService service = new UserInfoService();
        Method method = service.getClass()
                .getMethod("getUserInfo");

        // 调用方法得到返回值
        Object returnValue = method.invoke(service);

        // 校验方法返回值是否符合约束
        otherSet = executableValidator.validateReturnValue(
                service,
                method,
                returnValue);
    }
public @Valid UserInfo getUserInfo(){
    return new UserInfo();
}

对构造函数输入参数进行校验

    /**
     * 对构造函数输入参数进行校验
     */
    @Test
    public void constructorValidation()
        throws NoSuchMethodException {
    
        // 获取验证执行器
        ExecutableValidator executableValidator =
            validator.forExecutables();
    
        // 获取构造函数
        Constructor constructor =
            UserInfoService.class
                .getConstructor(UserInfo.class);
        Object[] paramObjects = new Object[]{new UserInfo()};
    
        // 校验构造函数
        otherSet = executableValidator
            .validateConstructorParameters(
            constructor, paramObjects);
    }