1. 程式人生 > >Spring基礎系列-引數校驗

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.valid[email protected]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.valid[email protected]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