SpringBoot - 自定义注解@Enum、@EnumValue校验前端参数是否为允许传入的值

时间:2025-04-02 08:59:42

文章目录

      • 1. 应用场景
      • 2. 回顾注解校验的使用
      • 3. 自定义注解校验整型枚举值
      • 4. 自定义注解校验各种类型枚举值
        • 4.1 使用示例1
        • 4.2 使用示例2
        • 4.3 使用示例3
        • 4.4 实现自定义注解
      • 5. 自定义注解校验整型和字符串类型枚举值

1. 应用场景

在实际工作的项目中,我们经常会遇到参数校验的问题,比如status字段我们只允许前端传给我们0,1,2这三种值,又比如timeUnit字段我们只允许前端传给我们m,h,d这三种值,这种场景经常遇到,平时做项目可能对参数校验没有太多要求,但是在实际的工作过程中,参数校验是非常重要的。我们今天想要讲的是如何对参数的枚举值进行校验,在此之前先来看一下注解校验的使用方法。

2. 回顾注解校验的使用

1、导入依赖

<dependency>
    <groupId></groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2、使用注解对前端传入的参数进行校验

@Data
public class DocAddReqVo {

    @NotBlank(message = "文档名称不能为空")
    @Pattern(regexp =  "^[^\"'&<>()+%\\\\]{0,95}$", message = "文档名称不能含有\"'&<>()+%这些特殊字符,且长度不能超过95个字符")
    private String name;

    @NotBlank(message = "分类id不能为空")
    private String categoryId;

    @NotEmpty(message = "文档标签不能为空")
    private Set<String> tags;

    @Length(max = 5000, message = "文本内容长度不能超过5000个字符")
    @NotBlank(message = "文档内容不能为空")
    private String content;

}

3、实体类上标记校验注解之后,一定要开启校验功能@Valid

@RestController
@Slf4j
@RequestMapping("/api/v1")
public class DocController implements CommonConstant {

    @Autowired
    private DocService docService;

    // 标注@Valid注解
    @PostMapping("/docs")
    public DocAddRespVo add(@RequestBody @Valid DocAddReqVo docAddReqVo) {
        Doc doc = docService.addDoc(new Doc(docAddReqVo));
        return new DocAddRespVo(doc.getId());
    }
}

整个注解校验的过程就是这样,当然还有其他很多注解和使用方法,这不是我们今天关注的重点,不再延伸。

3. 自定义注解校验整型枚举值

需求:假如有一个字段status用来标识商品的上下架,上架代表0,下架代表1,规定前端给我们传入的参数只能是0,1这两个值,而不能传入3,4,5等其他值,否则校验不通过,因此可以实现一个注解。

目标:实现注解 @AllowValue ,这个注解有一个属性vals,vals只能传入0,1

1、自定义一个注解@AllowValue

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 需要自定义一个校验器AllowConstraintValidator
@Constraint(validatedBy = { AllowConstraintValidator.class })
public @interface AllowValue {
    // 检验失败的提示信息
    String message() default "{}";

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

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

    // 注解的属性
    int[] vals() default {};
}

**2、自定义一个校验器AllowConstraintValidator **

public class AllowConstraintValidator implements ConstraintValidator<AllowValue, Integer> {
    // @AllowValue(vals = { 0, 1 }, message = "status只能为0和1"),将校验的枚举值放进set中
    Set<Integer> set = new HashSet<>();

    @Override
    public void initialize(AllowValue constraintAnnotation) {
        // 注解的属性
        int[] vals = constraintAnnotation.vals();
        for (int value : vals) {
            set.add(value);
        }
    }

    /**
     * 判断是否校验成功
     * @param value 要校验的值
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        // 如果set中包含要校验的值,说明校验通过,否则校验失败
        return value == null || set.contains(value);
    }
}

3、使用注解校验

@Data
public class AttachmentAddReqVo {

    @NotNull(message = "种类不能为空")
    // vals = { 0, 1 },代表只允许前端传入0和1这两个值
    @AllowValue(vals = { 0, 1 }, message = "status只能为0和1")
    private int status;
    
}

上面的自定义注解vals是整型数组,只能校验整型值,那如果我们既想校验整型值,又想校验字符串,怎么办呢?就需要更通用的注解校验

4. 自定义注解校验各种类型枚举值

假如有一个字段status用来标识商品的上下架,上架代表0,下架代表1

4.1 使用示例1

需求:规定前端给我们传入的参数只能是0、1这两个值,而不能传入其他值(null值也不允许),否则校验不通过

1、定义一个枚举类,枚举出来前端允许掺入的值:0和1

public enum  AllowIntValueEnum {
    /**
     * 商品上下架枚举类
     */
    UP(0),
    DOWN(1);

    private int value;

    AllowIntValueEnum(int value){
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

2、使用自定义注解@Enum校验:

@Data
public class DemoEntity {
    // clazz : 枚举类的名称
    // method :枚举类中获取要校验字段的方法名
    // allowNull : 是否允许null值
    // message:校验不通过时的提示信息
    @Enum(clazz = AllowIntValueEnum.class,method = "getValue",allowNull = false,message = "只允许传入枚举值0和1")
    private int status;
}
4.2 使用示例2

需求:规定前端给我们传入的参数只能是上架、下架这两个值,而不能传入其他值(null值允许),否则校验不通过

1、定义一个枚举类,枚举出来前端允许传入的值:上架和下架

public enum AllowStringValueEnum {
    /**
     * 商品上下架枚举类
     */
    UP("上架"),
    DOWN("下架");

    private String message;

    AllowStringValueEnum(String message){
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

2、使用自定义注解@Enum校验:

@Data
public class DemoEntity {
    // clazz : 枚举类的名称
    // method :枚举类中获取要校验字段的方法名
    // allowNull : 是否允许null值
    // message:校验不通过时的提示信息
    @Enum(clazz = AllowStringValueEnum.class,method = "getMessage",allowNull = true,message = "只允许传入枚举值上架和下架")
    private String name;
}
4.3 使用示例3

需求:规定前端给我们传入的参数只能是上架、下架这两个值,而不能传入其他值(null值不允许),否则校验不通过

1、定义一个枚举类,枚举出来前端允许传入的值:上架和下架

public enum AllowValueEnum {
    /**
     * 商品上下架枚举类
     */
     UP(0,"上架"),
     DOWN(1,"下架");

     private int value;
     private String message;
     AllowValueEnum(int value,String message){
         this.value = value;
         this.message = message;
     }

     public int getValue() {
        return value;
     }

     public String getMessage() {
        return message;
     }
}

2、使用自定义注解@Enum校验:

@Data
public class DemoEntity {
    // clazz : 枚举类的名称
    // method :枚举类中获取要校验字段的方法名
    // allowNull : 是否允许null值
    // message:校验不通过时的提示信息
    @Enum(clazz = AllowValueEnum.class,method = "getMessage",allowNull = false,message = "只允许传入枚举值上架和下架")
    private String status;
}

怎么样,看了上面的校验方式,是不是感觉很方便?那么这个注解是如何实现的呢?一起看看吧

4.4 实现自定义注解

1、自定义一个注解 @Enum

@Documented
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Enum.List.class)
@Constraint(validatedBy = { EnumValidator.class })
public @interface Enum {

    String message() default "{*.}";

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

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

    /**
     * 要校验的枚举类
     *
     * @return 枚举类
     */
    Class<?> clazz();

    /**
     * 要校验枚举类的哪个字段,这个字段通过getxxx()方法获取,返回方法的名称
     *
     * @return 方法的名称
     */
    String method() default "ordinal";

    /**
     * 是否允许null值,默认是允许
     */
    boolean allowNull() default true;
    
    @Documented
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface List {
        Enum[] value();
    }
}

2、自定义一个注解校验器 EnumValidator

public class EnumValidator implements ConstraintValidator<Enum,Object> {

    private Enum annotation;

    @Override
    public void initialize(Enum constraintAnnotation) {
        this.annotation = constraintAnnotation;
    }

    /**
     * 判断是否校验成功
     * @param value 待校验的值
     * @return 校验结果
     */
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // 如果待校验的值为null,是否校验通过
        if (value == null) {
            return annotation.allowNull();
        }
        // 返回枚举常量的数组:UP,DOWN
        Object[] objects = annotation.clazz().getEnumConstants();
        try {
            // 获取枚举类中获取校验字段的方法名
            Method method = annotation.clazz().getMethod(annotation.method());
            for (Object o : objects) {
                // 将方法执行的结果(o)和待校验的值value进行比较,如果相同,说明校验成功
               if (value.equals(method.invoke(o))) {
                    return true;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // 如果value没有在枚举值中,校验失败
        return false;
    }
}

5. 自定义注解校验整型和字符串类型枚举值

如果不想写枚举类,又想校验字符串类型枚举值,也是可以的。

1、自定义注解

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { EnumValueValidator.class })
public @interface EnumValue {
    // 默认错误消息
    String message() default "必须为指定值";

    // 字符串类型
    String[] strValues() default {};

    // 整型
    int[] intValues() default {};

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

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

    // 指定多个时使用
    @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        EnumValue[] value();
    }
}

2、自定义校验器

public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {

    private String[] strValues;

    private int[] intValues;

    @Override
    public void initialize(EnumValue constraintAnnotation) {
        strValues = constraintAnnotation.strValues();
        intValues = constraintAnnotation.intValues();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        if (Objects.isNull(value)) {
            return true;
        }
        if (value instanceof String) {
            for (String s : strValues) {
                if (s.equals(value)) {
                    return true;
                }
            }
        } else if (value instanceof Integer) {
            for (Integer s : intValues) {
                if (s == value) {
                    return true;
                }
            }
        }
        return false;
    }
}

3、使用自定义注解校验

@Data
public class DemoEntity {
    @EnumValue(strValues = { "上架", "下架"},message = "只允许传入枚举值上架和下架")
    private String name;
}