准备工作
引入相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
注:我们知道maven依赖都是环环相扣的,例如我们在项目中spring-boot-starter-web,它也会将validation的依赖引入,因此我们引入依赖的是时候,有时是可以忽略的。
约束性注解(简单)说明
注解 | 功能 |
@AssertFalse | 可以为null,如果不为null的话必须为false |
@AssertTrue | 可以为null,如果不为null的话必须为true |
@DecimalMax | 设置不能超过最大值 |
@DecimalMin | 设置不能超过最小值 |
@Digits | 设置必须是数字且数字整数的位数和小数的位数必须在指定范围内 |
@Future | 日期必须在当前日期的未来 |
@Past | 日期必须在当前日期的过去 |
@Max | 最大不得超过此最大值 |
@Min | 最大不得小于此最小值 |
@NotNull | 不能为null,可以是空 |
@Null | 必须为null |
@Pattern | 必须满足指定的正则表达式 |
@Size | 集合、数组、map等的size()值必须在指定范围内 |
| 必须是email格式 |
@Length | 长度必须在指定范围内 |
@NotBlank | 字符串不能为null,字符串trim()后也不能等于“” |
@NotEmpty | 不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“” |
@Range | 值必须在指定范围内 |
@URL | 必须是一个URL |
下面简单测试一下上述各注解:
测试所用模型为:
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* Validation注解
*
* @author JustryDeng
* @date 2019/1/15 0:43
*/
public class ValidationBeanModel {
@Setter
@Getter
public class AbcAssertFalse {
@AssertFalse
private Boolean myAssertFalse;
}
@Setter
@Getter
public class AbcAssertTrue {
@AssertTrue
private Boolean myAssertTrue;
}
@Setter
@Getter
public class AbcDecimalMax {
@DecimalMax(value = "12.3")
private String myDecimalMax;
}
@Setter
@Getter
public class AbcDecimalMin {
@DecimalMin(value = "10.3")
private String myDecimalMin;
}
@Setter
@Getter
public class AbcDigits {
@Digits(integer = 5, fraction = 3)
private Integer myDigits;
}
@Setter
@Getter
public class AbcEmail {
@Email
private String myEmail;
}
@Setter
@Getter
public class AbcFuture {
@Future
private Date myFuture;
}
@Setter
@Getter
public class AbcLength {
@Length(min = 5, max = 10)
private String myLength;
}
@Setter
@Getter
public class AbcMax {
@Max(value = 200)
private Long myMax;
}
@Setter
@Getter
public class AbcMin {
@Min(value = 100)
private Long myMin;
}
@Setter
@Getter
public class AbcNotBlank {
@NotBlank
private String myStringNotBlank;
@NotBlank
private String myObjNotBlank;
}
@Setter
@Getter
public class AbcNotEmpty {
@NotEmpty
private String myStringNotEmpty;
@NotEmpty
private String myNullNotEmpty;
@NotEmpty
private Map<String, Object> myMapNotEmpty;
@NotEmpty
private List<Object> myListNotEmpty;
@NotEmpty
private Object[] myArrayNotEmpty;
}
@Setter
@Getter
public class AbcNotNull {
@NotNull
private String myStringNotNull;
@NotNull
private Object myNullNotNull;
@NotNull
private Map<String, Object> myMapNotNull;
}
@Setter
@Getter
public class AbcNull {
@Null
private String myStringNull;
@Null
private Map<String, Object> myMapNull;
}
@Setter
@Getter
public class AbcPast {
@Past
private Date myPast;
}
@Setter
@Getter
public class AbcPattern {
@Pattern(regexp = "\\d+")
private String myPattern;
}
@Setter
@Getter
public class AbcRange {
@Range(min = 100, max = 100000000000L)
private Double myRange;
}
@Setter
@Getter
public class AbcSize {
@Size(min = 3, max = 5)
private List<Integer> mySize;
}
@Setter
@Getter
public class AbcURL {
@URL
private String myURL;
}
}
测试方法为:
import com.aspire.model.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationDemoApplicationTests {
private Validator validator;
@Before
public void initValidator() {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}
/**
* 在myAssertTrue属性上加@AssertTrue注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcAssertTrue类的myAssertTrue属性 -> 只能为true
*/
@Test
public void testAssertTrue() {
ValidationBeanModel.AbcAssertTrue vm = new ValidationBeanModel().new AbcAssertTrue();
vm.setMyAssertTrue(false);
fa(vm);
}
/**
* 在myAssertFalse属性上加@AssertFalse注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcAssertFalse类的myAssertFalse属性 -> 只能为false
*/
@Test
public void testAssertFalse() {
ValidationBeanModel.AbcAssertFalse vm = new ValidationBeanModel().new AbcAssertFalse();
vm.setMyAssertFalse(true);
fa(vm);
}
/**
* 在myDecimalMax属性上加@DecimalMax(value = "12.3")注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcDecimalMax类的myDecimalMax属性 -> 必须小于或等于12.3
*/
@Test
public void testDecimalMax() {
ValidationBeanModel.AbcDecimalMax vm = new ValidationBeanModel().new AbcDecimalMax();
vm.setMyDecimalMax("123");
fa(vm);
}
/**
* 在myDecimalMin属性上加@DecimalMin(value = "10.3")注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcDecimalMin类的myDecimalMin属性 -> 必须大于或等于10.3
*/
@Test
public void testDecimalMin() {
ValidationBeanModel.AbcDecimalMin vm = new ValidationBeanModel().new AbcDecimalMin();
vm.setMyDecimalMin("1.23");
fa(vm);
}
/**
* 在myDigits属性上加@Digits(integer = 5, fraction = 3)注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcDigits类的myDigits属性 -> 数字的值超出了允许范围(只允许在5位整数和3位小数范围内)
*/
@Test
public void testDigits() {
ValidationBeanModel.AbcDigits vm = new ValidationBeanModel().new AbcDigits();
vm.setMyDigits(1000738);
fa(vm);
}
/**
* 在myEmail属性上加@Email注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcEmail类的myEmail属性 -> 不是一个合法的电子邮件地址
*/
@Test
public void testEmail() {
ValidationBeanModel.AbcEmail vm = new ValidationBeanModel().new AbcEmail();
vm.setMyEmail("asd@.com");
fa(vm);
}
/**
* 在myFuture属性上加@Future注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcFuture类的myFuture属性 -> 需要是一个将来的时间
*/
@Test
public void testFuture() {
ValidationBeanModel.AbcFuture vm = new ValidationBeanModel().new AbcFuture();
vm.setMyFuture(new Date(10000L));
fa(vm);
}
/**
* 在myLength属性上加@Length(min = 5, max = 10)注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcLength类的myLength属性 -> 长度需要在5和10之间
*/
@Test
public void testLength() {
ValidationBeanModel.AbcLength vm = new ValidationBeanModel().new AbcLength();
vm.setMyLength("abcd");
fa(vm);
}
/**
* 在myMax属性上加@Max(value = 200)注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcMax类的myMax属性 -> 最大不能超过200
*/
@Test
public void testMax() {
ValidationBeanModel.AbcMax vm = new ValidationBeanModel().new AbcMax();
vm.setMyMax(201L);
fa(vm);
}
/**
* 在myMin属性上加@Min(value = 200)注解
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcMin类的myMin属性 -> 最小不能小于100
*/
@Test
public void testMin() {
ValidationBeanModel.AbcMin vm = new ValidationBeanModel().new AbcMin();
vm.setMyMin(99L);
fa(vm);
}
/**
* 在myStringNotBlank属性上加@NotBlank注解
* 在myObjNotBlank属性上加@NotBlank注解
*
* 注:如果属性值为null 或者 .trim()后等于"",那么会提示 不能为空
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcNotBlank类的myObjNotBlank属性 -> 不能为空
* com.aspire.model.ValidationBeanModel$AbcNotBlank类的myStringNotBlank属性 -> 不能为空
*/
@Test
public void testNotBlank() {
ValidationBeanModel.AbcNotBlank vm = new ValidationBeanModel().new AbcNotBlank();
vm.setMyObjNotBlank(null);
vm.setMyStringNotBlank(" ");
fa(vm);
}
/**
* 在myStringNotEmpty属性上加@NotEmpty注解
* 在myNullNotEmpty属性上加@NotEmpty注解
* 在myMapNotEmpty属性上加@NotEmpty注解
* 在myListNotEmpty属性上加@NotEmpty注解
* 在myArrayNotEmpty属性上加@NotEmpty注解
*
* 注:String可以是.trim()后等于""的字符串,但是不能为null
* 注:MAP、Collection、Array既不能是空,也不能是null
*
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcNotEmpty类的myNullNotEmpty属性 -> 不能为空
* com.aspire.model.ValidationBeanModel$AbcNotEmpty类的myListNotEmpty属性 -> 不能为空
* com.aspire.model.ValidationBeanModel$AbcNotEmpty类的myArrayNotEmpty属性 -> 不能为空
* com.aspire.model.ValidationBeanModel$AbcNotEmpty类的myMapNotEmpty属性 -> 不能为空
*/
@Test
public void testNotEmpty() {
ValidationBeanModel.AbcNotEmpty vm = new ValidationBeanModel().new AbcNotEmpty();
vm.setMyStringNotEmpty(" ");
vm.setMyNullNotEmpty(null);
vm.setMyMapNotEmpty(new HashMap<>(0));
vm.setMyListNotEmpty(new ArrayList<>(0));
vm.setMyArrayNotEmpty(new String[]{});
fa(vm);
}
/**
* 在myStringNotNull属性上加@NotNull注解
* 在myNullNotNull属性上加@NotNull注解
* 在myMapNotNull属性上加@NotNull注解
*
* 注:属性值可以是空的, 但是就是不能为null
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcNotNull类的myNullNotNull属性 -> 不能为null
*/
@Test
public void testNotNull() {
ValidationBeanModel.AbcNotNull vm = new ValidationBeanModel().new AbcNotNull();
vm.setMyStringNotNull(" ");
vm.setMyNullNotNull(null);
vm.setMyMapNotNull(new HashMap<>(0));
fa(vm);
}
/**
* 在myStringNull属性上加@Null注解
* 在myMapNotNull属性上加@Null注解
*
* 注:属性值必须是null, 是空都不行
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcNull类的myMapNull属性 -> 必须为null
* com.aspire.model.ValidationBeanModel$AbcNull类的myStringNull属性 -> 必须为null
*/
@Test
public void testNull() {
ValidationBeanModel.AbcNull vm = new ValidationBeanModel().new AbcNull();
vm.setMyStringNull(" ");
vm.setMyMapNull(new HashMap<>(0));
fa(vm);
}
/**
* 在myPast属性上加@Past注解
*
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcPast类的myPast属性 -> 需要是一个过去的时间
*/
@Test
public void testPast() {
ValidationBeanModel.AbcPast vm = new ValidationBeanModel().new AbcPast();
vm.setMyPast(new Date(20000000000000000L));
fa(vm);
}
/**
* 在myPattern属性上加@Pattern(regexp = "\\d+")注解
*
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcPattern类的myPattern属性 -> 需要匹配正则表达式"\d"
*/
@Test
public void testPattern() {
ValidationBeanModel.AbcPattern vm = new ValidationBeanModel().new AbcPattern();
vm.setMyPattern("ABC");
fa(vm);
}
/**
* 在myRange属性上加@Range(min = 100, max = 100000000000L)注解
*
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcRange类的myRange属性 -> 需要在100和100000000000之间
*/
@Test
public void testRange() {
ValidationBeanModel.AbcRange vm = new ValidationBeanModel().new AbcRange();
vm.setMyRange(32222222222222222222222222222222.323);
fa(vm);
}
/**
* 在mySize属性上加@Size(min = 3, max = 5)注解
*
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcSize类的mySize属性 -> 个数必须在3和5之间
*/
@Test
public void testSize() {
ValidationBeanModel.AbcSize vm = new ValidationBeanModel().new AbcSize();
List<Integer> list = new ArrayList<>(4);
list.add(0);
list.add(1);
vm.setMySize(list);
fa(vm);
}
/**
* 在myURL属性上加@URL注解
*
* <p>
* 程序输出: com.aspire.model.ValidationBeanModel$AbcURL类的myURL属性 -> 需要是一个合法的URL
*/
@Test
public void testURL() {
ValidationBeanModel.AbcURL vm = new ValidationBeanModel().new AbcURL();
vm.setMyURL("www.baidu.xxx");
fa(vm);
}
private <T> void fa(T obj) {
Set<ConstraintViolation<T>> cvSet = validator.validate(obj);
for (ConstraintViolation<T> cv : cvSet) {
System.err.println(cv.getRootBean().getClass().getName() + "类的"
+ cv.getPropertyPath() + "属性 -> " + cv.getMessage());
}
}
}
@Validated与@Valid的简单对比
这里参考了另一个博主的文章:https://blog.csdn.net/qq_27680317/article/details/79970590
结论
@Valid注解与@Validated注解功能大部分类似;两者的不同主要在于:@Valid属于javax下的,而@Validated属于spring下;@Valid支持嵌套校验、而@Validated不支持,@Validated支持分组,而@Valid不支持。笔者这里只简单介绍@Validated的使用时机。
简介
Spring Validation验证框架对参数的验证机制提供了@Validated(Spring’s JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。其中对于字段的特定验证注解比如@NotNull等网上到处都有,这里不详述
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
1. 分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
2. 使用位置
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。
3. 嵌套验证
在比较两者嵌套验证时,先说明下什么叫做嵌套验证。比如我们现在有个实体叫做Item:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@NotNull(message = "props不能为空")
@Size(min = 1, message = "至少要有一个属性")
private List<Prop> props;
}
Item带有很多属性,属性里面有属性id,属性值id,属性名和属性值,如下所示:
public class Prop {
@NotNull(message = "pid不能为空")
@Min(value = 1, message = "pid必须为正整数")
private Long pid;
@NotNull(message = "vid不能为空")
@Min(value = 1, message = "vid必须为正整数")
private Long vid;
@NotBlank(message = "pidName不能为空")
private String pidName;
@NotBlank(message = "vidName不能为空")
private String vidName;
}
属性这个实体也有自己的验证机制,比如属性和属性值id不能为空,属性名和属性值不能为空等。
现在我们有个ItemController接受一个Item的入参,想要对Item进行验证,如下所示:
@RestController
public class ItemController {
@RequestMapping("/item/add")
public void addItem(@Validated Item item, BindingResult bindingResult) {
doSomething();
}
}
上述代码是不能实现嵌套校验的,只在入参加了@Validated(或@Valid),Item实体的props属性不额外加注释也只有@NotNull和@Size,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证。
为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
我们修改Item类如下所示:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@Valid // 嵌套验证必须用@Valid
@NotNull(message = "props不能为空")
@Size(min = 1, message = "props至少要有一个自定义属性")
private List<Prop> props;
}
然后我们在ItemController的addItem函数上再使用@Validated或者@Valid,就能对Item的入参进行嵌套验证。此时Item里面的props如果含有Prop的相应字段为空的情况,Spring Validation框架就会检测出来,bindingResult就会记录相应的错误。
@Validated和@Valid区别小结
总结一下@Validated和@Valid在嵌套验证功能上的区别:
@Validated:用在方法入参上无法单独提供嵌套验证功能(要配合嵌套验证注解@Valid进行嵌套验证)。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。
@Valid:用在方法入参上无法单独提供嵌套验证功能(也要配合嵌套验证注解@Valid进行嵌套验证)。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。
自定义注解
虽然Bean Validation和Hibernate Validator已经提供了非常丰富的校验注解,但是在实际业务中,难免会碰到一些现有注解不足以校验的情况;这时,我们可以考虑自定义Validation注解。
示例:
第一步:创建自定义注解
import com.aspire.constraints.impl.JustryDengValidator;
import javax.validation.Constraint;
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.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 自定义校验注解
* 提示:
* 1、message、contains、payload是必须要写的
* 2、还需要什么方法可根据自己的实际业务需求,自行添加定义即可
*
* 注:当没有指定默认值时,那么在使用此注解时,就必须输入对应的属性值
*
* @author JustryDeng
* @date 2019/1/15 1:17
*/
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Documented
// 指定此注解的实现,即:验证器
@Constraint(validatedBy ={JustryDengValidator.class})
public @interface ConstraintsJustryDeng {
// 当验证不通过时的提示信息
String message() default "JustryDeng : param value must contais specified value!";
// 根据实际需求定的方法
String contains() default "";
// 约束注解在验证时所属的组别
Class<?>[] groups() default { };
// 负载
Class<? extends Payload>[] payload() default { };
}
第二步:编写(第一步中的校验器实现类)该注解
import com.aspire.constraints.anno.ConstraintsJustryDeng;
import org.hibernate.validator.internal.engine.ValidationContext;
import org.hibernate.validator.internal.engine.ValueContext;
import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* ConstraintsJustryDeng注解 校验器 实现
* <p>
* 注:验证器需要实现ConstraintValidator<U, V>, 其中 U为对应的注解类, V为被该注解标记的字段的类型(或其父类型)
*
* 注: 当项目启动后,会(懒加载)创建ConstraintValidator实例,在创建实例后会初始化调
* 用{@link ConstraintValidator#initialize}方法。
* 所以, 只有在第一次请求时,会走initialize方法, 后面的请求是不会走initialize方法的。
*
* 注: (懒加载)创建ConstraintValidator实例时, 会走缓存; 如果缓存中有,则直接使用相
* 同的ConstraintValidator实例; 如果缓存中没有,那么会创建新的ConstraintValidator实例。
* 由于缓存的key是能唯一定位的, 且 ConstraintValidator的实例属性只有在
* {@link ConstraintValidator#initialize}方法中才会写;在{@link ConstraintValidator#isValid}
* 方法中只是读。
* 所以不用担心线程安全问题。
*
* 注: 如何创建ConstraintValidator实例的,可详见源码
* @see ConstraintTree#getInitializedConstraintValidator(ValidationContext, ValueContext)
*
* @author JustryDeng
* @date 2019/1/15 1:19
*/
public class JustryDengValidator implements ConstraintValidator<ConstraintsJustryDeng, Object> {
/** 错误提示信息 */
private String contains;
/**
* 初始化方法, 在(懒加载)创建一个当前类实例后,会马上执行此方法
*
* 注: 此方法只会执行一次,即:创建实例后马上执行。
*
* @param constraintAnnotation
* 注解信息模型,可以从该模型中获取注解类中定义的一些信息,如默认值等
* @date 2019/1/19 11:27
*/
@Override
public void initialize(ConstraintsJustryDeng constraintAnnotation) {
System.out.println(constraintAnnotation.message());
this.contains = constraintAnnotation.contains();
}
/**
* 校验方法, 每个需要校验的请求都会走这个方法
*
* 注: 此方法可能会并发执行,需要根据实际情况看否是需要保证线程安全。
*
* @param value
* 被校验的对象
* @param context
* 上下文
*
* @return 校验是否通过
*/
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
if (value instanceof String) {
String strMessage = (String) value;
return strMessage.contains(contains);
} else if (value instanceof Integer) {
return contains.contains(String.valueOf(value));
}
return false;
}
}
第三步:自定义注解简单使用测试
Controller层中是这样的: