Spring validation参数校验高级篇之跨参数校验Cross-Parameter及分组序列校验@GroupSequenceProvider、@GroupSequence

本文详细介绍Springvalidation的高级特性,包括跨参数校验如何实现日期和数字比较,以及分组序列校验如何通过自定义枚举和GroupSequenceProvider进行参数验证。通过实例代码展示了这些在实际项目中的应用。
摘要由CSDN通过智能技术生成

Spring validation参数校验系列

1、Spring validation参数校验基本使用

2、Spring validation参数校验之自定义校验规则及编程式校验等进阶篇

3、【源码】Spring validation参数校验原理解析之Controller控制器参数校验中@RequestBody参数校验实现原理

4、【源码】Spring validation参数校验原理解析之Controller控制器参数校验中@ModelAttribute及实体类参数校验实现原理

5、【源码】Spring validation参数校验原理解析之基本类型参数及Service层方法参数校验实现原理

6、【源码】Spring validation校验的核心类ValidatorImpl、MetaDataProvider和AnnotationMetaDataProvider源码分析

7、Spring validation参数校验高级篇之跨参数校验Cross-Parameter及分组序列校验@GroupSequenceProvider、@GroupSequence

8、【源码】Spring validation参数校验之跨参数校验Cross-Parameter原理分析

9、【源码】Spring validation参数校验之分组序列校验@GroupSequenceProvider、@GroupSequence的实现原理

前言

《Spring validation参数校验系列》来到了第七篇,从基本使用到实现原理,层层深入熟悉并理解Validation的参数校验。这一篇带大家一起熟悉一下在开发中也会经常碰到的参数校验规则:跨参数校验和分组系列校验。

一、跨参数校验Cross-Parameter

在几乎所有后端项目都会碰到的一个场景,根据起止日期进行搜索。搜索时,如果起始日期和截止日期都有值,那么截止日期必须大于等于起始日期。在这种场景中,就可以通过跨参数校验来实现。直接上代码。

1.1 自定义一个跨参数校验的注解。

package com.jingai.validation.custom;

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

/**
 * 跨参数的大于等于比较
 */
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = CrossParameterComparisonValidator.class)
public @interface CrossParameterComparison {

    String message() default "跨参数大小比较失败";
    Class<?>[] groups() default {};
    String[] propertyNames() default {}; // 要比较的参数名称,可为空
    int firstInd() default 1; // 第一个参数下标,从1开始
    int secondInd() default 1; // 第二个参数下标,从1开始
    Class<?> propertyType() default Number.class; // 要比较的参数的类型
    String datePattern() default ""; // 日期格式
    Class<? extends Payload>[] payload() default {};

}

1.2 实现注解的约束校验器

package com.jingai.validation.custom;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
// 必须添加该注解,才能在isValid()方法中接收Object[]参数
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class CrossParameterComparisonValidator implements ConstraintValidator<CrossParameterComparison, Object[]> {

    private Class<?> propertyType;
    private String[] propertyNames;
    private int firstInd;
    private int secondInd;
    private DateFormat format;

    @Override
    public void initialize(CrossParameterComparison constraintAnnotation) {
        // 获取主键中的信息
        propertyType = constraintAnnotation.propertyType();
        propertyNames = constraintAnnotation.propertyNames();
        String pattern = constraintAnnotation.datePattern();
        if(Date.class.getSimpleName().equals(propertyType.getSimpleName())) {
            if(StringUtils.hasText(pattern)) {
                format = new SimpleDateFormat(pattern);
            } else {
                format = new SimpleDateFormat("yyyy-MM-dd");
            }
        }
        firstInd = constraintAnnotation.firstInd() - 1;
        secondInd = constraintAnnotation.secondInd() - 1;
        if(firstInd < 0)
            firstInd = 0;
        if(firstInd > secondInd) {
            secondInd = firstInd;
        }
    }

    @Override
    public boolean isValid(Object[] value, ConstraintValidatorContext context) {
        if(value == null || value.length < secondInd || value[firstInd] == null || value[secondInd] == null) {
            return true;
        }
        // 支持日期的比较
        if(Date.class.getSimpleName().equals(propertyType.getSimpleName())) {
            try {
                if(format.parse(value[firstInd].toString()).getTime() <=
                        format.parse(value[secondInd].toString()).getTime()) {
                    return true;
                }
            } catch (ParseException e) {
                log.error(String.format("日期转换失败:日期字符串为%s、%s", value[firstInd], value[secondInd]));
            }
        } else if(Number.class.getSimpleName().equals(propertyType.getSimpleName())) { // 支持数据的比较
            if(((Number)value[secondInd]).longValue() - ((Number)value[firstInd]).longValue() >= 0) {
                return true;
            }
        }
        if(propertyNames != null && propertyNames.length == 2) {
            // 自定义校验失败信息
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(String.format("参数%s【值:%s】没有小于等于%s【值:%s】的值",
                    propertyNames[0], value[firstInd].toString(), propertyNames[1], value[secondInd].toString()))
                    .addConstraintViolation();
        }
        return false;
    }
}

CrossParameterComparisonValidator支持针对日期以及数字类型的判断。在实际中,可以进行扩展,支持更多的数据类型。

1.3 跨参数校验使用

    @GetMapping("numbertest")
    @CrossParameterComparison(firstInd = 1, secondInd = 2, propertyNames = {"start", "end"})
    public Map<String, Object> numberTest(@Min(value = 1) int start, @Min(value = 1) int end) {
        // 省略业务逻辑
        return ResponseUtil.success();
    }

    @GetMapping("search")
    @CrossParameterComparison(firstInd = 2, secondInd = 3, propertyNames = {"startDate", "endDate"},
            datePattern = "yyyy-MM-dd", propertyType = Date.class)
    public Map<String, Object> search(String name, String startDate, String endDate) {
        // 省略业务逻辑
        return ResponseUtil.success();
    }

1.4 访问接口

​​

​​

二、分组序列校验@GroupSequenceProvider

在一些场景中,某个参数的值是随着另一个参数的值而进行变动的。以下以积分维护为例,积分增加是消费、参加活动、购买等方式获得,积分减少是抵扣、过期等方式扣减,针对积分是增还是减,传入的类型是不同的。此时可以定义业务类型在增加和减少的枚举中为不同的组,然后自定义DefaultGroupSequenceProvider,在DefaultGroupSequenceProvider的getValidationGroups()方法中,根据是增加还是减少,返回不同的组。代码如下:

2.1 自定义积分枚举类型

package com.jingai.validation.custom;

/**
 * 积分类型
 */
public enum IntegralType implements BaseEnum {
    ADD("1", "增加"),
    SUB("0", "扣减");

    private String code;
    private String display;

    private IntegralType(String code, String display) {
        this.code = code;
        this.display = display;
    }

    @Override
    public String getCode() {
        return this.code;
    }

    @Override
    public String getDisplay() {
        return this.display;
    }
}
package com.jingai.validation.custom;

/**
 * 积分增加的业务类型
 */
public enum IntegralAddType implements BaseEnum {

    CONSUM_RETURN("1", "消费返还"),
    BUY("2", "购买");


    private String code;
    private String display;

    private IntegralAddType(String code, String display) {
        this.code = code;
        this.display = display;
    }

    @Override
    public String getCode() {
        return this.code;
    }

    @Override
    public String getDisplay() {
        return this.display;
    }
}
package com.jingai.validation.custom;

/**
 * 积分扣减的业务类型
 */
public enum IntegralSubType implements BaseEnum {

    CONSUM_OFFSET("1", "消费抵用"),
    OVERDUE("2", "过期"),
    RETURN_GOODS("3", "退货扣减");

    private String code;
    private String display;

    private IntegralSubType(String code, String display) {
        this.code = code;
        this.display = display;
    }

    @Override
    public String getCode() {
        return this.code;
    }

    @Override
    public String getDisplay() {
        return this.display;
    }
}

2.2 自定义枚举校验注解

package com.jingai.validation.custom;

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

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 自定义注解,判断是否值在指定的枚举中
 */
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 标记由哪个类来执行校验逻辑,该类需要实现ConstraintValidator接口
@Constraint(validatedBy = InEnumValidator.class)
@Repeatable(InEnum.List.class)
public @interface InEnum {
    /**
     * 枚举类型
     */
    Class<? extends BaseEnum> enumType();

    String message() default "值不在枚举类型中";

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

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

    /**
     * 支持在同一个属性中添加多次该注解
     * @see NotBlank
     */
    @Target({ElementType.METHOD, ElementType.FIELD})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        InEnum[] value();
    }

}

该注解的代码在

Spring validation参数校验之自定义校验规则及编程式校验等进阶篇_spring validation 中list中单个对象校验-CSDN博客
 

这篇已经使用过。在分组序列校验中,同一个参数,在不同的分组中使用不同的值,所以需要使用多次使用该注解,需要在@InEnum定义中添加public @interface List和@Repeatable(InEnum.List.class)信息。InEnumValidator的代码没有改动,这里就不贴出来了。

2.3 自定义IntegralDetailGroupSequenceProvider

package com.jingai.validation.custom;

import com.jingai.validation.groups.IntegralDetailValidateGroup;
import com.jingai.validation.vo.IntegralDetailVo;
import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider;

import java.util.ArrayList;
import java.util.List;

public class IntegralDetailGroupSequenceProvider implements DefaultGroupSequenceProvider<IntegralDetailVo> {
    @Override
    public List<Class<?>> getValidationGroups(IntegralDetailVo object) {
        List<Class<?>> group = new ArrayList<>(2);
        group.add(IntegralDetailVo.class); // 必须添加,否则Default分组不会执行
        if(object != null) {
            // 根据不同的类型,返回不同的分组
            if(IntegralType.ADD.getCode().equals(object.getType())) {
                group.add(IntegralDetailValidateGroup.AddGroup.class);
            } else if(IntegralType.SUB.getCode().equals(object.getType())) {
                group.add(IntegralDetailValidateGroup.SubGroup.class);
            }
        }
        return group;
    }
}

2.4 在实体类中添加约束注解

package com.jingai.validation.vo;

import com.jingai.validation.custom.*;
import com.jingai.validation.groups.IntegralDetailValidateGroup;
import lombok.Data;
import org.hibernate.validator.group.GroupSequenceProvider;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;

// 添加分组序列
@GroupSequenceProvider(IntegralDetailGroupSequenceProvider.class)
@Data
public class IntegralDetailVo {

    private int id;

    @Min(value = 1, message = "会员id无效")
    private int memberId;

    @NotBlank(message = "积分类型不能为空")
    @InEnum(enumType = IntegralType.class, message = "积分类型")
    private String type;

    @NotBlank(message = "业务类型不能为空")
    /**
     * 此处使用不同的分组
     */
    @InEnum(enumType = IntegralAddType.class, groups = {IntegralDetailValidateGroup.AddGroup.class}, message = "业务类型")
    @InEnum(enumType = IntegralSubType.class, groups = {IntegralDetailValidateGroup.SubGroup.class}, message = "业务类型")
    private String busType;

    @Min(value = 1, message = "积分不能小于1")
    private int integral;

    private int afterIntegral;

}

2.5 参数校验

@RestController
public class IntegralController {

    @RequestMapping("changeintegral")
    public Map<String, Object> changeIntegral(@RequestBody @Validated IntegralDetailVo integralDetailVo) {
        // 省略业务代码
        return ResponseUtil.success();
    }

}

2.6 访问如上的接口,结果如下:

三、分组序列@GroupSequence

@GroupSequence注解用于控制的校验顺序。当需要指定校验的顺序时,就可以该注解。例如地址,要按省市区的顺序进行校验,则可以使用@GroupSequence。

3.1 添加实体类

package com.jingai.validation.vo;

import lombok.Data;

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

@Data
public class AddressVo {

    @NotBlank(message = "省份不能为空")
    private String province;

    @NotBlank(message = "市不能为空", groups = {Group1.class})
    private String city;

    @NotBlank(message = "区不能为空", groups = {Group2.class})
    private String area;

    @NotBlank(message = "详细地址不能为空", groups = {Group3.class})
    private String detail;

    public interface Group1 {};
    public interface Group2 {};
    public interface Group3 {};

    // 当分组选择Group时,会依此检查Default.class, Group1.class, Group2.class, Group3.class分组
    // 只要哪个分组校验失败了,就不会继续往下校验。和是否设置快速失败没有关系
    @GroupSequence({Default.class, Group1.class, Group2.class, Group3.class})
    public interface Group {}

}

3.2 校验

    @RequestMapping("address")
    public Map<String, Object> address(@RequestBody @Validated({AddressVo.Group.class}) AddressVo address) {
        // 省略其他代码
        return ResponseUtil.success();
    }

3.3 访问接口

在上面的代码中,只要省份不填写数据,那么每次都会提示”省份不能为空“。使用@GroupSequence注解时,按填写的组的顺序校验,只要校验不通过就不会往下校验,和是否设置快速失败没有关系。

结尾

以上为本次分享的高级篇之跨参数校验Cross-Parameter及分组序列校验@GroupSequenceProvider、@GroupSequence的全部内容。如果代码有不能理解的,建议可以看本人分享的《Spring validation参数校验系列》的前面几篇文章。限于篇幅,本篇就先分享到这里,希望对你有所帮助。

关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧。

  • 34
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值