1.背景
我們在平時的學習與工作中,都需要對參數進行校驗,比如在注冊時,用戶名密碼不能為空,用戶名長度必須小於10等等。雖然有些校驗在前端頁面會進行驗證,但是后端為了增加健壯性也需要對這些參數進行判斷(比如繞過前端頁面而直接調用了接口,參數的合法性未知),可能就會在controller或者service中就會有如下代碼的出現
package com.beemo.validation.controller; import com.beemo.validation.demo1.entity.Student; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestBody; import java.util.Objects; @RestController public class DemoController { @RequestMapping("/demo") public String saveDemo(@RequestBody Student student) { if (StringUtils.isEmpty(student.getName())) { return "學生名稱不能為空"; } if (student.getName().length() > 10) { return "學生名稱長度不能超過10位"; } if (Objects.isNull(student.getAge())) { return "學生年齡不能為空"; } if (student.getAge() <= 0) { return "學生年齡不能為負數"; } if (Objects.isNull(student.getNumber())) { return "學號不能為空"; } if (student.getNumber().length() != 10) { return "學號長度必須為10"; } // 其他判斷 // 調用service的方法等 return "ok"; } @Data class Student { /** * 姓名 */ private String name; /** * 年齡 */ private Integer age; /** * 學號 */ private String number; } }
從例子中可以看到,這僅僅是一個實體類3個字段的簡單驗證,就已經占據了很多的篇幅,也需要我們進行手動編寫這種判斷代碼,比較費時,代碼讀起來也沒什么營養,大部分都是在判斷合法性,等我們真正讀到想要的業務邏輯代碼可能需要往下翻好久,那么有沒有辦法能夠讓我們更簡潔更優雅的去驗證這些參數呢
2. Jakarta Bean Validation
2.1 Jakarta Bean Validation簡介
首先要知道Jakarta就是Java更名之后的名稱,Jakarta Bean Validation也就是Java Bean Validation,是一套Java的規范,它可以
通過使用注解的方式在對象模型上表達約束
以擴展的方式編寫自定義約束
提供了用於驗證對象和對象圖的API
提供了用於驗證方法和構造方法的參數和返回值的API
報告違反約定的集合
運行在Java SE,並且集成在Jakarta EE8中
例如:
public class User { private String email; @NotNull @Email public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } public class UserService { public void createUser(@Email String email, @NotNull String name) { ... } }
雖然可以手動運行校驗,但更加自然的做法是讓其他規則和框架在適時對數據進行校驗(用戶在表示框架中進行輸入,業務服務通過CDI執行,實體通過JPA插入或者更新)
換句話說,即運行一次,到處約束
2.2 相關網址
在2020年2月份已經發布了3.0.0-M1
版本
其中Jakarta Bean Validation
只是一套標准,我們需要使用其他組織機構提供的實現來進行驗證,官方支持的為Hibernate Validator
3. 動手實踐
3.1 所需環境
這里JDK使用了JDK1.8,使用maven進行所需jar文件依賴,使用springboot搭建框架腳手架,使用lombok簡化代碼
如果用的不是這幾個可以適當修改,大同小異,而且springboot以及或其他依賴的版本每天都在變化,各個版本之間難免有或多或少的差別,可能細節處與本文章有所不同,需要大家知曉,並且根據自己的版本進行調整(比如spring-boot-starter-parent版本2.2.7與2.3.0在驗證異常時返回json格式與內容就有很大不同)
3.2 搭建空框架
- 使用
spring initializr
創建springboot項目,依次選擇添加web
、validation
以及lombok
模塊,生成的pom.xml
依賴如下。我這里spring-boot-starter-parent
的版本為2.3.0,再添加其他所需的pom
依賴
... <!-- spring-boot版本 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.0.RELEASE</version> ... <!-- web模塊 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 驗證模塊,hibernate-validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency> <!-- guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>29.0-jre</version> </dependency>
3.3 編寫代碼
編寫背景:模擬英雄聯盟游戲的技能與英雄的保存
這里的命名遵循外服名稱而不是國服直譯,例如英雄為champion而不是hero,技能為ability而不是skill
3.3.1 實體類
- 英雄
package com.beemo.validation.demo2.entity; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; /** * 英雄entity */ @Data public class Champion { /** * 英雄名稱 */ @NotBlank(message = "英雄名稱不能為空") private String name; /** * 英雄頭銜 */ @NotBlank(message = "英雄頭銜不能為空") private String title; /** * 英雄描述 */ @NotBlank(message = "英雄描述不能為空") private String description; /** * 英雄類型 * 坦克、刺客、射手、法師、輔助以及戰士 */ @NotNull(message = "英雄類型不能為空") private Byte type; }
- 技能entity
package com.beemo.validation.demo2.entity; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; /** * 技能 */ @Data public class Ability { /** * 技能名稱 */ @NotBlank(message = "技能名稱不能為空") private String name; /** * 技能描述 */ @NotBlank(message = "技能描述不能為空") private String description; /** * 技能類型 * 例如魔法值、怒氣、能量等 */ @NotNull(message = "技能類型不能為空") private Byte type; }
3.3.2 控制層
- 英雄controller
package com.beemo.validation.demo2.controller; import com.beemo.validation.demo2.entity.Champion; 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; import javax.validation.Valid; @RestController @RequestMapping("/demo2/champion") @Validated public class ChampionController { /** * 保存 * @param entity 要保存的英雄實體 * @return 保存結果 */ @PostMapping("save") public String save(@Valid @RequestBody Champion entity) { // 調用service等 return "ok"; } }
- 技能controller
package com.beemo.validation.demo2.controller; import com.beemo.validation.demo2.entity.Ability; 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; import javax.validation.Valid; @RestController @RequestMapping("/demo2/ability") @Validated public class AbilityController { /** * 保存 * @param entity 要保存的技能實體 * @return 保存結果 */ @PostMapping("save") public String save(@Valid @RequestBody Ability entity) { // 調用service等 return "ok"; } }
3.3.3 測試
使用postman
或其他工具發送POST請求,進行驗證,我們直接輸入我們參數直接傳一個內容為空的json,查看結果
可以看到,這里返回了400異常,意為參數錯誤
我們再把所有參數補全,再試一下
可以看到,如果我們把參數補全之后,返回的是“ok”,即進入controller執行該方法。
那么,例子中添加的幾個注解都是什么意思,有什么作用,而且注解中寫的message信息在驗證后並沒有輸出,那么我們怎么樣輸出這些message呢
4. 注解含義
4.1 開啟驗證
首先我們看controller類最上方,我們標注了@Validataed(這里可以去掉,不用在控制類上添加該注解),該注解的含義是:這個類要啟用參數校驗。在save方法的參數中標注了@Valid,含義為我們要對緊跟的實體進行校驗,而具體校驗的內容,為實體類中的我們的定義的約束
以Ability類舉例,在name字段上方標記了@NotBlank,意為定義了該字段不允許為空的約束,如果name為空,校驗就不通過,就會返回我們之前碰到的400異常。而type字段也標注了@NotNull,也定義了該字段不允許為空的約束,具體的區別以及其他內置的約束如3.5所示
4.2 內置約束
內置約束位於javax.validation.constraints包
內,列表如下
4.2.1 @Null
- 被標注元素必須為
null
- 接收任意類型
比如在創建一個英雄時,ID需要由數據庫自增生成,而不是我們自定義,那么該我們在接收前台傳遞的json時就必須為空
4.2.2 @NotNull
- 被標注元素必須不為
null
- 接收任意類型
定義一個字段不能為空,例如技能類型或者英雄名稱
4.2.3 @AssertTrue
- 被標注元素必須true
- 支持的類型為
boolean
以及Boolean
null
被認為是有效的
要么為null,否則必須為true
4.2.4 @AssertFalse
- 被標注元素必須false
- 支持的類型為
boolean
以及Boolean
null
被認為是有效的
要么為null,否則必須為false
4.2.5 @Min
- 被標注元素必須為是一個數字,其值必須大於等於指定的最小值
- 支持的類型為
BigDecimal
、BigInteger
、byte
、short
、int
、long
以及各自的包裝類 - 注意
double
以及float
由於舍入錯誤而不被支持 null
被認為是有效的
4.2.6 @Max
- 被標注元素必須為是一個數字,其值必須小於等於指定的最大值
- 支持的類型為
BigDecimal
、BigInteger
、byte
、short
、int
、long
以及各自的包裝類 - 注意
double
以及float
由於舍入錯誤而不被支持 null
被認為是有效的
4.2.7 @DecimalMin
- 被標注元素必須為是一個數字,其值必須大於等於指定的最小值
- 支持的類型為
BigDecimal
、BigInteger
、CharSequence
、byte
、short
、int
、long
以及各自的包裝類 - 注意
double
以及float
由於舍入錯誤而不被支持 null
被認為是有效的
4.2.8 @DecimalMax
- 被標注元素必須為是一個數字,其值必須小於等於指定的最大值
- 支持的類型為
BigDecimal
、BigInteger
、CharSequence
、byte
、short
、int
、long
以及各自的包裝類 - 注意
double
以及float
由於舍入錯誤而不被支持 null
被認為是有效的
4.2.9 @Negative
- 被標注元素必須為是一個嚴格意義上的負數(即0被認為是無效的)
- 支持的類型為
BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
、double
以及各自的包裝類 null
被認為是有效的
4.2.10 @NegativeOrZero
- 被標注元素必須為是負數或者0
- 支持的類型為
BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
、double
以及各自的包裝類 null
被認為是有效的
4.2.11 @Positive
- 被標注元素必須為是一個嚴格意義上的正數(即0被認為是無效的)
- 支持的類型為
BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
、double
以及各自的包裝類 null
被認為是有效的
4.2.12 @PositiveOrZero
- 被標注元素必須為是正數或者0
- 支持的類型為
BigDecimal
、BigInteger
、byte
、short
、int
、long
、float
、double
以及各自的包裝類 null
被認為是有效的
4.2.13 @Size
- 被標注元素的大小必須在指定的邊界區間
- 支持的類型為
CharSequence
(計算字符序列的長度) 、Collection
(計算集合的大小)、Map
(計算map的大小) 、Array
(計算數組的長度) null
被認為是有效的
4.2.14 @Digits
- 被標注元素必須是在可接受范圍內的數字
- 支持的類型為
BigDecimal
、BigInteger
、CharSequence
、byte
、short
、int
、long
以及各自的包裝類 null
被認為是有效的
4.2.15 @Past
- 被標注元素必須是過去的某個時刻、日期或者時間
- “現在”的概念是附加在
Validator
或者ValidatorFactory
中的ClockProvider
定義的,默認的ClockProvider
根據虛擬機定義了當前時間,如果需要的話,會應用當前默認時區 -
支持的類型為java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包裝類
null
被認為是有效的
4.2.16 @PastOrPresent
- 被標注元素必須是過去或現在的某個時刻、日期或者時間
- “現在”的概念是附加在
Validator
或者ValidatorFactory
中的ClockProvider
定義的,默認的ClockProvider
根據虛擬機定義了當前時間,如果需要的話,會應用當前默認時區 - “現在”的概念相對的定義在使用的約束上,例如,如果約束在Year上,那么現在表示當前年份
-
支持的類型為java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包裝類
null
被認為是有效的
4.2.17 @Future
- 被標注元素必須是未來的某個時刻、日期或者時間
- “現在”的概念是附加在
Validator
或者ValidatorFactory
中的ClockProvider
定義的,默認的ClockProvider
根據虛擬機定義了當前時間,如果需要的話,會應用當前默認時區 -
支持的類型為java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包裝類
null
被認為是有效的
4.2.18 @FutureOrPresent
- 被標注元素必須是未來或現在的某個時刻、日期或者時間
- “現在”的概念是附加在
Validator
或者ValidatorFactory
中的ClockProvider
定義的,默認的ClockProvider
根據虛擬機定義了當前時間,如果需要的話,會應用當前默認時區 - “現在”的概念相對的定義在使用的約束上,例如,如果約束在Year上,那么現在表示當前年份
-
支持的類型為java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包裝類
null
被認為是有效的
4.2.19 @Pattern
- 被標注的
CharSequence
必須匹配指定的正則表達式,該正則表達式遵循Java的正則表達式規定 - 支持的類型為
CharSequence
null
被認為是有效的
4.2.20 @NotEmpty
- 被標注元素必須不為
null
或者空(以字符串舉例,不為null並且不為“”) - 支持的類型為
CharSequence
(計算字符序列的長度) 、Collection
(計算集合的大小)、Map
(計算map的大小) 、Array
(計算數組的長度)
4.2.21 @NotBlank
- 被標注元素必須不為
null
,並且必須包含至少一個非空格的字符 - 支持的類型為
CharSequence
4.2.22 @Email
- 字符串必須是格式良好的電子郵件地址
- 支持的類型為
CharSequence
5. 異常模塊
還有一個問題,就是我們定義的message沒有生效,比如“技能名稱不能為空”,並沒有出現在返回結果中,取而代之的是400異常,那么怎樣才能返回我們想要的message呢
首先我們在controller當中定一個一個方法,用@ExceptionHandler注解標注一下,用來獲取controller拋出的異常,然后我們跟蹤一下斷點,看一下到底是什么異常
package com.beemo.validation.demo2.controller; import com.beemo.validation.demo2.entity.Ability; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.ConstraintViolationException; import javax.validation.Valid; @RestController @RequestMapping("/demo2/ability") @Validated public class AbilityController { /** * 保存 * @param entity 要保存的技能實體 * @return 保存結果 */ @PostMapping("save") public String save(@Valid @RequestBody Ability entity) { // 調用service等 return "ok"; } @ExceptionHandler public void handleException(Exception e) { e.printStackTrace(); } }
拋出的是org.springframework.web.bind.MethodArgumentNotValidException
在看一下DEBUG窗口中的每個參數,發現bindingResult->errors->field和defaultMessage,一個違反約束的字段名稱,另一個是違我們自定義的message
此時我們就可以進行處理,返回我們想要的結果,而不是拋出400
5.1 優化返回值
在實際開發中,一般不會返回一個“ok”或者“success”這種字符串,通常情況下會返回一個json字符串,其中包含
- 一個表示結果的狀態值,例如
HTML狀態碼
或自定義狀態值 - 一個返回消息,解釋該狀態值或結果
- 承載數據
package com.beemo.demo2.common; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; @AllArgsConstructor @NoArgsConstructor public class R { private int code; private String msg; private Object data; public static R success() { return success(null); } public static R success(Object data) { return new R(1, "操作成功", data); } public static R violateConstraint(List<Map<String, String>> violation) { return new R(2, "參數校驗未通過", violation); } }
修改controller
package com.beemo.demo2.controller; import com.beemo.demo2.common.R; import com.beemo.demo2.entity.Ability; import org.springframework.validation.FieldError; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; import java.util.stream.Collectors; @RestController @RequestMapping("/demo2/ability") @Validated public class AbilityController { /** * 保存 * @param entity 要保存的技能實體 * @return 保存結果 */ @PostMapping("save") public R save(@Valid @RequestBody Ability entity) { // 調用service等 return R.success(); } }
將異常處理方法提出,標注@ControllerAdvice
注解,使得每個controller的異常都可以用該方法處理,並修改返回值,並且如果是單獨提出來一個模塊,需要在啟引用該模塊的啟動類上加掃描
package com.beemo.common.config; import com.beemo.common.common.R; import org.springframework.stereotype.Component; import org.springframework.validation.FieldError; 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 javax.validation.ConstraintViolationException; import java.util.List; import java.util.stream.Collectors; @ControllerAdvice @ResponseBody public class MyExceptionHandler { @ExceptionHandler public R handleException(MethodArgumentNotValidException e) { List<String> violations = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage). collect(Collectors.toList()); return R.violateConstraint(violations); } }
package com.beemo.demo2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication(scanBasePackages = "com.beemo.*") public class Demo2Application { public static void main(String[] args) { SpringApplication.run(Demo2Application.class, args); } }
然后我們再測試一下
發現得到的結果再也不是400異常,而是我們指定的message集合了
6. 驗證非前台傳遞的參數
除了在controller
驗證前台傳遞的參數之外,有時我們還需要驗證諸如自己new的對象,或者從其他方法查詢出來的對象,這時候我們可能需要把這些操作放在service
層或其他層
6.1 調用非本類的校驗方法
例如我們自己new了一個對象,然后調用其他類的一個驗證方法
建立一個service接口以及一個實現類
我們在實現類上,模擬controller校驗,加上@Validated
以及@Valid
注解
package com.beemo.demo3.service; import com.beemo.demo3.entity.Ability; /** * 技能service接口 */ public interface IAbilityService { /** * 保存 * @param ability */ void saveOne(Ability ability); }
package com.beemo.demo3.service.impl; import com.beemo.demo3.entity.Ability; import com.beemo.demo3.service.IAbilityService; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.util.Arrays; import java.util.List; /** * 技能service實現類 */ @Validated @Service public class AbilityServiceImpl implements IAbilityService { @Override public void saveOne(@Valid @NotNull Ability ability) { System.out.println("通過校驗"); // 進行保存操作等... }
然后在controller中調用該方法
package com.beemo.demo3.controller; import com.beemo.demo3.entity.Ability; import com.beemo.demo3.service.IAbilityService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/demo3/ability") @Validated public class AbilityController { @Autowired private IAbilityService abilityService; /** * 保存 * @return 保存結果 */ @PostMapping("save") public String save() { // new Ability ability = new Ability(); abilityService.saveOne(ability); return "ok"; } }
我們進行測試發現,並沒有我們符合想象的返回R,相反在后台控制台報了一個異常
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method AbilityServiceImpl#saveOne(Ability) redefines the configuration of IAbilityService#saveOne(Ability). at org.hibernate.validator.internal.metadata.aggregated.rule.OverridingMethodMustNotAlterParameterConstraints.apply(OverridingMethodMustNotAlterParameterConstraints.java:24) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final] at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.assertCorrectnessOfConfiguration(ExecutableMetaData.java:462) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final] at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:380) ~[ ......
一個重寫的方法禁止重新定義參數的約束配置,但是方法AbilityServiceImpl#saveOne(Ability) 重新定義了 IAbilityService#saveOne(Ability)的配置
翻譯過來就是如果你的接口沒有定義約束,那么你的實現類就不能夠定義該約束
按照異常信息,我們試着將驗證放在接口中在嘗試一下
package com.beemo.demo3.service; import com.beemo.demo3.entity.Ability; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; import javax.validation.constraints.NotNull; @Validated /** * 技能service接口 */ public interface IAbilityService { /** * 保存 * @param ability */ void saveOne(@Valid @NotNull Ability ability); }
測試之后發現返回結果為500異常,這次控制器打印異常信息明顯跟上次不一樣,貌似確實是通過校驗了,只不過拋出的異常不一樣
javax.validation.ConstraintViolationException: saveOne.ability: 不能為null at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.6.RELEASE.jar:5.2.6.RELEASE] ......
我們發現在service層中如果違法約束拋出的異常為ConstraintViolationException
,而並非在controller中的MethodArgumentNotValidException
我們再次改進異常處理方法,然后跟蹤一下異常的信息
根據調試的信息,我們就可以處理我們的返回值了
@ExceptionHandler public R handleException2(ConstraintViolationException e) { List<String> violations = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessageTemplate).collect(Collectors.toList()); return R.violateConstraint(violations); }
再測試一下
測試成功
6.2 調用本類的校驗方法
場景:我們需要從EXCEL中讀取數據,然后保存數據庫中,需要判斷每一條記錄,如果正確就進行保存,如果失敗則打印日志,接口和實現類如下
package com.beemo.demo3.service; import com.beemo.demo3.entity.Ability; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; import javax.validation.constraints.NotNull; @Validated /** * 技能service接口 */ public interface IAbilityService { /** * 保存 * @param ability */ void saveOne(@Valid @NotNull Ability ability); /** * 批量保存EXCEL中的數據 */ void saveOnesFromExcel(); }
package com.beemo.demo3.service.impl; import com.beemo.demo3.entity.Ability; import com.beemo.demo3.service.IAbilityService; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.validation.ConstraintViolationException; import java.util.List; /** * 技能service實現類 */ @Service @Slf4j public class AbilityServiceImpl implements IAbilityService { @Override public void saveOne(Ability ability) { System.out.println("通過校驗"); // 進行保存操作等... } /** * 批量保存EXCEL中的數據 */ @Override public void saveOnesFromExcel() { List<Ability> data = readFromExcel(); for (int i = 0, size = data.size(); i < size; i ++) { try { saveOne(data.get(i)); System.out.println("第" + i + "條記錄保存成功"); } catch (ConstraintViolationException e) { log.error("第" + i + "條記錄違法約束:" + e.getMessage()); } catch (Exception e) { log.error("第" + i + "條記錄保存失敗"); } } } /** * 從EXCEL中讀取 * @return */ private List<Ability> readFromExcel() { return Lists.newArrayList(new Ability(null, null, (byte)1), new Ability(null, "測試描述", null), new Ability("測試名稱", null, null), new Ability("約德爾誘捕器", "布置一個陷阱,陷阱可以束縛敵方英雄2秒並將目標暴露在己方視野內3秒。", (byte)1)); } }
我們模擬了一個從EXCEL中讀取list的方法,然后調用了save方法,該方法有參數驗證,我們來進行測試
控制台打印成功,證明我們的約束並沒有成功,但是我們的寫法看似沒問題
其實這個原因就是因為第一個方法saveFromExcel並沒有標注驗證,不論該方法怎么調用本類的驗證方法都不會生效,此問題原因同@Transactional以及@Aysnc標注的方法,其本質原因是因為代理的問題,這里不做過多探討,解決該問題的方法有三種
- (不推薦)將驗證方法移到其他類中 。這種方法奏效,但是無緣無故需要多建立一個service,有時候可能就是一個空方法,只不過參數有驗證,其他不知道的小伙伴看到可能會比較懵
- 注入
ApplicationContext
獲取bean
@Autowired private ApplicationContext applicationContext; /** * 批量保存EXCEL中的數據 */ @Override public void saveOnesFromExcel() { List<Ability> data = readFromExcel(); for (int i = 0, size = data.size(); i < size; i ++) { try { applicationContext.getBean(IAbilityService.class).saveOne(data.get(i)); System.out.println("第" + i + "條記錄保存成功"); } catch (ConstraintViolationException e) { log.error("第" + i + "條記錄違法約束:" + e.getMessage()); } catch (Exception e) { log.error("第" + i + "條記錄保存失敗"); } } }
3. 通過注入自己來獲取當前類的實例,再調用該實例的方法。需要加@Lazy注解防止自我注入時spring拋出org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.env.Environment' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}異常
@Autowired @Lazy private IAbilityService abilityService; /** * 批量保存EXCEL中的數據 */ @Override public void saveOnesFromExcel() { List<Ability> data = readFromExcel(); for (int i = 0, size = data.size(); i < size; i ++) { try { abilityService.saveOne(data.get(i)); System.out.println("第" + i + "條記錄保存成功"); } catch (ConstraintViolationException e) { log.error("第" + i + "條記錄違法約束:" + e.getMessage()); } catch (Exception e) { log.error("第" + i + "條記錄保存失敗"); } } }
6.3 關於@Validated的位置
我們已經清楚,約束配置的注解,例如@Valid
、@NotNull
等,需要在接口上進行配置,那么@Validated
需要標注在哪里呢,答案是接口和實現類都可以,但是標注位置不同,也有一些區別
- 標注在接口:意為實現類都回開啟驗證
- 標注在實現類:意為標注該注解的實現類才會開啟驗證,如果有一個實現類未標注
@Validated
,那么即使接口有約束配置,也不會在該實現類上進行校驗
6.4 關於實現類需不需要標注約束配置
個人感覺有優點優缺點
優點:一般看代碼的時候,都不會看接口,而是直接看實現類。如果標注在實現類上,可以更直觀的看到該方法的約束配置
缺點:必須與接口完全對應,如果接口修改約束配置,那么實現類必須相應的進行修改,否則會拋出異常
6.5 想讓枚舉類屬性驗證生效,需要添加@valid注解
6.6 feign調用中校驗也可以生效
無需在feign接口中添加校驗注解,只需在controller的接口方法參數中添加@valid
轉載:https://blog.csdn.net/csdn_mrsongyang/article/details/106115243