通过实现ConstraintValidator完成自定义校验注解

一、Spring中的校验注解

在Spring的使用过程中,有一些现成的注解可以使用

  • @AssertFalse:该值必须为False
  • @AssertTrue:该值必须为True
  • @DecimalMax(value,inclusive):被注释的元素必须是一个数字,其值必须小于等于指定的最大值 ,inclusive表示是否包含该值
  • @DecimalMin(value,inclusive):被注释的元素必须是一个数字,其值必须大于等于指定的最小值 ,inclusive表示是否包含该值
  • @Digits:限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
  • @Email:该值必须为邮箱格式
  • @Future:被注释的元素必须是一个将来的日期
  • @FutureOrPresent:被注释的元素必须是一个现在或将来的日期
  • @Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Negative:该值必须小于0
  • @NegativeOrZero:该值必须小于等于0
  • @NotBlank:该值不为空字符串,例如“ ”
  • @NotEmpty:该值不为空字符串,例如”“
  • @NotNull:该值不为Null
  • @Null:该值必须为Null
  • @Past:被注释的元素必须是一个过去的日期
  • @PastOrPresent:被注释的元素必须是一个过去或现在的日期
  • @Pattern(regexp):匹配正则
  • @Positive:该值必须大于0
  • @PositiveOrZero:该值必须大于等于0
  • @Size(min,max):数组大小必须在[min,max]这个区间

二、自定义注解

2.1 自定义注解类
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
 * @author Alan Chen
 * @description 手机号码校验注解
 * @date 2023/04/27
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {MobileValidator.class})
public @interface Mobile {

    boolean required() default true;

    String message() default "参数不正确";

    String regExp() default MobileRegExp.MOBILE_REG_EXP;

    Class[] groups() default {};

    Class[] payload() default {};
}

该自定义注解类中用到了四种元注解,最后一个注解@Constraint表示校验此注解的校验器类,可以多个。值得一提的是除了自定义的message、require属性外,下面的groups和payload也是必须添加的。

2.2 手机号码校验正则表达式
/**
 * @author Alan Chen
 * @description 手机号码校验正则表达式
 * @date 2023/04/27
 */
public class MobileRegExp {

    /**
     * 中国大陆、澳门、香港和台湾:
     * ^ 表示匹配字符串的开始位置。
     * 1 表示手机号码开头必须是数字 1(适用于中国大陆)。
     * [3-9] 表示第二个数字必须是 3、4、5、6、7、8、9 中的任意一个(适用于中国大陆)。
     * [5689] 表示手机号码开头必须是数字 5、6、8、9 中的任意一个(适用于澳门和香港)。
     * 09 表示手机号码开头必须是数字 09(适用于台湾)。
     * \d 表示任意数字。
     * {7} 或 {8} 或 {9} 表示前面的数字必须重复出现 7 次(适用于澳门和香港)或 8 次(适用于台湾)或 9 次(适用于中国大陆)。
     * | 表示逻辑或。
     * () 表示分组,用于将三个表达式组合在一起。
     * $ 表示匹配字符串的结束位置。
     */
    public final static String MOBILE_REG_EXP = "^(1[3-9]\\d{9}|[5689]\\d{7}|09\\d{8})$";

    /**
     * 中国-大陆:
     * ^ 表示匹配字符串的开始位置。
     * 1 表示手机号码开头必须是数字 1。
     * [3-9] 表示第二个数字必须是 3、4、5、6、7、8、9 中的任意一个。
     * \d 表示任意数字。
     * {9}表示前面的数字必须出现9次。
     * $ 表示匹配字符串的结束位置。
     */
    public final static String MOBILE_REG_EXP_ZH_CN = "^1[3-9]\\d{9}$";

    /**
     * 中国-澳门:
     * 澳门手机号码格式为8位数字,以6开头
     * ^ 表示匹配字符串的开始位置。
     * 6 表示手机号码开头必须是数字 6。
     * \d 表示任意数字。
     * {7} 表示前面的数字必须重复出现 7 次。
     * $ 表示匹配字符串的结束位置。
     */
    public final static String MOBILE_REG_EXP_ZH_MO = "^6\\d{7}$";

    /**
     * 中国-香港:
     * 香港手机号码格式为8位数字,以5、6、8、9开头
     * ^ 表示匹配字符串的开始位置。
     * [5689] 表示手机号码开头必须是数字 5、6、8、9 中的任意一个。
     * \d 表示任意数字。
     * {7} 表示前面的数字必须重复出现 7 次。
     * $ 表示匹配字符串的结束位置。
     */
    public final static String MOBILE_REG_EXP_ZH_HK = "^[5689]\\d{7}$";

    /**
     * 中国-台湾:
     * 台湾地区的手机号码开头一般是09,接下来是八位数字
     * ^ 表示匹配字符串的开始位置。
     * 09 表示手机号码开头必须是数字 09。
     * \d 表示任意数字。
     * {8} 表示前面的数字必须重复出现 8 次。
     * $ 表示匹配字符串的结束位置。
     */
    public final static String MOBILE_REG_EXP_ZH_TW = "^09\\d{8}$";
}
2.3 手机号码校验器
import org.apache.commons.lang3.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;

/**
 * @author Alan Chen
 * @description 手机号码校验器
 * @date 2023/04/27
 */
public class MobileValidator implements ConstraintValidator {

    private boolean require = false;

    private String regExp;

    @Override
    public void initialize(Mobile mobile) {
        require = mobile.required();
        regExp = mobile.regExp();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if (require == false) {
            return true;
        }
        return regExpMatch(value);
    }

    private boolean regExpMatch(String value) {
        if (StringUtils.isEmpty(value)) {
            return false;
        }
        return Pattern.compile(regExp).matcher(value).matches();
    }
}

校验类需要实现ConstraintValidator接口。接口使用了泛型,需要指定两个参数,第一个自定义注解类,第二个为需要校验的数据类型。实现接口后要override两个方法,分别为initialize方法和isValid方法。其中initialize为初始化方法,可以在里面做一些初始化操作,isValid方法就是我们最终需要的校验方法了。可以在该方法中实现具体的校验步骤。

2.4 group分组接口实现类

我们可能会将PersonEditVO对象用在不同的接口中接收参数,比如在新增和修改接口中。在新增接口中,需要校验mobile,在修改接口中不需要校验mobile。那注解中的groups字段就派上用场了。groups和@Validated配合能控制哪些注解需不需要开启校验。

我们首先定义4个groups分组接口AddAction、EditAction、UpdateAction、DeleteAction,并且继承Default接口。当然也可以不继承Default接口,因为使用注解时不显示指定groups的值,则默认为groups = {Default.class}。所以继承了Default接口,在用@Validated(AddAction.class)时,也会校验groups = {Default.class}的注解。

import javax.validation.groups.Default;

public interface AddAction extends Default {
}


public interface EditAction extends Default {
}

public interface UpdateAction extends Default {
}

public interface DeleteAction extends Default {
}

2.5 VO参数类
import com.ac.core.validation.action.AddAction;
import com.ac.core.validation.action.EditAction;
import com.ac.core.validation.validator.idcard.IdNo;
import com.ac.core.validation.validator.mobile.Mobile;
import com.ac.core.validation.validator.mobile.MobileRegExp;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class PersonEditVO {

    @NotNull(message = "ID不能为空", groups = EditAction.class)
    @ApiModelProperty("ID")
    private Long id;

    @NotBlank(message = "用户姓名不能为空", groups = AddAction.class)
    @Length(max = 5, message = "姓名最长5个字")
    @ApiModelProperty("用户姓名")
    private String memberName;

    @NotBlank(message = "手机号不能为空", groups = AddAction.class)
    @Mobile(message = "手机号格式不正确", regExp = MobileRegExp.MOBILE_REG_EXP_ZH_CN, groups = {AddAction.class, EditAction.class})
    @ApiModelProperty("手机号")
    private String mobile;

    @NotBlank(message = "证件号不能为空")
    @IdNo(message = "证件号格式不正确")
    @ApiModelProperty("证件号(身份证/港澳通行证/台湾通行证/护照)")
    private String idNo;
}
2.6 controller接口
import com.ac.core.validation.action.AddAction;
import com.ac.core.validation.action.EditAction;
import com.ac.member.vo.PersonEditVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Slf4j
@Api(tags = "Validation校验测试")
@RestController
@RequestMapping("validation")
public class ValidationController {

    @ApiOperation(value = "新增")
    @PostMapping
    public boolean add(@RequestBody @Validated(AddAction.class) PersonEditVO vo) {
        log.info("add,vo={}", vo);
        return true;
    }

    @ApiOperation(value = "修改")
    @PutMapping
    public boolean update(@RequestBody @Validated(EditAction.class) PersonEditVO vo) {
        log.info("update,vo={}", vo);
        return true;
    }
}

三、异常拦截

如果参数校验不通过,会抛出MethodArgumentNotValidException异常,我们全局处理下然后返回给接口。

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;

@ControllerAdvice
@Slf4j
public class ValidExceptionHandler {

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseBody
    public Object errorHandler(HttpServletRequest request, MethodArgumentNotValidException e) {
        List errors = e.getBindingResult().getAllErrors();
        if (CollectionUtil.isEmpty(errors)) {
            return errors;
        }
        String message = errors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(";"));
        return message;
    }
}

四、测试

证件号校验失败
返回多个校验失败信息

你可能感兴趣的:(通过实现ConstraintValidator完成自定义校验注解)