1. 程式人生 > >spring-boot 使用hibernate validation對引數進行優雅的校驗

spring-boot 使用hibernate validation對引數進行優雅的校驗

springboot天生支援使用hibernate validation對引數的優雅校驗,如果不使用它,只能對引數挨個進行如下方式的手工校驗,不僅難看,使用起來還很不方便: ``` java if(StringUtils.isEmpty(userName)){ throw new RuntimeException("使用者名稱不能為空"); } ``` 下面將介紹hibernate validation的基本使用方法。 ## 一、引入依賴 這裡在springboot 2.4.1中進行實驗,引入以下依賴: ``` xml org.springframework.boot spring-boot-starter-parent 2.4.1 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.projectlombok lombok 1.18.16
org.hibernate.validator hibernate-validator 6.1.6.Final
``` ## 二、基本請求引數校驗 如下的一個spring mvc的請求呼叫中有一個id引數(Integer型別),如果不允許它為空,該怎麼做 1. 在Controller上加上`@Validated`註解 2. 在需要校驗的欄位前面加上`@NotNull(message = "使用者id不能為空")`註解 3. 定義全域性異常處理類,定製化返回結果 ``` java @RestControllerAdvice @Slf4j public class ValidationAdvice { @ExceptionHandler(Exception.class) @ResponseBody public WrapperResult handler(Exception e) { //獲取異常資訊,獲取異常堆疊的完整異常資訊 StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); //日誌輸出異常詳情 log.error(sw.toString()); return WrapperResult.faild("服務異常,請稍後再試"); } @ExceptionHandler(ConstraintViolationException.class) @ResponseBody public WrapperResult handler(ConstraintViolationException e) { StringBuffer errorMsg = new StringBuffer(); Set> violations = e.getConstraintViolations(); violations.forEach(x -> errorMsg.append(x.getMessage()).append(";")); return WrapperResult.faild(errorMsg.toString()); } } ``` Controller層程式碼如下所示: ``` java @RestController @Slf4j @RequestMapping("/user") @Validated public class UserController { /** * 根據id查詢使用者資訊 * * @param id * @return */ @GetMapping public WrapperResult findUser(@NotNull(message = "使用者id不能為空") @RequestParam(value = "id") String id) { return WrapperResult.success(new UserModel()); } } ``` 如果發起請求`127.0.0.1:8080/user?id=` 則會返回結果 ``` json { "status": 1, "data": "使用者id不能為空;", "msg": "FAIL", "success": false } ``` ## 三、物件內參數校驗 上面是GET請求,下面介紹POST請求,請求物件內的引數校驗。 ### 1.Controller類上加上@Validated註解 ``` java @RestController @Slf4j @RequestMapping("/user") **@Validated** public class UserController { } ``` ### 2.在POST請求方法引數前面加上`@Validated `註解 ``` java @PostMapping("/mobile-regist") public WrapperResult mobileRegit(@Validated @RequestBody UserModel userModel) { return WrapperResult.success(true); } ``` ### 3.在上面介紹的`ValidationAdvice`類中加上物件引數校驗異常捕獲 ``` java //處理校驗異常,對於物件型別的資料的校驗異常 @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public WrapperResult handler(MethodArgumentNotValidException e) { StringBuffer sb = new StringBuffer(); List allErrors = e.getBindingResult().getAllErrors(); allErrors.forEach(msg -> sb.append(msg.getDefaultMessage()).append(";")); return WrapperResult.faild(sb.toString()); } ``` UserModel類的定義如下: ``` java @Data @Builder @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) public class UserModel { @NotEmpty(message = "姓名不能為空") private String name; @NotEmpty(message = "手機號不能為空") // @Mobile(message = "手機號格式不正確") private String mobile; @NotEmpty(message = "電子郵箱不能為空") @Email(message = "電子郵箱格式不正確") private String email; private String password; private String address; @NotNull(message = "年齡不能為空") @Min(value = 12, message = "允許註冊年齡最小為12歲") @Max(value = 24, message = "允許年齡最大為24歲") private Integer age; @NotEmpty(message = "聯絡人不允許為空") @Size(min = 1, max = 3, message = "聯絡人長度只允許1到3之間") private List contacts; } ``` 如果POST請求如下所示 ``` json { "name":"", "mobile":"12666666666", "email":"", "password":"", "address":"", "age": null, "contacts":[ ] } ``` 則會返回如下定製化返回結果: ``` json { "status": 1, "data": "電子郵箱不能為空;聯絡人長度只允許1到3之間;年齡不能為空;聯絡人不允許為空;姓名不能為空;手機號格式不正確;", "msg": "FAIL", "success": false } ``` ## 四、自定義校驗器 像是@NotNull、@Email等註解都是hibernate validation 內建的註解,我們想開發像是@Email註解一樣功能的註解,如何做呢,比如@Mobile,它的使用方法將和@Email一模一樣。 首先,先定義一個工具類存放`ValidationUtil`兩個常量值 ``` java public class ValidationUtil { //手機號校驗正則 public static final String MOBILE_REGX = "^[1][3-9][0-9]{9}$"; public static final String MOBILE_MSG = "手機號格式錯誤"; } ``` ### 1.定義註解`Mobile` 具體程式碼可以參考@Email的實現,直接將Email名字改成Mobile即可,如下所示: ``` java @Documented @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) public @interface Mobile { String message() default ValidationUtil.MOBILE_MSG; Class[] groups() default {}; Class[] payload() default {}; String regexp() default ValidationUtil.MOBILE_REGX; Pattern.Flag[] flags() default {}; @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Documented public @interface List { Mobile[] value(); } } ``` ### 2.定義`MobileValidator`實現對引數的校驗邏輯 ``` java public class MobileValidator implements ConstraintValidator { private String regexp; @Override public void initialize(Mobile constraintAnnotation) { //獲取校驗的手機號的格式 this.regexp = constraintAnnotation.regexp(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (!StringUtils.hasText(value)) { return true; } return value.matches(regexp); } } ``` ### 3.使用方法和`@Email`一模一樣 不贅述 ## 五、分組校驗 假設一個使用者註冊的場景,使用者註冊有三種方式 1. 使用者名稱+圖形驗證碼註冊 2. 郵箱+郵箱驗證碼註冊 3. 手機號+簡訊驗證碼註冊 使用者註冊的時候除了方式不一樣,其他使用者資訊基本相同,後端開了三個介面對應著著三種註冊方式,請求體中我們使用一個Model封裝了以上所有資訊,包含著使用者名稱、郵箱、手機號等資訊,這時候不同的介面被呼叫,model中需要校驗的引數就不一樣了: 使用者名稱註冊的時候郵箱地址和手機號可以為空,但是使用者名稱不能為空;通過郵箱註冊的時候,郵箱地址不能為空,但是使用者名稱和手機號可以為空;...... 分組校驗專門應對這種情況。 ### 1.首先定義三個介面,表示三種組類別 ``` java public interface ValidEmail { } public interface ValidMobile { } public interface ValidUserName { } ``` ### 2.在UserModel實體類上指名組類別 ``` java @Data @Builder @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) public class UserModel { @NotEmpty(message = "姓名不能為空", groups = {ValidUserName.class}) @UserName(groups = {ValidUserName.class}) private String name; @NotEmpty(message = "手機號不能為空", groups = {ValidMobile.class}) @Mobile(groups = {ValidMobile.class}) private String mobile; @NotEmpty(message = "電子郵箱不能為空", groups = {ValidEmail.class}) @Email(message = "電子郵箱格式不正確", groups = {ValidEmail.class}) private String email; private String password; private String address; @NotNull(message = "年齡不能為空") @Min(value = 12, message = "允許註冊年齡最小為12歲", groups = {ValidEmail.class,ValidMobile.class,ValidUserName.class}) @Max(value = 24, message = "允許年齡最大為24歲",groups = {ValidEmail.class,ValidMobile.class,ValidUserName.class}) private Integer age; @NotEmpty(message = "聯絡人不允許為空",groups = {ValidEmail.class,ValidMobile.class,ValidUserName.class}) @Size(min = 1, max = 3, message = "聯絡人長度只允許1到3之間",groups = {ValidEmail.class,ValidMobile.class,ValidUserName.class}) private List contacts; } ``` ### 3.Controller方法上指名驗證組別 ``` java /** * 手機號註冊 * * @param userModel * @return */ @PostMapping("/mobile-regist") public WrapperResult mobileRegit(@Validated(ValidMobile.class) @RequestBody UserModel userModel) { return WrapperResult.success(true); } ``` 這時候進行如下請求: POST http://127.0.0.1:8080/user/mobile-regist ``` json { "mobile":"12666666666", "password":"", "address":"", "age": null, "contacts":[ ] } ``` 則會返回結果: ``` json { "status": 1, "data": "聯絡人長度只允許1到3之間;手機號格式錯誤;聯絡人不允許為空;", "msg": "FAIL", "success": false } ``` 該請求中並沒有傳遞email和username欄位,而且結果中也未校驗出這兩個欄位,符合預期結果。 ## 六、手動校驗 此處的手動校驗並非是使用if/else進行簡單的手動校驗,而是使用Validation自帶的校驗工具對使用了@NotNull等註解的實體物件進行屬性校驗。 首先先獲取Valiation物件: ``` java private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); ``` ### 1. 全屬性校驗 ``` java /** * 驗證某個物件所有欄位 * * @param obj * @param * @return */ public static ValidationResult validateEntity(T obj) { ValidationResult result = new ValidationResult(); Set> set = validator.validate(obj, Default.class); if (!CollectionUtils.isEmpty(set)) { result.setHasErrors(true); Map errorMsg = new HashMap<>(); for (ConstraintViolation cv : set) { errorMsg.put(cv.getPropertyPath().toString(), cv.getMessage()); } result.setErrorMsg(errorMsg); } return result; } ``` ### 2.某個欄位的單獨校驗 ``` java /** * 驗證某個物件某個欄位 * * @param obj * @param propertyName * @param * @return */ public static ValidationResult validateProperty(T obj, String propertyName) { ValidationResult result = new ValidationResult(); Set> set = validator.validateProperty(obj, propertyName, Default.class); if (!CollectionUtils.isEmpty(set)) { result.setHasErrors(true); Map errorMsg = new HashMap<>(); for (ConstraintViolation cv : set) { errorMsg.put(propertyName, cv.getMessage()); } result.setErrorMsg(errorMsg); } return result; } ``` ValidationResult的定義如下: ``` java @Data @Builder @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) public class ValidationResult { private Boolean hasErrors; private Map errorMsg; } ``` ## 七、檔案上傳校驗 ### 1.tomcat容器下檔案上傳校驗 在springboot+tomcat架構下的檔案上傳校驗,假如已經有了如下的配置: ``` yaml spring: servlet: multipart: max-file-size: 1MB max-request-size: 1MB ``` 這表示只允許上傳小於1MB大小的檔案,如果不指定異常處理器,預設會報前端400,在`ValidationAdvice`類中新增如下程式碼可以自定義返回結果: ``` java //檔案上傳檔案大小超出限制 @ExceptionHandler(MaxUploadSizeExceededException.class) @ResponseBody public WrapperResult> fileSizeException(MaxUploadSizeExceededException exception) { log.error("檔案太大,上傳失敗",exception); return WrapperResult.faild("只允許上傳不大於"+exception.getMaxUploadSize()+"的檔案"); } ``` ### 2.其它容器 在Jetty容器中1中的方法可能會失效,未驗證;在undertow容器中是一定會失效,已經驗證。undertow容器畢竟和spring-boot沒有完全打磨好,不建議現階段使用。 ## 八、附錄 ### 1.所有校驗規則註解說明 | 註解 | 說明 | | ------------------------ | ------------------------------------------------ | | @Null | 被註解的元素必須為空 | | @NotNull | 被註解的元素必須不為空 | | @AssertTrue | 被註解的元素必須為true | | @AssertFlase | 被註解的元素必須為false | | @Min(value) | 被註解的元素必須是數字,且必須大於指定的最小值 | | @Max(value) | 被註解的元素必須是數字,且必須小於指定的最大值 | | @DecimalMin(value) | 被註解的元素必須是數字,且必須大於指定的最小值 | | @DecaimalMax(value) | 被註解的元素必須是數字,且必須小於指定的最大值 | | @Size(max=,min=) | 被註解元素的大小必須在指定的範圍內 | | @Digit(integer,fraction) | 被註解元素必須是數字,且其值必須在可接受的範圍內 | | @Past | 被註解元素必須是一個過去的日期 | | @Futrue | 被註解元素必須是一個將來的日期 | | @Pattern(regex=,flag=) | 被註解元素必須符合指定的正則表示式 | | @NotBlank | 驗證非空,且長度必須大於0 | | @Email | 被註解的元素必須是電子郵件地址 | | @Length(max=,min=) | 被註解的字串大小必須在指定的範圍內 | | @NotEmpty | 被註解的字串必須非空 | | @Range(max=,min=) | 被註解的元素必須在指定範圍內 | ### 2.校驗規則註解例子 ``` java // 空和非空檢查: @Null、@NotNull、@NotBlank、@NotEmpty @Null(message = "驗證是否為 null") private Integer isNull; @NotNull(message = "驗證是否不為 null, 但無法查檢長度為0的空字串") private Integer id; @NotBlank(message = "檢查字串是不是為 null,以及去除空格後長度是否大於0") private String name; @NotEmpty(message = "檢查是否為 NULL 或者是 EMPTY") private List stringList; // Boolean值檢查: @AssertTrue、@AssertFalse @AssertTrue(message = " 驗證 Boolean引數是否為 true") private Boolean isTrue; @AssertFalse(message = "驗證 Boolean 引數是否為 false ") private Boolean isFalse; // 長度檢查: @Size、@Length @Size(min = 1, max = 2, message = "驗證(Array,Collection,Map,String)長度是否在給定範圍內") private List integerList; @Length(min = 8, max = 30, message = "驗證字串長度是否在給定範圍內") private String address; // 日期檢查: @Future、@FutureOrPresent、@Past、@PastOrPresent @Future(message = "驗證日期是否在當前時間之後") private Date futureDate; @FutureOrPresent(message = "驗證日期是否為當前時間或之後") private Date futureOrPresentDate; @Past(message = "驗證日期是否在當前時間之前") private Date pastDate; @PastOrPresent(message = "驗證日期是否為當前時間或之前") private Date pastOrPresentDate; // 其它檢查: @Email、@CreditCardNumber、@URL、@Pattern、 @ScriptAssert、@UniqueElements @Email(message = "校驗是否為正確的郵箱格式") private String email; @CreditCardNumber(message = "校驗是否為正確的信用卡號") private String creditCardNumber; @URL(protocol = "http", host = "127.0.0.1", port = 8080, message= "校驗是否為正確的URL地址") private String url; @Pattern(regexp = "^1[3|4|5|7|8][0-9]{9}$", message = "正則校驗是否為正確的手機號") private String phone; // 對關聯物件元素進行遞迴校驗檢查 @Valid @UniqueElements(message = "校驗集合中的元素是否唯一") private List calendarEvent; @Data @ScriptAssert(lang = "javascript", script ="_this.startDate.before(_this.endDate)",message = "通過指令碼表示式校驗引數") private class CalendarEvent {   private Date startDate;   private Date endDate; } // 數值檢查: @Min、@Max、@Range、@DecimalMin、@DecimalMax、@Digits @Min(value = 0, message = "驗證數值是否大於等於指定值") @Max(value = 100, message = "驗證數值是否小於等於指定值") @Range(min = 0, max = 100, message = "驗證數值是否在指定值區間範圍內") private Integer score; @DecimalMin(value = "10.01", inclusive = false, message = "驗證數值是否大於等於指定值") @DecimalMax(value = "199.99", message = "驗證數值是否小於等於指定值") @Digits(integer = 3, fraction = 2, message = "限制整數位最多為3,小數位最多為2") private BigDecimal money; ``` ## 九、原始碼地址 https://gitee.com/kdyzm/validation-spring-boot-demo 我的部落格地址:https://blog.kdyzm.cn 歡迎留言