在上一篇文章 Springboot实现优雅的参数校验(Spring Validation)和 if else说再见,我们介绍了 Spring Validation 的初级用法,在实际开发中,无论是 Bean Validation 定义的约束,还是 Hibernate Validator 附加的约束,都是无法满足我们复杂的业务场景。所以,我们需要自定义约束。开发自定义约束一共只要两步:
下面,就让我们一起来实现一个自定义约束,用于校验参数必须在枚举值的范围内吧。
比如一个很常见的用户注册的场景,一般都要限定用户性别,这里我们不考虑 同性 未知的情况哈,只考虑 男和女 这种情况。0:代表女性,1:代表男性,
为了方便我们后面获取枚举类型值的,我先定义一个接口 GenderArrayValuable,这里的定义了一个 返回一个int[] 数据的方法。后面我们会在 GenderEnum 中实现这个接口。
package com.ratel.validation.core.validator;
/**
* 可生成 Int 数组的接口
*/
public interface GenderArrayValuable {
/**
* @return int 数组
*/
int[] array();
}
GenderEnum 枚举类实现了 GenderArrayValuable 的 IntArrayValuable 接口,返回值数组 ARRAYS。
import com.ratel.validation.core.validator.GenderArrayValuable;
import java.util.Arrays;
public enum GenderEnum implements GenderArrayValuable {
MALE(1, "男"),
FEMALE(0, "女");
/**
* 值数组
*/
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(GenderEnum::getValue).toArray();
/**
* 性别值
*/
private final Integer value;
/**
* 性别名
*/
private final String name;
GenderEnum(Integer value, String name) {
this.value = value;
this.name = name;
}
public Integer getValue() {
return value;
}
public String getName() {
return name;
}
@Override
public int[] array() {
return ARRAYS;
}
}
package com.ratel.validation.core.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = GenderValidator.class)
public @interface GenderCheck {
/**
* @return 实现 IntArrayValuable 接口的
*/
Class<? extends GenderArrayValuable> value();
/**
* @return 提示内容
*/
String message() default "必须在指定范围 {value}";
/**
* @return 分组
*/
Class<?>[] groups() default {};
/**
* @return Payload 数组
*/
Class<? extends Payload>[] payload() default {};
/**
* Defines several {@code @GenderCheck} constraints on the same element.
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
GenderCheck[] value();
}
}
package com.ratel.validation.core.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
public class GenderValidator implements ConstraintValidator<GenderCheck, Integer> {
/**
* 值数组
*/
private Set<Integer> values;
@Override
public void initialize(GenderCheck annotation) {
GenderArrayValuable[] values = annotation.value().getEnumConstants();
if (values.length == 0) {
this.values = Collections.emptySet();
} else {
this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toSet());
}
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
// 1 校验通过
if (values.contains(value)) {
return true;
}
// 2 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
context.disableDefaultConstraintViolation(); // 3 禁用默认的 message 的值
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
.replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 4. 重新添加错误提示语句
return false; // 5
}
}
实现 ConstraintValidator 接口。
实现 initialize(A constraintAnnotation)
方法,获得 @GenderEnum 注解的 values() 属性,获得值数组,设置到 values 属性种。
实现 boolean isValid(T var1, ConstraintValidatorContext var2);
方法,实现校验参数值,是否在 values 范围内。
在注释 1 处,校验参数值在范围内,直接返回 true ,校验通过。
在注释 2 处,校验不通过,自定义提示语句。
在注释 5 处,校验不通过,所以返回 false 。
至此,我们已经完成了自定义约束的实现。
定一个 UserUpdateGenderDTO 实体类,在 gender 字段上添加自定义的 @GenderCheck(value = GenderEnum.class, message = "性别必须是 {value}")
注解,限制传入的参数值,必须在 GenderEnum 枚举范围内。
package com.ratel.validation.entity;
import com.ratel.validation.core.validator.GenderCheck;
import com.ratel.validation.enums.GenderEnum;
import javax.validation.constraints.NotNull;
/**
* 用户更新性别 DTO
*/
public class UserUpdateGenderDTO {
/**
* 用户编号
*/
@NotNull(message = "用户编号不能为空")
private Integer id;
/**
* 性别
*/
@NotNull(message = "性别不能为空")
@GenderCheck(value = GenderEnum.class, message = "性别必须是 {value}")
private Integer gender;
public Integer getId() {
return id;
}
public UserUpdateGenderDTO setId(Integer id) {
this.id = id;
return this;
}
public Integer getGender() {
return gender;
}
public UserUpdateGenderDTO setGender(Integer gender) {
this.gender = gender;
return this;
}
@Override
public String toString() {
return "UserUpdateGenderDTO{" +
"id=" + id +
", gender=" + gender +
'}';
}
}
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
@RestController
@RequestMapping("/users")
@Validated
@Api(tags = "用户")
public class UserController {
private Logger logger = LoggerFactory.getLogger(getClass());
@PostMapping("/update_gender")
public String updateGender(@Valid UserUpdateGenderDTO updateGenderDTO) {
logger.info("[updateGender][updateGenderDTO: {}]", updateGenderDTO);
return "性别更新成功";
}
}
我们这里使用 knife4j 接口文档,当我们 把 gender的值传成 非 【0 ,1 】 以外的值,就会直接返回错误信息,"请求参数不合法:性别必须是 [0, 1]",
希望阅读完本文,能够让各位 c友 更加舒适且优雅的完成各种需要参数校验的地方。 不说了,赶紧给自己的系统去把参数校验给补全,嘿嘿。
当然,有一点要注意,Bean Validation 更多做的是,无状态的参数校验。怎么理解呢?
例如说,参数的大小长度,判断参数是否为空,是否是身份证号,是否是邮箱,是否是手机号 这些不依赖 外部数据源的等等,是适合通过 Spring Validation
中完成。
例如说,校验用户名,邮箱,手机号 唯一等等,依赖 外部数据源 的,是不适合通过 Spring Validation
中完成。