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参数校验系列》的前面几篇文章。限于篇幅,本篇就先分享到这里,希望对你有所帮助。
关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧。