通过实现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<? extends Payload>[] 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<Mobile, String> {

    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<ObjectError> errors = e.getBindingResult().getAllErrors();
        if (CollectionUtil.isEmpty(errors)) {
            return errors;
        }
        String message = errors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(";"));
        return message;
    }
}

四、测试

证件号校验失败
返回多个校验失败信息
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容