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

Spring基礎系列-參數校驗

nsh 們的 res 誤報 內部 主頁 定位 person big

原創作品,可以轉載,但是請標註出處地址: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

Spring基礎系列-參數校驗