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

相關文章
相關標籤/搜索