文章目录
Jakarta Validation 内置的 constraint
注解 | 描述 | 示例 |
---|---|---|
@Valid | 验证嵌套对象的约束。 | @Valid |
@NotNull | 被注解的元素不能为 null。 | @NotNull |
@NotEmpty | 集合、数组或字符串不能为空。 | @NotEmpty |
@NotBlank | 字符串不能为 null、空字符串或仅包含空格。 | @NotBlank |
@Size | 字符串、集合、数组或数字的大小必须在指定范围内。 | @Size(min = 1, max = 10) |
被注解的元素必须是有效的电子邮件地址。 | ||
@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 |
字符串不能为空且长度必须大于0 ,已弃用,使用Jakarta Validation的 | @NotEmpty | |
字符串不能为空且不能只包含空格 ,已弃用,使用Jakarta Validation的 | @NotBlank | |
验证字段是否为有效的 email 地址,已弃用,使用Jakarta Validation的 | ||
@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 |
必须通过 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字段为空,但是在更新时候不允许将名字更新为空字符。
分组校验有三个步骤:
- 定义一个分组类(或接口)
public interface Update extends Default{
}
- 在校验注解上添加groups属性指定分组
public class UserVO {
@NotBlank(message = "name 不能为空",groups = Update.class)
private String name;
// 省略其他代码...
}
- 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允许用户自定义校验。自定义校验注解使用起来和内置注解无异,在需要的字段上添加相应注解即可。
如:日期验证,枚举验证,手机号验证,金额验证。
实现很简单,分两步:
- 自定义校验注解
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})用来指定处理这个注解逻辑的类
- 编写校验者类
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 进行参数效验步骤整个过程如下图所示,用户访问接口,然后进行参数效验 ,如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理