Validation参数校验注解


Jakarta Validation 内置的 constraint

注解描述示例
@Valid验证嵌套对象的约束。@Valid
@NotNull被注解的元素不能为 null。@NotNull
@NotEmpty集合、数组或字符串不能为空。@NotEmpty
@NotBlank字符串不能为 null、空字符串或仅包含空格。@NotBlank
@Size字符串、集合、数组或数字的大小必须在指定范围内。@Size(min = 1, max = 10)
@Email被注解的元素必须是有效的电子邮件地址。@Email
@Min被注解的元素必须大于或等于指定的最小值。@Min(value = 0)
@Max被注解的元素必须小于或等于指定值。@Max(value = 100)
@Positive被注解的元素必须为正数。@Positive
@Negative被注解的元素必须为负数。@Negative
@PositiveOrZero被注解的元素必须为非负数(0或正数)。@PositiveOrZero
@NegativeOrZero被注解的元素必须为非正数(0或负数)。@NegativeOrZero
@AssertTrue被注解的元素必须为 true。@AssertTrue
@AssertFalse被注解的元素必须为 false。@AssertFalse
@Pattern字符串必须匹配正则表达式。@Pattern(regexp = “^[a-z]+$”)
@Digits数字的整数位数和小数位数必须在指定范围内。@Digits(integer = 3, fraction = 2)
@Null被注解的元素必须为 null。@Null
@Future被注解的时间必须在当前时间之后。@Future
@Past被注解的时间必须在当前时间之前。@Past
@FutureOrPresent被注解的时间必须在当前时间之后或等于当前时间。@FutureOrPresent
@PastOrPresent被注解的时间必须在当前时间之前或等于当前时间。@PastOrPresent
@DecimalMin数值必须大于或等于指定的最小值(可为小数)。@DecimalMin(value = 0.01)
@DecimalMax数值必须小于或等于指定的最大值(可为小数)。@DecimalMax(value = 100.00)

Hibernate Validator 附加的 constraint

注解描述示例
@Validated指定要在方法参数或返回值上应用的验证规则@Validated
@NotEmpty字符串不能为空且长度必须大于0 ,已弃用,使用Jakarta Validation的@NotEmpty
@NotBlank字符串不能为空且不能只包含空格 ,已弃用,使用Jakarta Validation的@NotBlank
@Email验证字段是否为有效的 email 地址,已弃用,使用Jakarta Validation的@Email
@Length字符串的长度必须在指定范围内@Length(min = 5, max = 20)
@Range数值必须在指定范围内@Range(min = 1, max = 100)
@URL必须是有效的 URL 地址@URL
@UniqueElements元素在集合中必须是唯一的@UniqueElements
@ScriptAssert必须通过指定的脚本验证@ScriptAssert(lang = “groovy”, script = “value.length() > 0”)
@ParameterScriptAssert必须通过基于参数的指定的脚本验证@ParameterScriptAssert(parameter = “param”, script = “param.size() > 0”)
@CodePointLength字符串的Unicode 码点长度必须在指定范围内@CodePointLength(max = 100)
@ConstraintComposition组合多个约束条件@ConstraintComposition(constraints = {@NotNull, @Size(min = 1)})
@DurationMin时间必须大于或等于指定的最小值@DurationMin(minutes = 5)
@DurationMax时间必须小于或等于指定的最大值@DurationMax(hours = 2)
@CreditCardNumber必须是有效的信用卡号@CreditCardNumber
@ISBN字符串必须是有效的 ISBN@ISBN
@Currency必须是有效的货币代码@Currency
@SafeHtml必须包含安全的 HTML@SafeHtml
@LuhnCheck必须通过 Luhn 算法验证@LuhnCheck
@ModCheck必须通过 Mod 算法验证,已弃用:使用@Mod10Check、@Mod11Check@ModCheck
@Mod10Check必须通过 Mod10 算法验证@Mod10Check
@Mod11Check必须通过 Mod11 算法验证@Mod11Check
@EAN必须是有效的 EAN(国际商品条码)号码@EAN

@Max(value)、@Min(value)、@DecimalMax(value)、@DecimalMin(Value)区别:

  • @Max、@Min接受一个Long类型的值
  • @DecimalMax、@DecimalMin接受一个字符串类型的值(BigDecimal的字符串表示形式,因此可以是小数)
  • 数字超过Long.MAX_VALUE或Long.MIN_VALUE以下或者数字是小数,@DecimalMax、@DecimalMin是唯一的选择。

校验注解使用示例

@ScriptAssert(lang = "javascript", script = "this.age < 18", alias = "this", message = "必须通过指定的脚本验证")
@Data
public class UserDemo1 {
    private Integer age;
}
@Validated
@RestController
@RequestMapping("valid")
public class ValidController {

/** ---------------------------- Jakarta Validation 内置的 constraint ---------------------------- **/

    /**
     * NotNull
     * @param value
     * @return
     */
    @GetMapping("NotNull")
    public String notNull(@NotNull(message = "被注解的元素不能为 null") Integer value) {
        return "OK";
    }

    @GetMapping("NotEmpty")
    public String notEmpty(@NotEmpty(message = "集合、数组或字符串不能为空") String value) {
        return "OK";
    }

    @GetMapping("NotBlank")
    public String notBlank(@NotBlank(message = "字符串不能为 null、空字符串或仅包含空格") String value) {
        return "OK";
    }

    @GetMapping("Size")
    public String Size(@Size(min = 1, max = 5, message = "字符串、集合、数组或数字的大小必须在指定范围内") String value) {
        return "OK";
    }

    @GetMapping("Email")
    public String Email(@Email(message = "被注解的元素必须是有效的电子邮件地址") String value) {
        return "OK";
    }

    @GetMapping("Min")
    public String Min(@Min(value = 1, message = "被注解的元素必须大于或等于指定的最小值") Integer value) {
        return "OK";
    }

    @GetMapping("Max")
    public String Max(@Max(value = 5, message = "被注解的元素必须小于或等于指定值") Integer value) {
        return "OK";
    }

    @GetMapping("Positive")
    public String Positive(@Positive(message = "被注解的元素必须为正数") Integer value) {
        return "OK";
    }

    @GetMapping("Negative")
    public String Negative(@Negative(message = "被注解的元素必须为负数") Integer value) {
        return "OK";
    }

    @GetMapping("PositiveOrZero")
    public String PositiveOrZero(@PositiveOrZero(message = "被注解的元素必须为非负数(0或正数)") Integer value) {
        return "OK";
    }

    @GetMapping("NegativeOrZero")
    public String NegativeOrZero(@NegativeOrZero(message = "被注解的元素必须为非正数(0或负数)") Integer value) {
        return "OK";
    }

    @GetMapping("AssertTrue")
    public String AssertTrue(@AssertTrue(message = "被注解的元素必须为 true") Boolean value) {
        return "OK";
    }

    @GetMapping("AssertFalse")
    public String AssertFalse(@AssertFalse(message = "被注解的元素必须为 false") Boolean value) {
        return "OK";
    }

    @GetMapping("Pattern")
    public String Pattern(@Pattern(regexp = "^[a-zA-Z]+$", message = "字符串必须匹配正则表达式") String value) {
        return "OK";
    }

    @GetMapping("Digits")
    public String Digits(@Digits(integer = 5, fraction = 2, message = "数字的整数位数和小数位数必须在指定范围内") String value) {
        return "OK";
    }

    @GetMapping("Null")
    public String Null(@Null(message = "被注解的元素必须为 null") String value) {
        return "OK";
    }

    @GetMapping("Future")
    public String Future(@Future(message = "被注解的时间必须在当前时间之后") Date value) {
        return "OK";
    }

    @GetMapping("Past")
    public String Past(@Past(message = "被注解的时间必须在当前时间之前") Date value) {
        return "OK";
    }

    @GetMapping("FutureOrPresent")
    public String FutureOrPresent(@FutureOrPresent(message = "被注解的时间必须在当前时间之后或等于当前时间") Date value) {
        return "OK";
    }

    @GetMapping("PastOrPresent")
    public String PastOrPresent(@PastOrPresent(message = "被注解的时间必须在当前时间之前或等于当前时间") Date value) {
        return "OK";
    }

    @GetMapping("DecimalMin")
    public String DecimalMin(@DecimalMin(value = "0.01", message = "数值必须大于或等于指定的最小值(可为小数)") String value) {
        return "OK";
    }

    @GetMapping("decimalMax")
    public String DecimalMax(@DecimalMax(value = "10.01", message = "数值必须小于或等于指定的最大值(可为小数)") String value) {
        return "OK";
    }



/** ---------------------------- Jakarta Validation 内置的 constraint ---------------------------- **/

    /**
     * Length
     * @param value
     * @return
     */
    @GetMapping("Length")
    public String Length(@Length(min = 1, max = 5, message = "字符串的长度必须在指定范围内") String value) {
        return "OK";
    }

    @GetMapping("Range")
    public String Range(@Range(min = 1, max = 5, message = "数值必须在指定范围内") String value) {
        return "OK";
    }

    @GetMapping("URL")
    public String URL(@URL(message = "必须是有效的 URL 地址") String value) {
        return "OK";
    }

    @PostMapping("UniqueElements")
    public String UniqueElements(@UniqueElements(message = "元素在集合中必须是唯一的") @RequestBody List<String> value) {
        return "OK";
    }

    @PostMapping("ScriptAssert")
    public String ScriptAssert(@Validated @RequestBody UserDemo1 value) {
        return "OK";
    }

    @PostMapping("ParameterScriptAssert")
    @ParameterScriptAssert(lang = "javascript", script = "req.age < 18", message = "必须通过指定的脚本验证")
    public String ParameterScriptAssert(@Validated @RequestBody UserDemo1 req) {
        return "OK";
    }
    @PostMapping("ParameterScriptAssert1")
    @ParameterScriptAssert(lang = "javascript", script = "age < 18", message = "必须通过指定的脚本验证")
    public String ParameterScriptAssert1(@Validated Integer age) {
        return "OK";
    }

    @GetMapping("CodePointLength")
    public String CodePointLength(@CodePointLength(min = 1, max = 5, message = "字符串的长度必须在指定范围内") String value) {
        return "OK";
    }

    // @GetMapping("ConstraintComposition")
    // public String ConstraintComposition(@ConstraintComposition String value) {
    //     return "OK";
    // }
}

@Valid与@Validated的使用范围

注解使用范围
@Valid字段、方法、参数、构造方法、类型
@Validated类(接口、朱姐、枚举)、方法、参数

分组校验

如果同一个参数,需要在不同场景下应用不同的校验规则,就需要用到分组校验了。比如:新注册用户还没起名字,我们允许name字段为空,但是在更新时候不允许将名字更新为空字符。

分组校验有三个步骤:

  1. 定义一个分组类(或接口)
public interface Update extends Default{
}
  1. 在校验注解上添加groups属性指定分组
public class UserVO {
    @NotBlank(message = "name 不能为空",groups = Update.class)
    private String name;
    // 省略其他代码...
}
  1. Controller方法的@Validated注解添加分组类
@PostMapping("update")
public ResultInfo update(@Validated({Update.class}) UserVO userVO) {
    return new ResultInfo().success(userVO);
}

自定义的Update分组接口继承了Default接口。校验注解(如: @NotBlank)和@validated默认其他注解都属于Default.class分组,这一点在javax.validation.groups.Default注释中有说明

/**
 * Default Jakarta Bean Validation group.
 * <p>
 * Unless a list of groups is explicitly defined:
 * <ul>
 *     <li>constraints belong to the {@code Default} group</li>
 *     <li>validation applies to the {@code Default} group</li>
 * </ul>
 * Most structural constraints should belong to the default group.
 *
 * @author Emmanuel Bernard
 */
public interface Default {
}

在编写Update分组接口时,如果继承了Default,下面两个写法就是等效的:
@Validated({Update.class}),@Validated({Update.class,Default.class})
如果Update不继承Default,@Validated({Update.class})就只会校验属于Update.class分组的参数字段

递归校验

如果 UserVO 类中增加一个 OrderVO 类的属性,而 OrderVO 中的属性也需要校验,就用到递归校验了,只要在相应属性上增加@Valid注解即可实现(对于集合同样适用)

public class OrderVO {
    @NotNull
    private Long id;
    @NotBlank(message = "itemName 不能为空")
    private String itemName;
    // 省略其他代码...
}
public class UserVO {
    @NotBlank(message = "name 不能为空",groups = Update.class)
    private String name;
    //需要递归校验的OrderVO
    @Valid
    private OrderVO orderVO;
    // 省略其他代码...
}   

自定义校验

validation 为我们提供了这么多特性,几乎可以满足日常开发中绝大多数参数校验场景了。但是,一个好的框架一定是方便扩展的。有了扩展能力,就能应对更多复杂的业务场景,毕竟在开发过程中,唯一不变的就是变化本身。 Validation允许用户自定义校验。自定义校验注解使用起来和内置注解无异,在需要的字段上添加相应注解即可。

如:日期验证,枚举验证,手机号验证,金额验证。

实现很简单,分两步:

  1. 自定义校验注解
package cn.soboys.core.validator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;


/**
 * @author kenx
 * @version 1.0
 * @date 2021/1/21 20:49 
 * 日期验证 约束注解类
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsDateTimeValidator.class}) // 标明由哪个类执行校验逻辑
public @interface IsDateTime {
    // 校验出错时默认返回的消息
    String message() default "日期格式错误";
    //分组校验
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    //下面是我自己定义属性
    boolean required() default true;
    String dateFormat() default "yyyy-MM-dd";
}

注意:message用于显示错误信息这个字段是必须的,groups和payload也是必须的
@Constraint(validatedBy = { HandsomeBoyValidator.class})用来指定处理这个注解逻辑的类

  1. 编写校验者类
import cn.hutool.core.util.StrUtil;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/1/21 20:51
 * 日期验证器
 */
public class IsDateTimeValidator implements ConstraintValidator<IsDateTime, String> {

    private boolean required = false;
    private String dateFormat = "yyyy-MM-dd";

    /**
     * 用于初始化注解上的值到这个validator
     * @param constraintAnnotation
     */
    @Override
    public void initialize(IsDateTime constraintAnnotation) {
        required = constraintAnnotation.required();
        dateFormat = constraintAnnotation.dateFormat();
    }

    /**
     * 具体的校验逻辑
     * @param value
     * @param context
     * @return
     */
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (required) {
            return ValidatorUtil.isDateTime(value, dateFormat);
        } else {
            if (StrUtil.isBlank(value)) {
                return true;
            } else {
                return ValidatorUtil.isDateTime(value, dateFormat);
            }
        }
    }
}

注意这里验证逻辑我抽出来单独写了一个工具类,ValidatorUtil

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/1/21 20:51
 * 验证表达式
 */
public class ValidatorUtil {
    private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
    private static final Pattern money_pattern = Pattern.compile("^[0-9]+\\.?[0-9]{0,2}$");

    /**
     * 验证手机号
     * @param src
     * @return
     */
    public static boolean isMobile(String src) {
        if (StrUtil.isBlank(src)) {
            return false;
        }
        Matcher m = mobile_pattern.matcher(src);
        return m.matches();
    }
    /**
     * 验证枚举值是否合法 ,所有枚举需要继承此方法重写
     *
     * @param beanClass 枚举类
     * @param status    对应code
     * @return
     * @throws Exception
     */
    public static boolean isEnum(Class<?> beanClass, String status) throws Exception {
        if (StrUtil.isBlank(status)) {
            return false;
        }
        //转换枚举类
        Class<Enum> clazz = (Class<Enum>) beanClass;
        /**
         * 其实枚举是语法糖
         * 是封装好的多个Enum类的实列
         * 获取所有枚举实例
         */
        Enum[] enumConstants = clazz.getEnumConstants();

        //根据方法名获取方法
        Method getCode = clazz.getMethod("getCode");
        Method getDesc = clazz.getMethod("getDesc");
        for (Enum enums : enumConstants) {
            //得到枚举实例名
            String instance = enums.name();
            //执行枚举方法获得枚举实例对应的值
            String code = getCode.invoke(enums).toString();
            if (code.equals(status)) {
                return true;
            }
            String desc = getDesc.invoke(enums).toString();
            System.out.println(StrFormatter.format("实列{}---code:{}desc{}", instance, code, desc));
        }
        return false;
    }

    /**
     * 验证金额0.00
     *
     * @param money
     * @return
     */
    public static boolean isMoney(BigDecimal money) {
        if (StrUtil.isEmptyIfStr(money)) {
            return false;
        }
        if (!NumberUtil.isNumber(String.valueOf(money.doubleValue()))) {
            return false;
        }
        if (money.doubleValue() == 0) {
            return false;
        }
        Matcher m = money_pattern.matcher(String.valueOf(money.doubleValue()));
        return m.matches();
    }

    /**
     * 验证 日期
     *
     * @param date
     * @param dateFormat
     * @return
     */
    public static boolean isDateTime(String date, String dateFormat) {
        if (StrUtil.isBlank(date)) {
            return false;
        }
        try {
            DateUtil.parse(date, dateFormat);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

校验流程解析
使用 Validation API 进行参数效验步骤整个过程如下图所示,用户访问接口,然后进行参数效验 ,如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值