JSR(Java Specification Requests)是Java界的重要标准;JSR又细分很多标准,其中JSR303就代表Bean Validation。更多细节可参考:https://jcp.org/en/jsr/detail?id=303。
目录
准备工作
约束性注解(简单)说明
@Validated的使用时机
@Validated与@Valid的简单对比说明
自定义注解
对注解抛出的异常进行处理
欢迎来到JustryDeng的博客!正文start!
引入相关依赖:
org.springframework.boot
spring-boot-starter-validation
注:本人测试时,还引入了lombok、SpringBoot的web、test等基础依赖,这里就不一一给出了。
注解 |
功能 |
@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,字符串trin()后也不能等于“” |
@NotEmpty |
不能为null,集合、数组、map等size()不能为0;字符串trin()后可以等于“” |
@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 myMapNotEmpty;
@NotEmpty
private List
测试方法为:
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注解
*
* 程序输出: 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注解
*
* 程序输出: 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")注解
*
* 程序输出: 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")注解
*
* 程序输出: 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)注解
*
* 程序输出: 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注解
*
* 程序输出: com.aspire.model.ValidationBeanModel$AbcEmail类的myEmail属性 -> 不是一个合法的电子邮件地址
*/
@Test
public void testEmail() {
ValidationBeanModel.AbcEmail vm = new ValidationBeanModel().new AbcEmail();
vm.setMyEmail("[email protected]");
fa(vm);
}
/**
* 在myFuture属性上加@Future注解
*
* 程序输出: 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)注解
*
* 程序输出: 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)注解
*
* 程序输出: 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)注解
*
* 程序输出: 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()后等于"",那么会提示 不能为空
*
* 程序输出: 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
*
*
* 程序输出: 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
*
* 程序输出: 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, 是空都不行
*
* 程序输出: 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注解
*
*
* 程序输出: 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+")注解
*
*
* 程序输出: 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)注解
*
*
* 程序输出: 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)注解
*
*
* 程序输出: com.aspire.model.ValidationBeanModel$AbcSize类的mySize属性 -> 个数必须在3和5之间
*/
@Test
public void testSize() {
ValidationBeanModel.AbcSize vm = new ValidationBeanModel().new AbcSize();
List list = new ArrayList<>(4);
list.add(0);
list.add(1);
vm.setMySize(list);
fa(vm);
}
/**
* 在myURL属性上加@URL注解
*
*
* 程序输出: 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 void fa(T obj) {
Set> cvSet = validator.validate(obj);
for (ConstraintViolation cv : cvSet) {
System.err.println(cv.getRootBean().getClass().getName() + "类的"
+ cv.getPropertyPath() + "属性 -> " + cv.getMessage());
}
}
}
@Validated的使用位置较多(可详见源码),但其主流的使用位置却是以下两种:
◎在Controller层中,放在模型参数对象前。
当Controller层中参数是一个对象模型时,只有将@Validated直接放在该模型前,该模型内部的字段才会被
校验(如果有对该模型的字段进行约束的话)。
◎在Controller层中,放在类上。
当一些约束是直接出现在Controller层中的参数前时,只有将@Validated放在类上时,参数前的约束才会生效。
以下是简单的测试代码:
import com.aspire.model.ValidationBeanModel;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.DecimalMax;
/**
* Controller层 --- 初步简单测试 @Validated 的使用位置
*
* 对比测试过程:
* 方案一 : 不在类上加@Validated注解,访问这六个接口
* 方案二 : 在类上加@Validated注解,再次访问这六个接口
*
* 对比方案一和方案二,可初步得出@Validated的使用时机:
* 1.当我们是在模型里面对模型字段添加约束注解,在Controller中使用模型接收数
* 据时,@Validated要直接放在该模型参数前才有效。 如: "/test/one"
* 2.当我们是直接在Controller层中的参数前,使用约束注解时,@Validated要直接放在类上,
* 才会有效。如: /test/six
*
*
* @author JustryDeng
* @date 2019/1/18 22:22
*/
@RestController
@Validated
public class JustryDengController {
@RequestMapping(value = "/test/one")
public String validatioOne(@Validated ValidationBeanModel.AbcDecimalMax myDecimalMax) {
System.out.println(myDecimalMax.getMyDecimalMax());
return "one pass!";
}
@RequestMapping(value = "/test/two")
@Validated
public String validatioTwo(ValidationBeanModel.AbcDecimalMax myDecimalMax) {
System.out.println(myDecimalMax.getMyDecimalMax());
return "two pass!";
}
@RequestMapping(value = "/test/three")
public String validatioThree(ValidationBeanModel.AbcDecimalMax myDecimalMax) {
System.out.println(myDecimalMax.getMyDecimalMax());
return "three pass!";
}
@RequestMapping(value = "/test/four")
public String validatioFour(@Validated @DecimalMax(value = "12.3") String myDecimalMax) {
System.out.println(myDecimalMax);
return "four pass!";
}
@RequestMapping(value = "/test/five")
@Validated
public String validatioFive(@DecimalMax(value = "12.3") String myDecimalMax) {
System.out.println(myDecimalMax);
return "five pass!";
}
@RequestMapping(value = "/test/six")
@Validated
public String validatioSix(@DecimalMax(value = "12.3") String myDecimalMax) {
System.out.println(myDecimalMax);
return "six pass!";
}
}
@Valid注解与@Validated注解功能大部分类似;两者的不同主要在于:@Valid属于javax下的,而@Validated属于spring下;@Valid支持嵌套校验、而@Validated不支持,@Validated支持分组,而@Valid不支持。笔者这里只简单介绍@Validated的使用时机。
虽然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 javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* ConstraintsJustryDeng注解 校验器 实现
*
* 注:验证器需要实现ConstraintValidator, 其中 U为对应的注解类, V为该注解的@Target指向的类型
*
* @author JustryDeng
* @date 2019/1/15 1:19
*/
public class JustryDengValidator implements ConstraintValidator {
/** 错误提示信息 */
private String contains;
/**
* 初始化方法, 在执行isValid 方法前,会先执行此方法
*
* @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
* Provides contextual data and operation when applying a given constraint validator.
* @return 校验是否通过
* @date 2019/1/19 11:30
*/
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
if (value instanceof String) {
String strMessgae = (String) value;
return strMessgae.contains(contains);
} else if (value instanceof Integer) {
return contains.contains(String.valueOf(value));
}
return false;
}
}
第三步:自定义注解简单使用测试
Controller层中是这样的:
访问一下这个接口(当参数值符合要求时):
访问一下这个接口(当参数值不符合要求时):
说明:当注解校验不通过时,直接将异常信息返回给前端其实并不友好,我们可以将异常包装一下,返回给前端。
描述:当我们将@Validated注解放在参数模型前时,会校验该参数模型内的被标注了的字段。当校验不通过时
会抛出MethodArgumentNotValidException异常。
处理前:
参数模型是这样的:
Controller是这样的:
使用postman测试,当校验不通过时显示如下:
处理后:
参数模型是这样的(没有变):
Controller是这样的(在@Validated注解的参数后,紧接着加上BindingResult):
再次使用postman测试,当校验不通过时显示如下:
postman中返回了数据,说明虽然参数错误,但是不影响程序的执行。
程序在控制台输出了如下信息:
可见,后台已经获悉了错误,至于怎么处理,这就看伟大的程序员们了。
描述:当我们将@Validated注解放在参数模型前时,会校验该参数模型内的被标注了的字段。当校验不
通过时会抛出MethodArgumentNotValidException异常;当我们将@Validated注解放在类上时,
会校验该类中的所有接口中的直接被约束的参数,当校验不通过时会抛出
ConstraintViolationException异常。
处理前:
示例一:Controller是这样的:
使用postman测试,当校验不通过时显示如下:
示例二:Controller是这样的:
注:此处的User模型与情况一里给出的模型是一样的,这里就不再给出了。
使用postman测试,当校验不通过时显示如下:
进行处理:加入AOP全局异常处理器:
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
/**
* SpringMVC统一异常处理
* 注:@ControllerAdvice为Controller层增强器,其只能处理Controller层抛出的异常;
* 由于代码间的层级调用机制 、异常的处理机制等,所以这里处理Controller层的异常,就相当于
* 处理了全局异常
*
* 注: @RestControllerAdvice等同于 @ResponseBody 加上 @ControllerAdvice
*
* @author JustryDeng
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 具体的处理异常的逻辑
* 注:@ExceptionHandler的value属性指定要处理哪些异常;
*
* @param ex
* 捕获到的异常
* @return 返回给前端的data
*/
@ExceptionHandler(value = {Exception.class})
public Map globalExceptionHandleMethod(Exception ex) {
Map resultMap = new HashMap<>(4);
if (ex instanceof ConstraintViolationException) {
ConstraintViolationException cvExceptionex = (ConstraintViolationException) ex;
resultMap.put("msg", "@Validated约束在类上,直接校验接口的参数时异常 -> " + cvExceptionex.getMessage());
resultMap.put("code", "1");
} else if (ex instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException manvExceptionex = (MethodArgumentNotValidException) ex;
resultMap.put("msg", "@Validated约束在参数模型前,校验该模型的字段时发生异常 -> " + manvExceptionex.getMessage());
resultMap.put("code", "2");
} else {
resultMap.put("msg", "系统异常");
resultMap.put("code", "3");
}
return resultMap;
}
}
处理后,使用postman再次进行上述两个请求:
可见,异常处理成功!