Spring基础系列-参数校验

原创做品,能够转载,可是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9953744.htmlhtml

Spring中使用参数校验

概述

​ JSR 303中提出了Bean Validation,表示JavaBean的校验,Hibernate Validation是其具体实现,并对其进行了一些扩展,添加了一些实用的自定义校验注解。前端

​ Spring中集成了这些内容,你能够在Spring中以原生的手段来使用校验功能,固然Spring也对其进行了一点简单的扩展,以便其更适用于Java web的开发。java

​ 就我所知,Spring中添加了BindingResult用于接收校验结果,同时添加了针对方法中单个请求参数的校验功能,这个功能等于扩展了JSR 303的校验注解的使用范围,使其再也不仅仅做用于Bean中的属性,而是可以做用于单一存在的参数。git

JSR 303 Bean Validation

​ JSR 303中提供了诸多实用的校验注解,这里简单罗列:web

注解 说明 备注
AssertTrue 标注元素必须为true boolean,Boolean,Null
AssertFalse 标注元素必须为false boolean,Boolean,Null
DecimalMax(value,isclusive) 标注元素必须小于等于指定值 BigDecimal,BigInteger, CharSequence,byte,short, int, long,Byte,Short, Integer,Long,Null
DecimalMin(value,isclusive) 标注元素必须大于等于指定值 BigDecimal,BigInteger, CharSequence,byte,short, int, long,Byte,Short, Integer,Long,Null
Digits(integer,fraction) 标注元素必须位于指定位数以内 BigDecimal,BigInteger, CharSequence,byte,short, int, long,Byte,Short, Integer,Long,Null
Email(regexp,flags) 标注元素必须为格式正确的邮件地址 CharSequence
Future 标注元素必须为未来的日期 Date,Calendar,Instant, LocalDate,LocalDateTime, LocalTime,MonthDay, OffsetDateTime,OffsetTime, Year,YearMonth, ZonedDateTime,HijrahDate, JapaneseDate,MinguoDate, ThaiBuddhistDate
FutureOrPresent 标注元素必须为如今或未来的日期 同Future
Max(value) 标注元素必须小于等于指定值 BigDecimal,BigInteger, CharSequence,byte,short, int, long,Byte,Short, Integer,Long,Null
Min(value) 标注元素必须大于等于指定值 BigDecimal,BigInteger, CharSequence,byte,short, int, long,Byte,Short, Integer,Long,Null
Negative 标注元素必须为严格负值 BigDecimal,BigInteger, CharSequence,byte,short, int, long,Byte,Short, Integer,Long,Null
NegativeOrZero 标注元素必须为严格的负值或者0值 BigDecimal,BigInteger, CharSequence,byte,short, int, long,Byte,Short, Integer,Long,Null
NotBlank 标注元素必须不为null,且必须包含至少一个非空字符 CharSequence
NotEmpty 标注元素必须不为null,且必须包含至少一个子元素 CharSequence,Collection,Map,Array
NotNull 标注元素必须不为null all
Null 标注元素必须为null all
Past 标注元素必须为过去的日期 同Future
PastOrPresent 标注元素必须为过去的或者如今的日期 同Future
Pattern(regexp,flags) 标注元素必须匹配给定的正则表达式 CharSequence,Null
Positive 标注元素必须为严格的正值 BigDecimal,BigInteger, CharSequence,byte,short, int, long,Byte,Short, Integer,Long,Null
PositiveOrZero 标注元素必须为严格的正值或者0值 BigDecimal,BigInteger, CharSequence,byte,short, int, long,Byte,Short, Integer,Long,Null
Size(min,max) 标注元素必须在指定范围以内 CharSequence,Collection,Map,Array

​ 上面的罗列的注解都可做用于方法、字段、构造器、参数,还有注解类型之上,其中做用为注解类型目的就是为了组合多个校验,从而自定义一个组合校验注解。正则表达式

Hibernate Validation

​ Hibernate Validation承载自JSR 303的Bean Validation,拥有其全部功能,并对其进行了扩展,它自定义了如下校验注解:spring

注解 说明 备注
Length(min,max) 标注元素的长度必须在指定范围以内,包含最大值 字符串
Range(min,max) 标注元素值必须在指定范围以内 数字值,或者其字符串形式
URL(regexp,flags) 标注元素必须为格式正确的URL 字符串
URL(protocol,host,port) 标注元素必须知足给定的协议主机和端口号 字符串

Spring开发中使用参数校验

Spring中Bean Validation

​ 在Spring中进行Bean Validation有两种状况:json

单组Bean Validation

​ 所谓单组就是不分组,或者只有一组,在底层就是Default.class表明的默认组。后端

​ 使用单组校验是最简单的,下面看看实现步骤:api

第一步:建立Bean模型,并添加校验注解
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Person {
    private String id;
    @NotNull(message = "姓名不能为null")
    private String name;
    @NotNull(message = "性别不能为null")
    private String sex;
    @Range(min = 1,max = 150,message = "年龄必须在1-150之间")
    private int age;
    @Email(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*.\\w+([-.]\\w+)*$", message = "邮箱格式不正确")
    private String email;
    @Pattern(regexp = "^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$", message = "手机号格式不正确")
    private String phone;
    @URL(protocol = "http",host = "localhost",port = 80,message = "主页URL不正确")
    private String hostUrl;
    @AssertTrue(message = "怎么能没有工做呢?")
    private boolean isHasJob;
    private String isnull;
}
第二步:添加API,以Bean模型为参数,启动参数校验
@RestController
@RequestMapping("person")
public class PersonApi {
    @RequestMapping("addPerson")
    public Person addPerson(@Valid final Person person){
        return person;
    }
}

​ 启动应用页面请求:

http://localhost:8080/person/addPerson

​ 结果为:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Mon Nov 12 17:20:53 CST 2018
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='person'. Error count: 4

​ 查看日志:

2018-11-12 17:20:53.722  WARN 15908 --- [io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 4 errors
Field error in object 'person' on field 'sex': rejected value [null]; codes [NotNull.person.sex,NotNull.sex,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.sex,sex]; arguments []; default message [sex]]; default message [性别不能为null]
Field error in object 'person' on field 'age': rejected value [0]; codes [Range.person.age,Range.age,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.age,age]; arguments []; default message [age],150,1]; default message [年龄必须在1-150之间]
Field error in object 'person' on field 'name': rejected value [null]; codes [NotNull.person.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.name,name]; arguments []; default message [name]]; default message [姓名不能为null]
Field error in object 'person' on field 'isHasJob': rejected value [false]; codes [AssertTrue.person.isHasJob,AssertTrue.isHasJob,AssertTrue]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.isHasJob,isHasJob]; arguments []; default message [isHasJob]]; default message [怎么能没有工做呢?]]

​ 可见当咱们不传任何参数的时候,总共有4处校验出错结果,分别为:

姓名不能为空
性别不能为空
年龄必须在1-150之间
怎么能没有工做呢?

​ 可见AssertTrue和AssertFalse自带NotNull属性,Range也自带该属性,他们都不能为null,是必传参数,而后咱们传参:

http://localhost:8080/person/addPerson?name=weiyihaoge&age=30&hasJob=true&sex=nan

​ 页面结果为:

{"id":0,"name":"weiyihaoge","sex":"nan","age":30,"email":null,"phone":null,"hostUrl":null,"isnull":null,"hasJob":true}

​ 日志无提示。

​ 下面咱们简单测试下其余几个校验注解:

http://localhost:8080/person/addPerson?name=weiyihaoge&age=30&hasJob=true&sex=nan&email=1111&phone=123321123&hostUrl=http://localhost:80

​ 可见如下结果:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Mon Nov 12 17:28:55 CST 2018
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='person'. Error count: 2

​ 日志显示:

2018-11-12 17:28:55.511  WARN 15908 --- [nio-8080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'person' on field 'phone': rejected value [123321123]; codes [Pattern.person.phone,Pattern.phone,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.phone,phone]; arguments []; default message [phone],[Ljavax.validation.constraints.Pattern$Flag;@5665d34e,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@6d2bcb00]; default message [手机号格式不正确]
Field error in object 'person' on field 'email': rejected value [1111]; codes [Email.person.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@57ff52fc,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@2f6c1958]; default message [邮箱格式不正确]]

​ 新加的这三个参数都不是必传的,可是一旦传了,就必须保证格式正确,不然就会出现这种状况:校验失败。

总结

​ 使用方法就是在Bean的字段上添加校验注解,在其中进行各类设置,添加错误信息,而后在API里的请求参数中该Bean模型以前添加@Valid注解用于启动针对该Bean的校验,其实这里使用@Validated注解一样能够启动校验,也就是说这里使用@Valid@Validated都可。前者是在JSR 303中定义的,后者是在Spring中定义的。

多组Bean Validation

​ 有时候一个Bean会用同时做为多个api接口的请求参数,在各个接口中须要进行的校验是不相同的,这时候咱们就不能使用上面针对单组的校验方式了,这里就须要进行分组校验了。

​ 所谓分组就是使用校验注解中都有的groups参数进行分组,可是组从何来呢,这个须要咱们本身定义,通常以接口的方式定义。这个接口只是做为组类型而存在,不分担任何其余做用。

第一步:建立分组接口
public interface ModifyPersonGroup {}
第二步:建立Bean模型,并添加分组校验注解
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Person {
    @NotNull(groups = {ModifyPersonGroup.class}, message = "修改操做时ID不能为null")
    private String id;
    @NotNull(message = "姓名不能为null")
    private String name;
    @NotNull(message = "性别不能为null")
    private String sex;
    @Range(min = 1,max = 150,message = "年龄必须在1-150之间")
    private int age;
    @Email(regexp = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*.\\w+([-.]\\w+)*$", message = "邮箱格式不正确")
    private String email;
    @Pattern(regexp = "^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$", message = "手机号格式不正确")
    private String phone;
    @URL(protocol = "http",host = "localhost",port = 80,message = "主页URL不正确")
    private String hostUrl;
    @AssertTrue(message = "怎么能没有工做呢?")
    private boolean isHasJob;
    @Null(groups = {ModifyPersonGroup.class},message = "修改时isnull必须是null")
    private String isnull;
}
第三步:添加API,以Bean模型为参数,启动参数校验
@RestController
@RequestMapping("person")
public class PersonApi {
    @RequestMapping("addPerson")
    public Person addPerson(@Valid final Person person){
        return person;
    }
    @RequestMapping("modifyPerson")
    public Person modifyPerson(@Validated({Default.class, ModifyPersonGroup.class}) final Person person){
        return person;
    }
}

​ 浏览器发起请求:

http://localhost:8080/person/modifyPerson

​ 页面显示:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Mon Nov 12 17:57:12 CST 2018
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='person'. Error count: 5

​ 日志显示:

2018-11-12 17:57:12.264  WARN 16208 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 5 errors
Field error in object 'person' on field 'name': rejected value [null]; codes [NotNull.person.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.name,name]; arguments []; default message [name]]; default message [姓名不能为null]
Field error in object 'person' on field 'isHasJob': rejected value [false]; codes [AssertTrue.person.isHasJob,AssertTrue.isHasJob,AssertTrue]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.isHasJob,isHasJob]; arguments []; default message [isHasJob]]; default message [怎么能没有工做呢?]
Field error in object 'person' on field 'age': rejected value [0]; codes [Range.person.age,Range.age,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.age,age]; arguments []; default message [age],150,1]; default message [年龄必须在1-150之间]
Field error in object 'person' on field 'sex': rejected value [null]; codes [NotNull.person.sex,NotNull.sex,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.sex,sex]; arguments []; default message [sex]]; default message [性别不能为null]
Field error in object 'person' on field 'id': rejected value [null]; codes [NotNull.person.id,NotNull.id,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.id,id]; arguments []; default message [id]]; default message [修改操做时ID不能为null]]

​ 经过上面的内容能够看到在请求修改接口的时候,会提示操做ID不能为null,可是在请求添加接口的时候却不会提示。也就是说这个校验只在请求修改接口的时候才会进行,如此即为分组。

​ 注意:这里有个Default.class默认分组,全部在Bean中添加的未进行分组的校验注解均属于默认分组,当只有默认分组的时候,咱们能够省略它,可是一旦拥有别的分组,想要使用默认分组中的校验就必须将该分组类型也添加到@Validated注解中。

​ 注意:这里只能使用@Validated,不能使用@Valid注解,千万记住。

Spring中Parameter Validation

​ Spring针对Bean Validation进行了扩展,将其校验注解扩展到单个请求参数之上了,这仅仅在Spring中起做用。

第一步:定义API接口,并在接口请求参数上添加校验注解
第二步:添加@Validated注解到API类上
@RestController
@RequestMapping("person")
@Validated
public class PersonApi {
    @RequestMapping("addPerson")
    public Person addPerson(@Valid final Person person){
        return person;
    }
    @RequestMapping("modifyPerson")
    public Person modifyPerson(@Validated({Default.class, ModifyPersonGroup.class}) final Person person){
        return person;
    }
    @RequestMapping("deletePerson")
    public String deletePerson(@NotNull(message = "删除时ID不能为null") final String id){
        return id;
    }
}

​ 页面请求:

http://localhost:8080/person/deletePerson

​ 页面显示:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Mon Nov 12 18:07:56 CST 2018
There was an unexpected error (type=Internal Server Error, status=500).
deletePerson.id: ???ID???null

​ 日志显示:

2018-11-12 18:07:56.073 ERROR 10676 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: deletePerson.id: 删除时ID不能为null] with root cause

​ 可见日志提示方式不同,Spring是采用MethodValidationPostProcessor后处理器进行校验的。

自定义校验注解

​ 当现有的校验注解没法知足咱们的业务需求的时候咱们能够尝试自定义校验注解,自定义有两种状况,一种是将原有的多个校验注解组合成为一个校验注解,这样免去了进行个多个注解的麻烦,另外一种状况就是彻底建立一种新的校验注解,来实现自定义的业务校验功能。

自定义组合注解

第一步:建立组合校验注解
public @interface ValidateGroup {    
}
第二步:为该注解添加必要的基础注解,并添加@Constraint注解,将该注解标记为Bean验证注解,其属性validatedBy置为{}
import javax.validation.Constraint;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Documented
@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
public @interface ValidateGroup {

}
第三步:为该注解添加子元素注解和必要的方法

​ 所谓子元素注解,指的是要组合的注解

import javax.validation.Constraint;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Documented
@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Max(150)
@Min(1)
public @interface ValidateGroup {
    @OverridesAttribute(constraint = Min.class, name = "value") long min() default 0;
    @OverridesAttribute(constraint = Max.class,name = "value") long max() default 150L;

    String message() default "组合注解校验不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}
第四步:为该注解添加List注解,以便实现同用。
import javax.validation.Constraint;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Documented
@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Max(150)
@Min(1)
@Repeatable(ValidateGroup.List.class)
@ReportAsSingleViolation
public @interface ValidateGroup {
    @OverridesAttribute(constraint = Min.class, name = "value") long min() default 0;
    @OverridesAttribute(constraint = Max.class,name = "value") long max() default 150L;

    String message() default "组合注解校验不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    @Documented
    @Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})
    @Retention(RUNTIME)
    public @interface List{
        ValidateGroup[] value();
    }
}

​ 至此完成该组合注解建立,诸多疑问下面一一罗列。

校验注解解析

​ 咱们仔细观察一个基础的校验注解,能够看到它被多个注解标注:

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })
@Repeatable(List.class)
public @interface Max {...}

​ 首先前三个注解你们都很熟悉,那是Java中注解的三大基础部件,不作解释,重点看多出来的两个注解。

@Constraint(validatedBy = { })

​ 这个注解是在JSR 303中定义的新注解,主要目的就是将一个注解标记为一个Bean Validation注解,其参数validatedBy 表示的是校验的逻辑类,即具体的校验逻辑所在类,这里置空是由于在JSR 303中并无实现校验逻辑类,而Hibernate Validation中对JSR 303中全部的校验注解的校验逻辑进行了实现。当咱们自定义建立新的校验注解的时候,就必需要手动实现ConstraintValidator接口,进行校验逻辑编写。

@Repeatable(List.class)

​ 这个注解表示该注解是能够重用的,里面的List也不是java中的集合List,而是定义在当前校验注解内部的一个内部注解@List,用于承载多个当前注解重用。

​ 而后咱们再看注解内部的各个方法定义:

message方法

​ message方法是每一个校验注解必备方法,主要用于设置校验失败的提示信息。该值能够直接在标注校验注解的时候自定义,若是不进行定义,那么将会采用默认的提示信息,这些信息都统一保存在hibernate-validator的jar包内的ValidationMessage.properties配置文件中。

​ 下面罗列一部分:

...
javax.validation.constraints.Max.message             = must be less than or equal to {value}
javax.validation.constraints.Min.message             = must be greater than or equal to {value}
javax.validation.constraints.Negative.message        = must be less than 0
javax.validation.constraints.NegativeOrZero.message  = must be less than or equal to 0
javax.validation.constraints.NotBlank.message        = must not be blank
javax.validation.constraints.NotEmpty.message        = must not be empty
javax.validation.constraints.NotNull.message         = must not be null
javax.validation.constraints.Null.message            = must be null
javax.validation.constraints.Past.message            = must be a past date
javax.validation.constraints.PastOrPresent.message   = must be a date in the past or in the present
javax.validation.constraints.Pattern.message         = must match "{regexp}"
...
groups方法

​ 这个方法时用来实现分组校验功能的,如前所述,在咱们定义好分组校验接口以后,咱们在Bean的字段上添加校验注解的时候,就能够设置groups属性的值为这个接口类,须要注意的是默认的Default.class分组,未进行手动分组的校验注解所有属于该分组,在接口Bean参数中启用分组校验的时候,若是须要进行默认分组的校验,还须要手动将Default.class添加到@Validated的分组设置中。

payload方法

​ 这个方法用于设置校验负载,何为负载?

​ 基于我的理解,我认为这个负载能够理解成为JSR 303为咱们在校验注解中提供的一个万能属性,咱们能够将其扩展为任何咱们想要定义的功能,好比咱们能够将其扩展为错误级别,在添加校验注解的时候用于区分该校验的级别,咱们能够将其扩展为错误类型,用于区分不一样类型的错误等,在JSR 303中定义了一种负载,值提取器,咱们先来看下这个负载定义:

/**
 * Payload type that can be attached to a given constraint declaration.
 * Payloads are typically used to carry on metadata information
 * consumed by a validation client.
 * With the exception of the {@link Unwrapping} payload types, the use of payloads 
 * is not considered portable.
 */
public interface Payload {
}
public interface Unwrapping {
    // Unwrap the value before validation.解包
    public interface Unwrap extends Payload {
    }
    // Skip the unwrapping if it has been enabled on the {@link ValueExtractor} by 
    // the UnwrapByDefault
    public interface Skip extends Payload {
    }
}

​ 有关payload的使用:咱们能够在执行校验的时候使用ConstraintViolation::getConstraintDescriptor::getPayload方法获取每个校验问题的payload设置,从而根据这个设置执行一些预约义的操做。

组合约束新增注解:

@ReportAsSingleViolation

​ 默认状况下,组合注解中的一个或多个子注解校验失败的状况下,会分别触发子注解各自错误报告,若是想要使用组合注解中定义的错误信息,则添加该注解。添加以后只要组合注解中有至少一个子注解校验失败,则会生成组合注解中定义的错误报告,子注解的错误信息被忽略。

@OverridesAttribute

​ 属性覆盖注解,其属性constraint用于指定要覆盖的属性所在的子注解类型,name用于指定要覆盖的属性的名称,好比此处:

@OverridesAttribute(constraint = Min.class, name = "value") long min() default 0;

​ 表示使用当前组合注解的min属性覆盖Min子注解的value属性。

@OverridesAttribute.List

​ 当有多个属性须要覆盖的时候可使用@OverridesAttribute.List。举例以下:

@OverridesAttribute.List( {
        @OverridesAttribute(constraint=Size.class, name="min"),
        @OverridesAttribute(constraint=Size.class, name="max") } )
    int size() default 5;

​ 可见该注解主要用于针对同一个子注解中的多个属性须要覆盖的状况。

自定义建立注解

不一样于以前的组合注解,建立注解须要彻底新建一个新的注解,与已有注解无关的注解。
第一步:建立注解,标注基本元注解
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})
public @interface NewValidation {
}
第二步:添加校验基础注解,和固定属性
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})
@Constraint(validatedBy = {})
@Repeatable(NewValidation.List.class)
public @interface NewValidation {

    String message() default "含有敏感内容!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    @Documented
    @Retention(RUNTIME)
    @Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})
    public @interface List{
        NewValidation[] value();
    }
}
第三步:添加额外属性,可省略
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})
@Constraint(validatedBy = {NewValidator.class})

@Repeatable(NewValidation.List.class)
public @interface NewValidation {
    String[] value() default {"111","222","333"};
    String message() default "含有敏感内容!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    @Documented
    @Retention(RUNTIME)
    @Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})
    public @interface List{
        NewValidation[] value();
    }
}
额外属性通常用做判断的基础条件设置,若是不须要能够不添加该属性。

至此一个简单的校验注解完成了,下面是重点,实现校验逻辑:
@Component
public class NewValidator implements ConstraintValidator<NewValidation, CharSequence> {

    private String[] value;

    @Override
    public void initialize(NewValidation constraintAnnotation) {
        this.value = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if(value == null || value.length() == 0) {
            return true;
        }
        for(String s :Arrays.asList(this.value)) {
            if(value.toString().contains(s)) {
                return false;
            }
        }
        return true;
    }
}

注意:

  • 自定义新建的校验注解都须要手动实现校验逻辑,这个校验逻辑实现类须要配置到校验注解的@Constraint(validatedBy = {NewValidator.class})注解中去,将两者关联起来。
  • 校验逻辑须要实现ConstraintValidator接口,这个接口是一个泛型接口,接收一个关联校验注解类型A和一个校验目标类型T。
  • 咱们须要实现接口中的两个方法initialize和isValid。前者用于内部初始化,通常就是将要校验的目标内容获取到,后者主要就是完成校验逻辑了。
咱们测试自定义的两个注解:
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Person {

    @NewValidation(value = {"浩哥","浩妹"})
    private String name;

    @ValidateGroup(min = 1)
    private int age;

}
@RestController
@RequestMapping("person")

public class PersonApi {

    @RequestMapping("addPerson")
    public Person addPerson(@Valid final Person person){
        return person;
    }

}
浏览器发起请求:
http://localhost:8080/person/addPerson?name=惟一浩哥
页面提示:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Tue Nov 13 14:34:18 CST 2018
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='person'. Error count: 2
日志提示:
2018-11-13 14:34:18.727  WARN 11472 --- [nio-8080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'person' on field 'age': rejected value [0]; codes [ValidateGroup.person.age,ValidateGroup.age,ValidateGroup.int,ValidateGroup]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.age,age]; arguments []; default message [age],150,1]; default message [组合注解校验不正确]
Field error in object 'person' on field 'name': rejected value [惟一浩哥]; codes [NewValidation.person.name,NewValidation.name,NewValidation.java.lang.String,NewValidation]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.name,name]; arguments []; default message [name],[Ljava.lang.String;@1100068d]; default message [含有敏感内容!]]
因而可知,两个自定义校验所有生效。当咱们修改正确以后再请求时,没有错误报告。
http://localhost:8080/person/addPerson?name=weiyihaoge&age=30
页面结果:
{"name":"weiyihaoge","age":30}

校验结果的处理

说了这么多,咱们看到例子中校验结果咱们都没有进行任何处理,这一节咱们简单介绍如何处理校验结果。

其实咱们在使用spring进行开发的时候,要么开发的是restful接口,要么是前端控制器,前者通常用于先后端分离的开发模式,或者微服务开发模式,后者则通常用于小型项目中先后端不分离的开发模式,前者的状况下,咱们能够不对结果进行处理,它会自动抛出异常,后者的状况,则必需要进行处理,毕竟,咱们多是须要将校验结果返回前端页面的。

咱们如何在控制器中处理校验结果呢?咱们须要一个校验结果的承接器,当发生校验失败时,将结果放到这个承接器中,咱们再针对这个承接器进行处理便可。Spring中这个承接器就是BindingResult。例以下面这样:
@RequestMapping("addPerson2")

    public List<String> addPerson(@Validated final Person person, BindingResult result) {

        if(result.hasErrors()) {
            List<ObjectError> errorList = result.getAllErrors();
            List<String> messageList = new ArrayList<>();
            errorList.forEach(e -> messageList.add(e.getDefaultMessage()));
            return messageList;
        }
        return null;
    }
页面发起请求:
http://localhost:8080/person/addPerson2?name=惟一浩哥
页面结果:
["含有敏感内容!","组合注解校验不正确"]

注意:

在使用BingingResult承接校验结果进行处理的时候,添加在Bean前方的校验启动注解要是用Spring提供的@Validated,而不能使用JSR 303提供的@Valid。使用后者仍是会正常抛出异常。由此咱们在进行控制器开发的时候一概使用@Validated便可。

备注:

Java数据校验详解

Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC

Spring3.1 对Bean Validation规范的新支持(方法级别验证)

SpringMVC数据验证——第七章 注解式控制器的数据验证、类型转换及格式化——跟着开涛学SpringMVC

相关文章
相关标签/搜索