在 Web 应用中,客户端提交数据之前都会进行数据的校验,比如用户注册时填写的邮箱地址是否符合规范、用户名长度的限制等等,不过这并不意味着服务端的代码可以免去数据验证的工作,用户也可能使用 HTTP 工具直接发送违法数据。为了保证数据的安全性,服务端的数据校验是必须的。
Spring-Validation
对 Hibernate Validation
进行了二次封装,在 SpringMVC
模块中添加了自动校验机制,可以利用注解对 Java Bean
的字段的值进行校验,并将校验信息封装进特定的类中
如果spring boot
的版本大于2.3.x,那么我们需要手动导入依赖:
<dependency>
<groupId>org.hibernate.validatorgroupId>
<artifactId>hibernate-validatorartifactId>
<version>6.2.0.Finalversion>
dependency>
如果版本小于2.3.x,那么 spring-boot-starter-web
会自动导入这个依赖
我这里使用 2.2.5版本的spring boot,所以不需要手动导入:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.5.RELEASEversion>
parent>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
所有的具体限制类型的注解,都必须有@Validation或者 @Validate 注解存在
@Null
被注释的元素必须为null@NotNull
被注释的元素必须不为null@AssertTrue
被注释的元素必须为true@AssertFalse
被注释的元素必须为false@Min(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值@Max(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值@DecimalMin(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值@DecimalMax(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值@Size(max, min)
被注释的元素的大小必须在指定的范围内@Digits (integer, fraction)
被注释的元素必须是一个数字,其值必须在可接受的范围内@Past
被注释的元素必须是一个过去的日期 @Future 被注释的元素必须是一个将来的日期@Pattern(value)
被注释的元素必须符合指定的正则表达式@Email
被注释的元素必须是电子邮箱地址@Length
被注释的字符串的大小必须在指定的范围内@NotEmpty
被注释的字符串的必须非空@Range
被注释的元素必须在合适的范围内Get
的请求的参数一般是使用 RequestParam
和PathVariable
来传递,此时,对Controller类使用 @Validated
注解 ,并在参数上声明 约束注解,如果校验失败,抛出ConstraintViolationException
异常
这里我发现一点,就是我们使用@PathVariable
时,必须要在请求路径上写明参数,比如@PathVariable("name")
,那么请求参数就必须是 /xxx/{name}
@RequstParam
的参数检验:
@RestController
@Validated
public class ValidController {
@RequestMapping("/first")
public String first(@Length(min = 4,max = 5)@RequestParam("name") String name, @RequestParam("age") String age){
System.out.println("名字是 "+name);
System.out.println("年龄是 "+age);
return "first";
}
}
当我们访问 http://127.0.0.1:8080/first?name=啊&age=10
时
报错如下:
@PathVariable
的参数检验:
@RestController
@Validated
public class ValidController {
@GetMapping("/{age}")
public String firstandPathV(@Max(100) @Min(0) @PathVariable("age")int age){
System.out.println("年龄是"+age);
return String.valueOf(age);
}
}
可以发现,参数校验失败的message都是直接指明的,所以我们可以使用message在前端使用
put
和post
请求一般都是使用 @RequestBody
来传递参数,然后后端是使用 DTO 对象来接收,所以我们只需要给DTO 对象加上 @Validated
注解就能实现自动参数校验。这里抛出的异常是
MethodArgumentNotValidException
,spring在参数校验失败时会把这个异常自动转化为400(Bad Request),而前面使用Get方法参数校验失败时返回的则是 500
DTO表示数据传输对象(Data Transfer Object) ,用于服务器和客户端之间交互传输使用的,在spring web项目中可以表示用于接收请求参数的 Bean对象
编写DTO对象:
public class Student {
@Length(min = 4)
private String name;
@NotNull
private int age;
}
如图,name的校验是最小长度是4, age的校验是不为空
@RestController
@Validated
public class ValidController {
@PostMapping("/second")
public String second(@RequestBody @Validated Student student){
System.out.println("年龄是"+student.getAge());
return "second";
}
}
当我们校验name失败时:
返回的是400,但是JAVA程序内部是不会报错的
但是这里我发现age的非空校验无效:
如图,请求的参数 age
的值是null ,但是访问之后:
校验失败,年龄的值是默认值0。
但是当我在String name
上加上 @NotNull
注解时,使用上面的json串会报错。
所以我猜 int age
在赋值时是无法赋值 null的,所以age的值默认还是0,这就能通过 @NotNull
的参数校验,而String name
赋值时能被赋值 null,所以无法通过 @NotNull
的参数校验。所以以后尽量不要使用无法被赋值为null的类型对象使用 @NotNull
进行参数校验
也可以将注解写在对象前面,在类上就不需要写了:
@RestController
public class VController_1 {
@PostMapping("/third")
public String third(@RequestBody @Validated Student student){
return "third";
}
}
前面已经说过,用GET方法参数校验是直接抛出异常,POST和PUT 参数校验则是返回400。
但是一般使用统一异常处理来返回更友好的提示,使状态码返回200,而由业务码去区分系统的异常情况
创建异常处理类:
@RestControllerAdvice
public class HandleExce {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public String handleExce(MethodArgumentNotValidException e){
BindingResult result = e.getBindingResult();
StringBuilder sb=new StringBuilder();
for(FieldError fieldError:result.getFieldErrors()){ sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(",");
}
String msg=sb.toString();
return msg;
}
}
当我们访问验证错误时(注意MethodArgumentNotValidException
是使用RequestBody
校验失败时的报错):
主要是遍历异常对象 e 并把所有内容取出的for循环
处理ConstraintViolationException
异常:
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public String handleExce2(ConstraintViolationException e){
String message = e.getMessage();
return message;
}
访问错误后返回的是:
可能存在多个方法需要使用同一个DTO类来接收参数,而不同的方法的校验规则可能是不一样的,这时,简单地在DTO类的字段上加约束注解无法解决这个问题。因此, 分组校验就出来专门解决这个问题。
在DTO上配置分组:
public class Student {
public interface big{}
public interface small{}
@Length(min = 4)
@NotNull
private String name;
@Max(value = 100 ,groups = big.class)
@Max(value = 20 ,groups = small.class)
private int age;
}
我们使用的类内部的接口来进行分组,那么我们在使用的时候也容易选择具体的组
@PostMapping("/test_group_big")
public String test_group(@RequestBody @Validated(Student.big.class) Student student){
return "test_group_big";
}
@PostMapping("/test_group_small")
public String test_group_s(@RequestBody @Validated(Student.small.class)Student student){
return "test_group_small";
}
如上,一个使用的是 max=100,一个是max=20,那么我们测试是否符合:
如图,确实实现了分组的校验配置
注意:
group
的校验全部不生效我们前面所作的DTO内的属性都是基本数据类型
和String
,但是当属性有一个是对象的时候,然后这个对象也有校验规则,那么就需要嵌套校验
DTO:
public class Student {
public interface big{}
public interface small{}
@Length(min = 4)
@NotNull
private String name;
@Max(value = 100 ,groups = big.class)
@Max(value = 20 ,groups = small.class)
private int age;
@Valid
private Classes classes;
//一系列getter和setter,这里省略
}
@Valid
指定我们的嵌套对象我们的嵌套类 Classes:
public class Classes {
@NotNull
String name;
@Max(40)
@Min(1)
int counts;
//一系列setter和getter
}
Controller:
@PostMapping("/test_nest")
public String test_nest(@RequestBody @Validated Student student){
System.out.println(student.getClasses().getName());
System.out.println(student.getClasses().getCounts());
return "test_nest";
}
如图,校验成功
如果嵌套的不是单个对象,而是对象的集合,那么使用方法也一样:
public class Student {
@Valid
private List<Classes> classes;
}
现在我们的Student
类嵌套了List
集合的校验,但是我们并不需要改动,spring Validation会自动给所有的Classes类进行校验,测试结果如下:
刚刚是嵌套在 Student
类里面的Classes
对象集合,Spring Validation
会自动对集合内所有对象进行校验,但是如果请求体传的是json数组作为Student
集合,我们直接使用list或set来接收,那么校验就不会生效
@PostMapping("/test_before")
public String test_list_before(@RequestBody @Validated List<Student> stulist){
for (Student student : stulist) {
System.out.println(student.getName());
System.out.println(student.getAge());
}
return "test_nest";
}
我们这里使用的是List直接接收数组
我们的Student的name原本设置的是 @Length(min = 4)
,但是可以发现,这里校验并没有生效
我们这时需要自定义一个list :(只需要看list
和tostring
即可,其他都是list的接口方法)
在 list
上使用 @Valid
即可
public class Stulist<E> implements List<E> {
@Valid
public List<E> list=new ArrayList<>();
@Override
public String toString() {
return list.toString();
}
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public boolean contains(Object o) {
return list.contains(o);
}
@Override
public Iterator<E> iterator() {
return list.iterator();
}
@Override
public Object[] toArray() {
return new Object[0];
}
@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
@Override
public boolean add(E e) {
return list.add(e);
}
@Override
public boolean remove(Object o) {
return list.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return list.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends E> c) {
return list.addAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return false;
}
@Override
public boolean retainAll(Collection<?> c) {
return false;
}
@Override
public void clear() {
}
@Override
public E get(int index) {
return null;
}
@Override
public E set(int index, E element) {
return null;
}
@Override
public void add(int index, E element) {
}
@Override
public E remove(int index) {
return null;
}
@Override
public int indexOf(Object o) {
return 0;
}
@Override
public int lastIndexOf(Object o) {
return 0;
}
@Override
public ListIterator<E> listIterator() {
return null;
}
@Override
public ListIterator<E> listIterator(int index) {
return null;
}
@Override
public List<E> subList(int fromIndex, int toIndex) {
return null;
}
}
然后Controller:
@PostMapping("/test_list")
public String test_list(@RequestBody @Validated Stulist<Student> stulist){
for (Student student : stulist) {
System.out.println(student.getName());
System.out.println(student.getAge());
}
return "test_nest";
}
如图,校验有效
框架提供的都是一些简单校验,我们可以自定义校验来满足我们的需求
我们创建 @EncryptID
这个注解
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.ANNOTATION_TYPE,ElementType.CONSTRUCTOR,ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ENConstraintValidator.class)
public @interface EncryptID {
//默认错误消息
String message() default "加密格式错误";
//分组
Class<?>[] groups() default {};
//负载
Class<? extends Payload>[] payload() default {};
}
还需要创建约束校验类:
public class ENConstraintValidator implements ConstraintValidator<EncryptID,String> {
//由数字或者a-f的字母组成,长度在32-256之间
private static final Pattern PATTERN= Pattern.compile("^[a-f\\d]{32,256}$");
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if(s!=null){
Matcher matcher=PATTERN.matcher(s);
return matcher.find();
}
return true;
}
}
这样,我们就可以使用 @EncryptID
这个注解进行校验了:
这里用在Classes
类的name属性
public class Classes {
@NotNull
@EncryptID
String name;
校验失败时,返回的是注解的默认 message
spring validation
默认会检验完所有的字段,然后才抛出异常,可以通过配置,开启 Fail Fast模式,一旦校验失败就立即返回
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.hibernate.validator.HibernateValidator;
@Configuration
public class ValiConfig {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory= Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true) //配置快速失败
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}