1. 导读

在springboot 中,校验对象的参数值时,如果某个参数的值是固定的几个,那么可以使下面的枚举校验器 或 固定值校验证器。这两种验证器目录适用于参数值是数字、字符串,也可以进行拓展。下面举例介绍的 自定义枚举校验 和 固定值校验证的适用场景基本一样。

2. 业务说明

2.1 业务场景一

邮寄状态 postStatus 的取值限定在范围是 0、1、2、3 。其中:

  • 0 :表示 未邮寄
  • 1 :表示 已邮寄
  • 2 :表示 收到货
  • 3 :表示 邮寄出错

2.2 业务场景二

证件类型 cardType 的取值 限定范围是 A、B、C、D、E、F 。其中:

  • A :表示 身份证
  • B :表示 护照
  • C :表示 军官
  • D :表示 台湾居民来往大陆通行证
  • E :表示 港澳居民来往内地通行证
  • F :表示 外国人永久居留

2.3 解决思路

有两种方式可以实现:

  • 使用validator的@Pattern注解

业务场景一此时需要将postStatus的类型改为String类型

@Pattern(regexp = "^[0-3]$", message = "post status must be 0、1、2、3")

业务场景二

@Pattern(regexp = "^[A-F]$", message = "card type must be A、B、C、D、E、F")
  • 使用自定义枚举校验器

3. 使用自定义枚举校验器

3.1 定义通用的枚举校验器 EnumValueValidator

这个枚举校验器是通用的,正常情况下,一个项目中只要写这一枚举校验器即可。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.Validator.class)
/**
 * 枚举校验器
 */
public @interface EnumValidator {

 String message() default "{ EnumValidator's value is invalid }";

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

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

 Class<? extends EnumValidations.Enumerable> enumClass();

 class Validator implements ConstraintValidator<EnumValidator, Object> {

  private Class<? extends EnumValidations.Enumerable> enumClass=null;
  
  @Override
  public void initialize(EnumValidator enumValue) {
   enumClass = enumValue.enumClass();
  }

  @Override
  public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
   if (value == null) {
    return Boolean.FALSE;
   }
   try {
    EnumValidations.Enumerable result = off(enumClass,String.valueOf(value));
    return result == null ? false : true;
   } catch (Exception e) {
    e.printStackTrace();
    return Boolean.FALSE;
   }
  }

  public static <E extends EnumValidations.Enumerable> E off(@Nonnull Class<E> classType, String value) {
   for (E enumConstant : classType.getEnumConstants()) {
    if (enumConstant.getCode().equals(value)) {
     return enumConstant;
    }
   }
   return null;
  }
 }
}

3.2 定义枚举值

定义枚举类:

// 所有的枚举校验都在此文件中
public class EnumValidations {

 public interface  Enumerable{
  public String getCode();
  public String getText() ;
 }
 
 /**
  *  邮寄状态
  */
 public enum PostStatusEnum  implements Enumerable  {  
  //0:未邮寄, 1:已邮寄,2:收到货,3:邮寄出错
  A("0", "未邮寄"),
  B("1", "已邮寄"),
  C("2", "收到货"),
  D("3", "邮寄出错");
 
  private String code; 
  private String desc; 
 
  private PostStatusEnum(String code, String desc) {
   this.code = code;
   this.desc = desc;
  } 
 
  public String getCode() {
   return code;
  } 
 
  public String getDesc() {
   return desc;
  } 
 }
 
 /**
  *用户信息 —— 证件类型  CardType
  */
 public enum UserInfoCardTypeEnum   implements Enumerable  { 
  
  A("A", "身份证"),
  B("B", "护照"),
  C("C", "军官"),
  D("D", "台湾居民来往大陆通行证"),
  E("E", "港澳居民来往内地通行证"),
  F("F", "外国人永久居留") ;
 
  private String code;
  private String desc;
  
  private UserInfoCardTypeEnum(String code, String desc) {
   this.code = code;
   this.desc = desc;
  } 
 
  public String getCode() {
   return code;
  } 
  public String getDesc() {
   return desc;
  }
 }
}

3.3 使用校验器

public class UpdateOrderStatusVO implements Serializable { 
 
    /**
     * 邮寄状态:0:未邮寄, 1:已邮寄,2:收到货,3:邮寄出错
     */
    @NotNull(message="邮寄状态不能为空")
    @EnumValidator(enumClass = EnumValidations.PostStatusEnum.class,message="邮寄状态取值不正确")
    private String postStatus;

    /**证件类型 */
    @NotNull(message="证件类型不能为空")
    @EnumValidator(enumClass = EnumValidations.UserInfoCardTypeEnum.class,message="证件类型取值不正确")
    private String  cardType;

   // getter/setter
}

使用枚举校验会出现大量的重复代码,比如 属性、构造函数、 getter/setter 等。下面推荐一种更优化方法。

4. 使用 固定值校验证器(推荐)

4.1 定义 固定值校验证器 FixedValueValidator

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

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

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {FixedValueValidator.FixedValueValid.class})

public @interface FixedValueValidator {

    String message() default "FixedValue's value is invalid";

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

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

    String[] fixedValue();

    class FixedValueValid implements ConstraintValidator<FixedValueValidator, Object> {

        String[] fixedValue = null;

        public void initialize(FixedValueValidator validData) {
            this.fixedValue = validData.fixedValue();
        }

        public boolean isValid(Object value, ConstraintValidatorContext constraintContext) {
            if (fixedValue == null || fixedValue.length == 0 ) {
                return false;
            }

            if (value == null) {
                return true;
            }

            boolean flag = false;
            for (String str : fixedValue) {
                if (String.valueOf(value).equals(String.valueOf(str))) {
                    flag = true;
                    break;
                }
            }
            return flag;
        }
    }
}

4.2 使用 固定值校验证

public class UpdateOrderStatusVO implements Serializable { 
 
    /** 邮寄状态 */
    @NotNull(message="邮寄状态不能为空")
    @FixedValueValidator(fixedValue ={"0", "1", "2", "3" }, message="邮寄状态值错误" )
    private String postStatus;

    /** 证件类型 */
    @NotNull(message="证件类型不能为空")
    @FixedValueValidator(fixedValue ={"A", "B", "C", "D", "E", "F"}, message="证件类型值错误")
    private String  cardType;

   // getter/setter   
}

5. 捕获错误

方法1:通过 BindingResult 捕获错误

@RequestMapping("/order/updateStatus")
public void save(@Validated UpdateOrderStatusVO vo, BindingResult bindingResult,@Validated Student student, BindingResult bindingResult2) {
    if (bindingResult.hasErrors()) {
        for (ObjectError error : bindingResult.getAllErrors()) {
            System.out.println(error.getDefaultMessage());
        }
    }

    if (bindingResult2.hasErrors()) {
        for (ObjectError error : bindingResult2.getAllErrors()) {
            System.out.println(error.getDefaultMessage());
        }

    }
}

方法2:通过 ConstraintViolationException 捕获错误(推荐)

@RequestMapping("/order/updateStatus")
public void save(@Validated UpdateOrderStatusVO vo,@Validated Student student) {
   // 。。。。
}

统一的异常的捕获类:

@ControllerAdvice
@Component
public class GlobalExceptionHandler {

 /**
  * 拦截捕捉 MissingServletRequestParameterException 异常
  */
 @ResponseBody
 @ExceptionHandler(value = MissingServletRequestParameterException.class)
 public String doMissingServletRequestParameterHandler(MissingServletRequestParameterException exception) {
  MissingServletRequestParameterException exs = (MissingServletRequestParameterException) exception;
  System.err.println(exs.getMessage());
  return "bad request, ";
 }

 /**
  * 拦截捕捉 ConstraintViolationException 异常
  */
 @ResponseBody
 @ExceptionHandler(value = ConstraintViolationException.class)
 public Map ConstraintViolationExceptionHandler(ConstraintViolationException ex) {
  Map map = new HashMap();
  Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
  Iterator<ConstraintViolation<?>> iterator = constraintViolations.iterator();
  List<String> msgList = new ArrayList<>();
  while (iterator.hasNext()) {
   ConstraintViolation<?> cvl = iterator.next();
   System.err.println(cvl.getMessageTemplate());
   msgList.add(cvl.getMessageTemplate());
  }
  map.put("status", 500);
  map.put("msg", msgList);
  return map;
 }
}

6. 总结

通过对两个校验器的使用、比较,发现 固定值校验器 使用更加的简单,不需要创建类、属性、构造方法和 getter/setter 等。