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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM