原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9953744.html
Spring中使用参数校验
概述
 JSR 303中提出了Bean Validation,表示JavaBean的校验,Hibernate Validation是其具体实现,并对其进行了一些扩展,添加了一些实用的自定义校验注解。
 Spring中集成了这些内容,你可以在Spring中以原生的手段来使用校验功能,当然Spring也对其进行了一点简单的扩展,以便其更适用于Java web的开发。
 就我所知,Spring中添加了BindingResult用于接收校验结果,同时添加了针对方法中单个请求参数的校验功能,这个功能等于扩展了JSR 303的校验注解的使用范围,使其不再仅仅作用于Bean中的属性,而是能够作用于单一存在的参数。
JSR 303 Bean Validation
 JSR 303中提供了诸多实用的校验注解,这里简单罗列:
注解	说明	备注
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,拥有其所有功能,并对其进行了扩展,它自定义了以下校验注解:
注解	说明	备注
Length(min,max)	标注元素的长度必须在指定范围之内,包含最大值	字符串
Range(min,max)	标注元素值必须在指定范围之内	数字值,或者其字符串形式
URL(regexp,flags)	标注元素必须为格式正确的URL	字符串
URL(protocol,host,port)	标注元素必须满足给定的协议主机和端口号	字符串
Spring开发中使用参数校验
Spring中Bean Validation
 在Spring中进行Bean Validation有两种情况:
单组Bean Validation
 所谓单组就是不分组,或者只有一组,在底层就是Default.class代表的默认组。
 使用单组校验是最简单的,下面看看实现步骤:
第一步:创建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.C