唯一浩哥

架构之路----Java有不少事

Spring基础系列-参数校验

原创作品,可以转载,但是请标注出处地址: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.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

posted @ 2018-11-13 18:24  唯一浩哥  阅读(5932)  评论(0编辑  收藏  举报