淺談自定義校驗(yàn)注解ConstraintValidator
一、前言
系統(tǒng)執(zhí)行業(yè)務(wù)邏輯之前,會(huì)對(duì)輸入數(shù)據(jù)進(jìn)行校驗(yàn),檢測(cè)數(shù)據(jù)是否有效合法的。所以我們可能會(huì)寫大量的if else等判斷邏輯,特別是在不同方法出現(xiàn)相同的數(shù)據(jù)時(shí),校驗(yàn)的邏輯代碼會(huì)反復(fù)出現(xiàn),導(dǎo)致代碼冗余,閱讀性和可維護(hù)性極差。
JSR-303是Java為Bean數(shù)據(jù)合法性校驗(yàn)提供的標(biāo)準(zhǔn)框架,它定義了一整套校驗(yàn)注解,可以標(biāo)注在成員變量,屬性方法等之上。
hibernate-validator就提供了這套標(biāo)準(zhǔn)的實(shí)現(xiàn),我們?cè)谟肧pringboot開發(fā)web應(yīng)用時(shí),會(huì)引入spring-boot-starter-web依賴,它默認(rèn)會(huì)引入spring-boot-starter-validation依賴,而spring-boot-starter-validation中就引用了hibernate-validator依賴。
但是,在比較高版本的spring-boot-starter-web中,默認(rèn)不再引用spring-boot-starter-validation,自然也就不會(huì)默認(rèn)引入到hibernate-validator依賴,需要我們手動(dòng)添加依賴。
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.1.7.Final</version> </dependency>
hibernate-validator中有很多非常簡(jiǎn)單好用的校驗(yàn)注解,例如NotNull,@NotEmpty,@Min,@Max,@Email,@PositiveOrZero等等。這些注解能解決我們大部分的數(shù)據(jù)校驗(yàn)問題。如下所示:
package com.nobody.dto; import lombok.Data; import javax.validation.constraints.*; @Data public class UserDTO { @NotBlank(message = "姓名不能為空") private String name; @Min(value = 18, message = "年齡不能小于18") private int age; @NotEmpty(message = "郵箱不能為空") @Email(message = "郵箱格式不正確") private String email; }
二、自定義參數(shù)校驗(yàn)器
但是,hibernate-validator中的這些注解不一定能滿足我們?nèi)康男枨?,我們想校?yàn)的邏輯比這復(fù)雜。所以,我們可以自定義自己的參數(shù)校驗(yàn)器。
首先引入依賴是必不可少的。
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.1.7.Final</version> </dependency>
最近不是基金很火嗎,一大批的韭菜瘋狂地涌入買基金的浪潮中。我就以用戶開戶為例,首先要校驗(yàn)此用戶是不是成年人(即不能小于18歲),以及名字是不是以"新韭菜"開頭的,符合條件的才允許開戶。
定義一個(gè)注解,用于校驗(yàn)用戶的姓名是不是以“新韭菜”開頭的。
package com.nobody.annotation; import com.nobody.validator.IsLeekValidator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented @Constraint(validatedBy = IsLeekValidator.class) // 指定我們自定義的校驗(yàn)類 public @interface IsLeek { /** * 是否強(qiáng)制校驗(yàn) * * @return 是否強(qiáng)制校驗(yàn)的boolean值 */ boolean required() default true; /** * 校驗(yàn)不通過時(shí)的報(bào)錯(cuò)信息 * * @return 校驗(yàn)不通過時(shí)的報(bào)錯(cuò)信息 */ String message() default "此用戶不是韭零后,無法開戶!"; /** * 將validator進(jìn)行分類,不同的類group中會(huì)執(zhí)行不同的validator操作 * * @return validator的分類類型 */ Class<?>[] groups() default {}; /** * 主要是針對(duì)bean,很少使用 * * @return 負(fù)載 */ Class<? extends Payload>[] payload() default {}; }
定義校驗(yàn)類,實(shí)現(xiàn)ConstraintValidator接口,接口使用了泛型,需要指定兩個(gè)參數(shù),第一個(gè)是自定義注解,第二個(gè)是需要校驗(yàn)的數(shù)據(jù)類型。重寫2個(gè)方法,initialize方法主要做一些初始化操作,它的參數(shù)是我們使用到的注解,可以獲取到運(yùn)行時(shí)的注解信息。isValid方法就是要實(shí)現(xiàn)的校驗(yàn)邏輯,被注解的對(duì)象會(huì)傳入此方法中。
package com.nobody.validator; import com.nobody.annotation.IsLeek; import org.springframework.util.StringUtils; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class IsLeekValidator implements ConstraintValidator<IsLeek, String> { // 是否強(qiáng)制校驗(yàn) private boolean required; @Override public void initialize(IsLeek constraintAnnotation) { this.required = constraintAnnotation.required(); } @Override public boolean isValid(String name, ConstraintValidatorContext constraintValidatorContext) { if (required) { // 名字以"新韭菜"開頭的則校驗(yàn)通過 return !StringUtils.isEmpty(name) && name.startsWith("新韭菜"); } return false; } }
三、使用自定義注解
通過以上幾個(gè)步驟,我們自定義的校驗(yàn)注解就完成了,我們使用測(cè)試下效果。
package com.nobody.dto; import com.nobody.annotation.IsLeek; import lombok.Data; import javax.validation.constraints.*; @Data public class UserDTO { @NotBlank(message = "姓名不能為空") @IsLeek // 我們自定義的注解 private String name; @Min(value = 18, message = "年齡不能小于18") private int age; @NotEmpty(message = "郵箱不能為空") @Email(message = "郵箱格式不正確") private String email; }
寫個(gè)接口,模擬用戶開戶業(yè)務(wù),調(diào)用測(cè)試。注意,記得加上@Valid注解開啟校驗(yàn),不然不生效。
package com.nobody.controller; import com.nobody.dto.UserDTO; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @RestController @RequestMapping("user") public class UserController { @PostMapping("add") public UserDTO add(@RequestBody @Valid UserDTO userDTO) { System.out.println(">>> 用戶開戶成功..."); return userDTO; } }
如果參數(shù)校驗(yàn)不通過,會(huì)拋出MethodArgumentNotValidException異常,我們?nèi)痔幚硐氯缓蠓祷亟o接口。
package com.nobody.exception; import javax.servlet.http.HttpServletRequest; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import lombok.extern.slf4j.Slf4j; @ControllerAdvice @Slf4j public class GlobalExceptionHandler { // 處理接口參數(shù)數(shù)據(jù)格式錯(cuò)誤異常 @ExceptionHandler(value = MethodArgumentNotValidException.class) @ResponseBody public Object errorHandler(HttpServletRequest request, MethodArgumentNotValidException e) { return e.getBindingResult().getAllErrors(); } }
我們先測(cè)試用戶姓名不帶"新韭菜"前綴的進(jìn)行測(cè)試,發(fā)現(xiàn)校驗(yàn)不通過,證明注解生效了。
POST http://localhost:8080/user/add
Content-Type: application/json
{"name": "小綠", "age": 19, "email": "845136542@qq.com"}
[ { "codes": [ "IsLeek.userDTO.name", "IsLeek.name", "IsLeek.java.lang.String", "IsLeek" ], "arguments": [ { "codes": [ "userDTO.name", "name" ], "arguments": null, "defaultMessage": "name", "code": "name" }, true ], "defaultMessage": "此用戶不是韭零后,無法開戶!", "objectName": "userDTO", "field": "name", "rejectedValue": "小綠", "bindingFailure": false, "code": "IsLeek" }
如果多個(gè)參數(shù)校驗(yàn)失敗,報(bào)錯(cuò)信息也都能獲得。如下所示,姓名和郵箱都校驗(yàn)失敗。
POST http://localhost:8080/user/add
Content-Type: application/json
{"name": "小綠", "age": 19, "email": "84513654"}
[ { "codes": [ "Email.userDTO.email", "Email.email", "Email.java.lang.String", "Email" ], "arguments": [ { "codes": [ "userDTO.email", "email" ], "arguments": null, "defaultMessage": "email", "code": "email" }, [], { "defaultMessage": ".*", "codes": [ ".*" ], "arguments": null } ], "defaultMessage": "郵箱格式不正確", "objectName": "userDTO", "field": "email", "rejectedValue": "84513654", "bindingFailure": false, "code": "Email" }, { "codes": [ "IsLeek.userDTO.name", "IsLeek.name", "IsLeek.java.lang.String", "IsLeek" ], "arguments": [ { "codes": [ "userDTO.name", "name" ], "arguments": null, "defaultMessage": "name", "code": "name" }, true ], "defaultMessage": "此用戶不是韭零后,無法開戶!", "objectName": "userDTO", "field": "name", "rejectedValue": "小綠", "bindingFailure": false, "code": "IsLeek" } ]
以下是所有參數(shù)校驗(yàn)通過的情況:
POST http://localhost:8080/user/add
Content-Type: application/json
{"name": "新韭菜小綠", "age": 19, "email": "84513654@qq.com"}
{
"name": "新韭菜小綠",
"age": 19,
"email": "84513654@qq.com"
}
我們可能會(huì)將UserDTO對(duì)象用在不同的接口中接收參數(shù),比如在新增和修改接口中。在新增接口中,不需要校驗(yàn)userId;在修改接口中需要校驗(yàn)userId。那注解中的groups字段就派上用場(chǎng)了。groups和@Validated配合能控制哪些注解需不需要開啟校驗(yàn)。
我們首先定義2個(gè)groups分組接口Update和Create,并且繼承Default接口。當(dāng)然也可以不繼承Default接口,因?yàn)槭褂米⒔鈺r(shí)不顯示指定groups的值,則默認(rèn)為groups = {Default.class}。所以繼承了Default接口,在用@Validated(Create.class)時(shí),也會(huì)校驗(yàn)groups = {Default.class}的注解。
package com.nobody.annotation; import javax.validation.groups.Default; public interface Create extends Default { }
package com.nobody.annotation; import javax.validation.groups.Default; public interface Update extends Default { }
在用到注解的地方,填寫groups的值。
package com.nobody.dto; import com.nobody.annotation.Create; import com.nobody.annotation.IsLeek; import com.nobody.annotation.Update; import lombok.Data; import javax.validation.constraints.*; @Data public class UserDTO { @NotBlank(message = "用戶ID不能為空", groups = Update.class) private String userId; @NotBlank(message = "姓名不能為空", groups = {Update.class, Create.class}) @IsLeek private String name; @Min(value = 18, message = "年齡不能小于18") private int age; @NotEmpty(message = "郵箱不能為空") @Email(message = "郵箱格式不正確") private String email; }
最后,在需要聲明校驗(yàn)的地方,通過@Validated的指定即可。
package com.nobody.controller; import com.nobody.annotation.Create; import com.nobody.annotation.Update; import com.nobody.dto.UserDTO; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("user") public class UserController { @PostMapping("add") public Object add(@RequestBody @Validated(Create.class) UserDTO userDTO) { System.out.println(">>> 用戶開戶成功..."); return userDTO; } @PostMapping("update") public Object update(@RequestBody @Validated(Update.class) UserDTO userDTO) { System.out.println(">>> 用戶信息修改成功..."); return userDTO; } }
調(diào)用add接口時(shí),即使不傳userId也能通過,即不對(duì)userId進(jìn)行校驗(yàn)。
POST http://localhost:8080/user/add
Content-Type: application/json
{"name": "新韭菜小綠", "age": 18, "email": "84513654@qq.com"}
調(diào)用update接口時(shí),不傳userId,會(huì)校驗(yàn)不通過。
POST http://localhost:8080/user/update
Content-Type: application/json
{"name": "新韭菜小綠", "age": 18, "email": "84513654@qq.com"}
[ { "codes": [ "NotBlank.userDTO.userId", "NotBlank.userId", "NotBlank.java.lang.String", "NotBlank" ], "arguments": [ { "codes": [ "userDTO.userId", "userId" ], "arguments": null, "defaultMessage": "userId", "code": "userId" } ], "defaultMessage": "用戶ID不能為空", "objectName": "userDTO", "field": "userId", "rejectedValue": null, "bindingFailure": false, "code": "NotBlank" } ]
此演示項(xiàng)目已上傳到Github,如有需要可自行下載,歡迎 Star 。 https://github.com/LucioChn/spring
以上就是淺談自定義校驗(yàn)注解ConstraintValidator的詳細(xì)內(nèi)容,更多關(guān)于自定義校驗(yàn)注解ConstraintValidator的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java基本數(shù)據(jù)類型與類型轉(zhuǎn)換實(shí)例分析
這篇文章主要介紹了Java基本數(shù)據(jù)類型與類型轉(zhuǎn)換,結(jié)合實(shí)例形式分析了Java基本數(shù)據(jù)類型分類、用法,類型轉(zhuǎn)換及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2020-04-04對(duì)Jpa中Entity關(guān)系映射中mappedBy的全面理解
這篇文章主要介紹了對(duì)Jpa中Entity關(guān)系映射中mappedBy的全面理解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12Mac安裝多個(gè)JDK并實(shí)現(xiàn)動(dòng)態(tài)切換
有時(shí)候我們有多個(gè)項(xiàng)目需要使用多個(gè)版本JDK,本文主要介紹了Mac安裝多個(gè)JDK并實(shí)現(xiàn)動(dòng)態(tài)切換,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07Java 方法引用與ambda表達(dá)式的聯(lián)系
這篇文章主要介紹了Java 方法引用與ambda表達(dá)式的聯(lián)系,方法引用通過方法的名字來指向一個(gè)方法, 方法引用同樣是Java 8 引入的新特性,而且和Lambda表達(dá)式有著不小的聯(lián)系,它同樣可以根據(jù)上下文進(jìn)行推導(dǎo),進(jìn)而可以簡(jiǎn)化代碼2022-06-06mybatis-plus(insertBatchSomeColumn批量添加方式)
這篇文章主要介紹了mybatis-plus(insertBatchSomeColumn批量添加方式),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03Java從JDK源碼角度對(duì)Object進(jìn)行實(shí)例分析
這篇文章主要介紹了Java從JDK源碼角度對(duì)Object進(jìn)行實(shí)例分析,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12比較Java數(shù)組和各種List的性能小結(jié)
這篇文章主要是分別對(duì)Java數(shù)組、ArrayList、LinkedList和Vector進(jìn)行隨機(jī)訪問和迭代等操作,并比較這種集合的性能。有需要的可以參考借鑒。2016-08-08SpringBoot打印POST請(qǐng)求原始入?yún)ody體方式
這篇文章主要介紹了SpringBoot打印POST請(qǐng)求原始入?yún)ody體方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09利用Redis實(shí)現(xiàn)延時(shí)處理的方法實(shí)例
這篇文章主要給大家介紹了關(guān)于利用Redis實(shí)現(xiàn)延時(shí)處理的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用Redis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03