简介
Spring Validation
是在Spring Context
下的,在Spring Boot
项目中,我们引入spring-boot-starter-web
便会引入进来,Spring Validation
是对Hibernate Validator
的二次封装,使我们可以更方便的在Spring MVC
中完成自动校验。
Hibernate Validator
是对JSR-303(Bean Validation)
的参考实现。Hibernate Validator
提供了JSR-303
规范中所有内置constraint
的实现,除此之外还有一些附加的constraint
。
JSR-303
定义的constraint
:
Constraint | Description |
---|---|
@Null | 被注解的元素必须为null |
@NotNull | 被注解的元素必须不为null |
@AssertTure | 被注解的元素必须为ture |
@AssertFalse | 被注解的元素必须为false |
@Min(value) | 被注解的元素必须是数字且必须大于等于指定值 |
@Max(value) | 被注解的元素必须是数字且必须小于等于指定值 |
@DecimalMin(value) | 被注解的元素必须是数字且必须大于等于指定值 |
@DecimalMax(value) | 被注解的元素必须是数字且必须小于等于指定值 |
@Size(max, min) | 被注解的元素必须在指定的范围内 |
@Digits(integer, fraction) | 被注解的元素必须是数字且其值必须在给定的范围内 |
@Past | 被注解的元素必须是一个过去的日期 |
@Future | 被注解的元素必须是一个将来的日期 |
@Pattern(value) | 被注解的元素必须符合给定正则表达式 |
Hibernate Validator
附加实现的constraint
Constraint | Description |
---|---|
被注解的元素必须是Email 地址 |
|
@Length(min, max) | 被注解的元素长度必须在指定的范围内 |
@NotEmpty | 被注解的元素必须 |
@Range | 被注解的元素(可以是数字或者表示数字的字符串)必须在给定的范围内 |
@URL | 被注解的元素必须是URL |
当然,我们也可以自定义实现,自定义实现在下面使用中在讲吧。
使用
在开始使用之前,先做好准备工作,创建一个Spring Boot
项目,然后引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
只需要引入这个依赖就可以了。
使用@Validated注解拦截校验
在Controller
中,我们需要校验前端传递过来的参数,我们可以这么写
@RestController
public class TestController {
@PostMapping("/test")
public Object test(@RequestBody @Validated User user, BindingResult result) {
if (result.hasErrors()) {
return result.getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList());
}
return user;
}
}
只需要在需要校验的实体前面打上@Validated
注解就可以了,这时候,如果我们传递的参数符合要求,则会正常返回。否则返回:
[
"age字段不合法",
"name字段不合法"
]
它会将我们所有不合法信息一次性全部返回,在日常开发中,我们可以吧校验BindingResult
是否有错误信息的校验统一抽出到一个工具类中去做处理,使用项目中统一格式返回错误信息就好。这就是一个最简单的校验示例了,其他注解也都是类似的,就不多举例了,可以自己尝试着玩玩。
在日常开发中想必都曾遇到过这样的需求,比如这个age这个字段,我想要这个字段只在PC
端校验,在App
端不做限制,这就需要用到分组校验了,每个注解都提供了一个group
属性,利用这个属性就可以轻易做到以上需求。比如在User上的注解中加入group
属性,指定其被校验的group
:
public class User {
@Length(min = 1, max = 22, message = "name字段不合法", groups = {App.class, PC.class})
private String name;
@Min(value = 1, message = "age字段不合法", groups = PC.class)
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
在Controller中的@Validated中指定当前group
:
@RestController
public class TestController {
@PostMapping("/test")
public Object test(@RequestBody @Validated(App.class) User user, BindingResult result) {
if (result.hasErrors()) {
return result.getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList());
}
return user;
}
}
这时候我再使用两个不合法字段访问返回:
[
"name字段不合法"
]
可以看到,它并没有对age
字段进行校验。这就是它的分组校验。
在方法实现中拦截校验
它不只是在Controller校验前端传递过来的参数的时候可以用,它在方法中同样可以用,我们可以这样来使用:
@RestController
public class TestController {
@Autowired
ObjectMapper objectMapper;
@Autowired
SmartValidator smartValidator;
@GetMapping("/test")
public Object test() {
String context = "{\"name\": \"felixu\",\"age\": 0}";
User user = null;
try {
user = objectMapper.readValue(context, User.class);
} catch (IOException e) {
e.printStackTrace();
}
BeanPropertyBindingResult result = new BeanPropertyBindingResult(user, "user");
smartValidator.validate(user, result);
if (result.hasErrors()) {
return result.getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList());
}
return user;
}
}
使用需要被校验的实体构造BeanPropertyBindingResult
对象,然后将传递给SmartValidator
的validate
方法来完成跟上面相同的校验。validate
有个重载方法,也接收分组,所以这种方式同样可以实现分组校验。
自定义实现
需求总是多变的,有时候,可能上面的校验方式并不能满足我们的要求,这时候就需要我们自定义一下校验了,要做到自定义注解来校验,我们需要做以下两步,首先实现ConstraintValidator<A extends Annotation, T>
(ps:原谅我的自恋。。。):
public class IsFelixuValidator implements ConstraintValidator<IsFelixu, String> {
@Override
public void initialize(IsFelixu constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if ("felixu".equals(value)) {
return true;
}
return false;
}
}
isValid
便是我们的校验逻辑,true
为通过校验。
然后我们实现注解:
@Documented
@Constraint(
// 指定对应的校验类
validatedBy = {IsFelixuValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsFelixu {
String message() default "this value is not felixu";
// 这两个属性必须要存在
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
这样就ok
了,我们继续使用之前的来做测试,在User
的name
属性上加上@IsFelixu
注解,此时测试,如果不传递name
为felixu
的值,则会提示如下信息:
[
"this value is not felixu",
"age字段不合法"
]
总结
JSR-303
的发布使得在数据自动绑定和验证变得简单,使开发人员在定义数据模型时不必考虑实现框架的限制。当然Bean Validation
还只是提供了一些最基本的constraint
。
上面只是相对简单的用法,也是我们现在项目中所用到的方式,在实际的开发过程中,用户可以根据自己的需要组合或开发出更加复杂的constraint
。这就需要想象力了,从上面的用法中应该可以想到很多地方可以去使用,但是设计和实现时,往往需要考虑诸多因素,比如易用性和封装的复杂度,等等方面,还需要自己去考量了。