1. 使用场景
某接口的入参某属性,只希望为系统中某枚举的中定义过的值。
例如:用户类型枚举中定义 10-普通用户 20-管理员 30-游客 40-中级用户 50-高级用户
希望某接口的入参 userType 只能填入这几种type
支持扩展场景:
- 只允许填入枚举中定义过的某些值
- 不允许填入某些值
2. 技术实现
2.1 实现思路
使用Hibernate Validator
校验工具,自定义校验注解及其校验逻辑。
Hibernate Validator
官方文档:
https://docs.jboss.org/hibernate/validator/7.0/reference/en-US/html_single/#validator-gettingstarted
2.2 代码实现
2.2.1 引入依赖
<!--springboot2.3.3版本后 参数校验需加上 begin-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--springboot2.3.3版本后 参数校验需加上 end-->
2.2.2 自定义校验注解
/**
* 校验枚举类值注解
*
* @author Gangbb
* @date 2021/11/6
**/
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RUNTIME)
@Repeatable(VerifyEnum.List.class)
@Constraint(validatedBy = {EnumValidator.class})
public @interface VerifyEnum {
/**
* 枚举类型
*/
Class<? extends Enum<?>> enumClass();
/**
* 枚举中用于校验的属性值
**/
String keyColumn();
/**
* 只允许填的枚举code值(填了非枚举code值会报错)
*/
String[] allowedValues() default { };
/**
* 不允许填的枚举code值
*/
String[] notAllowedValues() default { };
/**
* 错误消息
**/
String message() default "";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default {};
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface List {
VerifyEnum[] value();
}
}
2.2.3 校验工具类
/**
* hibernate Validator校验工具类
*
* @author Gangbb
* @date 2022/1/29
**/
public class ValidatorUtils {
/**
* 验证器工程
*/
private static ValidatorFactory factory;
/**
* 对象验证器
*/
public static Validator validator;
/**
* 方法验证器
*/
public static ExecutableValidator executableValidator;
static {
initValidator();
clean();
}
/**
* @Author Gangbb
* @Description 初始化ValidatorFactory和Validator
* @Date 2021/9/21
**/
public static void initValidator() {
factory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
validator = factory.getValidator();
}
/**
* @Author Gangbb
* @Description 初始化ValidatorFactory和ExecutableValidator
* @Date 2021/9/21
**/
public static void initExecutableValidator() {
factory = Validation.buildDefaultValidatorFactory();
executableValidator = factory.getValidator().forExecutables();
}
/**
* @Author Gangbb
* @Description 关闭ValidatorFactory工厂
* @Date 2021/9/21
**/
public static void clean() {
factory.close();
}
/**
* @Author Gangbb
* @Description 对类中的某方法参数校验
* @Date 2021/9/21
**/
public static<T> Set<ConstraintViolation<T>> validMethod(T t, Method method, Object[] parameterValues){
return executableValidator.validateParameters(t, method, parameterValues);
}
/**
* 校验对象
*
* @Param [object:待检验对象, groups:对象分组]
* @return void
* @Author Gangbb
* @Date 2021/10/25
**/
public static void validateObject(Object object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if(CollectionUtil.isNotEmpty(constraintViolations)){
String errorMsg = StrUtil.format("对象{}校验异常:{}", object.getClass().getSimpleName(), getErrorMsg(constraintViolations));
throw new ApiException(ResultEnum.PARAMETER_VERIFICATION_FAIL.getCode(), errorMsg);
}
}
/**
* 校验对象列表
*
* @param objectList 待校验对象列表
* @param groups 校验分组
* @date 2022/1/8
**/
public static void validateObjectList(List<Object> objectList, Class<?>... groups) {
for (Object o : objectList) {
validateObject(o, groups);
}
}
/**
* 校验填入值是否合法
* 目前用于:DictValidator、EnumValidator
*
* @param allowedValues 允许填入值数组(在codeValues再)
* @param notAllowedValues 不允许填入值数组
* @param value 当前填入值须校验的值
* @return String
* @author Gangbb
* @date 2022/01/29
**/
public static String validateValues(String[] allowedValues, String[] notAllowedValues, Object value) {
// notAllowedValues存在情况
if (notAllowedValues != null && notAllowedValues.length > 0) {
List<String> notAllowedList = Arrays.asList(notAllowedValues);
if (notAllowedList.contains(String.valueOf(value))) {
return StrUtil.format("不能填写以下值{}", notAllowedList);
}
}
// allowedValues存在情况
if (allowedValues != null && allowedValues.length > 0) {
List<String> allowedList = Arrays.asList(allowedValues);
if (!allowedList.contains(String.valueOf(value))) {
return StrUtil.format("可填值只能为{}", Arrays.toString(allowedValues));
}
}
return "";
}
/**
* 校验填入值是否合法
* 目前用于:DictValidator、EnumValidator
*
* @param allowedValues 允许填入值数组(在codeValues再)
* @param notAllowedValues 不允许填入值数组
* @param value 当前填入值须校验的值
* @param codeValues 默认可填值
* @return String
* @author Gangbb
* @date 2021/11/18
**/
public static String validateValues(String[] allowedValues, String[] notAllowedValues,
Object value, List<Object> codeValues) {
// notAllowedValues存在情况
if(notAllowedValues != null && notAllowedValues.length > 0){
List<String> notAllowedList = Arrays.asList(notAllowedValues);
codeValues.removeAll(notAllowedList);
if(notAllowedList.contains(String.valueOf(value))){
return StrUtil.format("不能填写以下值{}", notAllowedList);
}
}
// allowedValues存在情况
if(allowedValues != null && allowedValues.length > 0){
List<String> allowedList = Arrays.asList(allowedValues);
// 将codeValues统一转成String
List<String> stringCodeValues = codeValues.stream().map(String::valueOf).collect(Collectors.toList());
if(!stringCodeValues.containsAll(allowedList)){
// @VerifyEnum填入allowedValues存在非枚举code值
throw new RuntimeException("填入allowedValues存在非允许值");
}else{
if(allowedList.contains(String.valueOf(value))){
return "";
}else{
return StrUtil.format("可填值只能为{}", Arrays.toString(allowedValues));
}
}
}
// 校验字段值是否是字典允许数据
if(codeValues.contains(value)){
return "";
}else{
// 重置错误提示
return StrUtil.format("可填值只能为{}", codeValues);
}
}
/**
* 获取校验错误消息
* @param c
* @return
*/
public static String getErrorMsg(Set<ConstraintViolation<Object>> c){
StringBuffer msg = new StringBuffer();
if (CollectionUtil.isNotEmpty(c)){
for (ConstraintViolation<Object> constraintViolation : c) {
String itemMessage = constraintViolation.getMessage();
String itemName = constraintViolation.getPropertyPath().toString();
msg.append("字段<").append(itemName).append(">--").append(itemMessage).append("。");
}
}
return msg.toString();
}
/**
* 拼装单个对象校验信息
*
* @Param [s, c]
* @return void
* @Author Gangbb
* @Date 2021/10/25
**/
public static void getOneInfo(StringBuffer s, Set<ConstraintViolation<Object>> c){
if (CollectionUtil.isNotEmpty(c)){
s.append("{ ");
for (ConstraintViolation<Object> constraintViolation : c) {
String itemMessage = constraintViolation.getMessage();
String itemName = constraintViolation.getPropertyPath().toString();
s.append("字段<" + itemName + "> :" + itemMessage + "。");
}
s.append(" }");
}
}
/**
* 拼装多个对象校验信息
*
* @Param [s, collect]
* @return void
* @Author Gangbb
* @Date 2021/10/25
**/
public static void getListInfo(StringBuffer s, List<Set<ConstraintViolation<Object>>> collect){
for (int i = 0; i < collect.size(); i++) {
Set<ConstraintViolation<Object>> constraintViolations = collect.get(i);
if (CollectionUtil.isNotEmpty(constraintViolations)){
s.append("[ " + "列表第["+ i + "]项校验不通过:");
getOneInfo(s, constraintViolations);
s.append(" ]");
}
}
}
/**
* 注解校验,获取处理校验结果
*
* @param errorMsg 错误信息
* @param context 校验上下文
* @return boolean
* @author Gangbb
* @date 2021/10/29
**/
public static boolean getResult(String errorMsg, ConstraintValidatorContext context){
if(StrUtil.isNotBlank(errorMsg)){
// 重置错误提示
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(errorMsg)
.addConstraintViolation();
return false;
}
return true;
}
}
2.2.4 校验处理逻辑
/**
* 注解@VerifyEnum 处理逻辑方法
*
* @author Gangbb
* @date 2022/1/29
**/
public class EnumValidator implements ConstraintValidator<VerifyEnum, Object> {
private Class<? extends Enum<?>> enumClass;
private String[] allowedValues;
private String[] notAllowedValues;
private String keyColumn;
@Override
public void initialize(VerifyEnum constraintAnnotation) {
enumClass = constraintAnnotation.enumClass();
allowedValues = constraintAnnotation.allowedValues();
notAllowedValues = constraintAnnotation.notAllowedValues();
keyColumn = constraintAnnotation.keyColumn();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if(value != null){
// 校验enumClass中是否存在keyColumn字段
if(!ReflectUtil.hasField(enumClass, keyColumn)){
throw new RuntimeException(StrUtil.format("EnumValidator:<{}>中不存在填入的<{}>字段", enumClass.getSimpleName(), keyColumn));
}
// 获取所有该枚举的指定字段的值列表
List<Object> codeValues = EnumUtil.getFieldValues(enumClass, keyColumn);
String validateValueMsg = ValidatorUtils.validateValues(allowedValues, notAllowedValues, value, codeValues);
if(StrUtil.isNotBlank(validateValueMsg)){
return ValidatorUtils.getResult(validateValueMsg, context);
}else{
return true;
}
}
return true;
}
}
3. 使用示例
枚举类
/**
* 组织架构类别枚举
*
* @author Gangbb
* @date 2022/1/4
**/
public enum SysDeptTypeEnum {
SCHOOL(1, "学校"),
INVALID(2, "单位/学院"),
DEPARTMENT(3, "部门"),
MAJOR(4, "专业"),
;
private final int code;
private final String name;
SysDeptTypeEnum(int code, String info) {
this.code = code;
this.name = info;
}
public static String getValue(int code) {
SysDeptTypeEnum[] enums = values();
for (SysDeptTypeEnum item : enums) {
if (item.code == code) {
return item.getName();
}
}
return null;
}
public int getCode() {
return code;
}
public String getName() {
return name;
}
}
DTO请求参数实体类
@Data
public class SysDeptDto {
/**
* 组织架构d
*/
@NotNull(message = "组织架构Id不能为空", groups = { EditGroup.class })
private Long deptId;
/**
* 组织架构类别(字典类别:sys_dept_type)
*/
@NotNull(message = "组织架构类别不能为空", groups = { AddGroup.class, EditGroup.class })
@VerifyEnum(enumClass = SysDeptTypeEnum.class, keyColumn = "code", groups = { AddGroup.class, EditGroup.class })
private Integer type;
// 其他属性省略...
}
Controller接口
/**
* 新增组织架构
*
* @param dto 组织架构新增/修改 请求参数Dto
* @return ApiResult<Void>
* @date 2022-01-05
**/
@PostMapping()
public ApiResult<Void> add(@Validated(AddGroup.class) @RequestBody SysDeptDto dto){
return toApiRes(sysDeptService.insertByDto(dto));
}
请求测试
稍微修改下注解代码
@VerifyEnum(enumClass = SysDeptTypeEnum.class, keyColumn = "code", notAllowedValues = {"5"}, groups = { AddGroup.class, EditGroup.class })
测试: