1. 程式人生 > >自定義校驗註解ConstraintValidator

自定義校驗註解ConstraintValidator

# 一 前言 系統執行業務邏輯之前,會對輸入資料進行校驗,檢測資料是否有效合法的。所以我們可能會寫大量的if else等判斷邏輯,特別是在不同方法出現相同的資料時,校驗的邏輯程式碼會反覆出現,導致程式碼冗餘,閱讀性和可維護性極差。 鑑於通用性和普遍性,Spring框架提供了validator元件,通過一些校驗器,可以對一些資料進行統一的完整性和有效性等校驗,即簡單又好用。 JSR-303是Java為Bean資料合法性校驗提供的標準框架,它定義了一整套校驗註解,可以標註在成員變數,屬性方法等之上。 hibernate-validator就提供了這套標準的實現,我們在用Springboot開發web應用時,會引入spring-boot-starter-web依賴,它預設會引入spring-boot-starter-validation依賴,而spring-boot-starter-validation中就引用了hibernate-validator依賴。 ![](https://img-blog.csdnimg.cn/20210313160505969.png) 但是,在比較高版本的spring-boot-starter-web中,預設不再引用spring-boot-starter-validation,自然也就不會預設引入到hibernate-validator依賴,需要我們手動新增依賴。 ```xml org.hibernate.validator hibernate-validator 6.1.7.Final ``` hibernate-validator中有很多非常簡單好用的校驗註解,例如NotNull,@NotEmpty,@Min,@Max,@Email,@PositiveOrZero等等。這些註解能解決我們大部分的資料校驗問題。如下所示: ```java 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; } ``` # 二 自定義引數校驗器 但是,hibernate-validator中的這些註解不一定能滿足我們全部的需求,我們想校驗的邏輯比這複雜。所以,我們可以自定義自己的引數校驗器。 首先引入依賴是必不可少的。 ```xml org.hibernate.validator
hibernate-validator 6.1.7.Final
``` 最近不是基金很火嗎,一大批的韭菜瘋狂地湧入買基金的浪潮中。我就以使用者開戶為例,首先要校驗此使用者是不是成年人(即不能小於18歲),以及名字是不是以"新韭菜"開頭的,符合條件的才允許開戶。 定義一個註解,用於校驗使用者的姓名是不是以“新韭菜”開頭的。 ```java package com.nobody.annotation; import com.nobody.validator.IsLeekValidator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; /** * @Description 校驗是否韭菜的註解 * @Author Mr.nobody * @Date 2021/3/11 * @Version 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented @Constraint(validatedBy = IsLeekValidator.class) // 指定我們自定義的校驗類 public @interface IsLeek { /** * 是否強制校驗 * * @return 是否強制校驗的boolean值 */ boolean required() default true; /** * 校驗不通過時的報錯資訊 * * @return 校驗不通過時的報錯資訊 */ String message() default "此使用者不是韭零後,無法開戶!"; /** * 將validator進行分類,不同的類group中會執行不同的validator操作 * * @return validator的分類型別 */ Class[] groups() default {}; /** * 主要是針對bean,很少使用 * * @return 負載 */ Class[] payload() default {}; } ``` 定義校驗類,實現ConstraintValidator介面,介面使用了泛型,需要指定兩個引數,第一個是自定義註解,第二個是需要校驗的資料型別。重寫2個方法,initialize方法主要做一些初始化操作,它的引數是我們使用到的註解,可以獲取到執行時的註解資訊。isValid方法就是要實現的校驗邏輯,被註解的物件會傳入此方法中。 ```java package com.nobody.validator; import com.nobody.annotation.IsLeek; import org.springframework.util.StringUtils; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; /** * @Description 自定義校驗器 * @Author Mr.nobody * @Date 2021/3/11 * @Version 1.0 */ public class IsLeekValidator implements ConstraintValidator { // 是否強制校驗 private boolean required; @Override public void initialize(IsLeek constraintAnnotation) { this.required = constraintAnnotation.required(); } @Override public boolean isValid(String name, ConstraintValidatorContext constraintValidatorContext) { if (required) { // 名字以"新韭菜"開頭的則校驗通過 return !StringUtils.isEmpty(name) && name.startsWith("新韭菜"); } return false; } } ``` # 三 使用自定義註解 通過以上幾個步驟,我們自定義的校驗註解就完成了,我們使用測試下效果。 ```java package com.nobody.dto; import com.nobody.annotation.IsLeek; import lombok.Data; import javax.validation.constraints.*; /** * @Description * @Author Mr.nobody * @Date 2021/3/11 * @Version 1.0 */ @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; } ``` 寫個介面,模擬使用者開戶業務,呼叫測試。注意,記得加上@Valid註解開啟校驗,不然不生效。 ```java 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; /** * @Description * @Author Mr.nobody * @Date 2021/3/11 * @Version 1.0 */ @RestController @RequestMapping("user") public class UserController { @PostMapping("add") public UserDTO add(@RequestBody @Valid UserDTO userDTO) { System.out.println(">>> 使用者開戶成功..."); return userDTO; } } ``` 如果引數校驗不通過,會丟擲MethodArgumentNotValidException異常,我們全域性處理下然後返回給介面。 ```java 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; /** * @Description 統一異常處理 * @Author Mr.nobody * @Date 2020/10/23 * @Version 1.0 */ @ControllerAdvice @Slf4j public class GlobalExceptionHandler { // 處理介面引數資料格式錯誤異常 @ExceptionHandler(value = MethodArgumentNotValidException.class) @ResponseBody public Object errorHandler(HttpServletRequest request, MethodArgumentNotValidException e) { return e.getBindingResult().getAllErrors(); } } ``` 我們先測試使用者姓名不帶"新韭菜"字首的進行測試,發現校驗不通過,證明註解生效了。 ```bash POST http://localhost:8080/user/add Content-Type: application/json {"name": "小綠", "age": 19, "email": "[email protected]"} ``` ```bash [ { "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" } ``` 如果多個引數校驗失敗,報錯資訊也都能獲得。如下所示,姓名和郵箱都校驗失敗。 ```bash POST http://localhost:8080/user/add Content-Type: application/json {"name": "小綠", "age": 19, "email": "84513654"} ``` ```bash [ { "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" } ] ``` 以下是所有引數校驗通過的情況: ```bash POST http://localhost:8080/user/add Content-Type: application/json {"name": "新韭菜小綠", "age": 19, "email": "[email protected]"} ``` ```bash { "name": "新韭菜小綠", "age": 19, "email": "[email protected]" } ``` 我們可能會將UserDTO物件用在不同的介面中接收引數,比如在新增和修改介面中。在新增介面中,不需要校驗userId;在修改介面中需要校驗userId。那註解中的groups欄位就派上用場了。groups和@Validated配合能控制哪些註解需不需要開啟校驗。 我們首先定義2個groups分組介面Update和Create,並且繼承Default介面。當然也可以不繼承Default介面,因為使用註解時不顯示指定groups的值,則預設為groups = {Default.class}。所以繼承了Default介面,在用@Validated(Create.class)時,也會校驗groups = {Default.class}的註解。 ```java package com.nobody.annotation; import javax.validation.groups.Default; /** * @Description * @Author Mr.nobody * @Date 2021/3/13 * @Version 1.0 */ public interface Create extends Default { } ``` ```java package com.nobody.annotation; import javax.validation.groups.Default; /** * @Description * @Author Mr.nobody * @Date 2021/3/13 * @Version 1.0 */ public interface Update extends Default { } ``` 在用到註解的地方,填寫groups的值。 ```java 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.*; /** * @Description * @Author Mr.nobody * @Date 2021/3/11 * @Version 1.0 */ @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; } ``` 最後,在需要宣告校驗的地方,通過@Validated的指定即可。 ```java 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; /** * @Description * @Author Mr.nobody * @Date 2021/3/11 * @Version 1.0 */ @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; } } ``` 呼叫add介面時,即使不傳userId也能通過,即不對userId進行校驗。 ```java POST http://localhost:8080/user/add Content-Type: application/json {"name": "新韭菜小綠", "age": 18, "email": "[email protected]"} ``` ```java { "userId": null, "name": "新韭菜小綠", "age": 18, "email": "[email protected]" } ``` 呼叫update介面時,不傳userId,會校驗不通過。 ```java POST http://localhost:8080/user/update Content-Type: application/json {"name": "新韭菜小綠", "age": 18, "email": "[email protected]"} ``` ```java [ { "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" } ] ``` > 此演示專案已上傳到Github,如有需要可自行下載,歡迎 Star 。 [https://github.com/LucioChn/spring](https://github.com/LucioChn/spring) ![](https://img-blog.csdnimg.cn/20210313155448624.png#pic_