SpringBoot 異常處理/自定義格式校驗(yàn)的問題實(shí)例詳解
1. 問題簡要描述
這個問題是在測試自定義注解實(shí)現(xiàn)數(shù)據(jù)格式校驗(yàn)時(shí)遇到的,當(dāng)前有兩個注解,ValidateParamsMatched和SafeParam。前者的功能是對類中的兩個列表的長度進(jìn)行比較,后者則是對字符串進(jìn)行校驗(yàn),判斷是否包含特殊字符
但是,當(dāng)分別觸發(fā)這兩個校驗(yàn)時(shí),都會觸發(fā)異常并返回請求,但之后后者能返回請求信息。
這里問題本質(zhì)上是沒弄懂對應(yīng)的異常處理代碼,但也值得記錄一下。
2. 異常觸發(fā)
1) 參數(shù)級別約束
在 Controller 方法上直接使用參數(shù)級約束,并且,Controller 類上有 @Validated 注解時(shí),驗(yàn)證失敗會觸發(fā) ConstraintViolationException 異常
@Validated // 必需
@RestController
public class ProductController {
@GetMapping("/products")
public Product getProduct(@ValidCategory String category) {
// ...
}
}在 DTO 的字段上使用參數(shù)級約束,且方法參數(shù)前有 @Valid 注解時(shí),驗(yàn)證失敗會觸發(fā) MethodArgumentNotValidException 異常
@PostMapping("/products")
public ResponseEntity<?> createProduct(
@Valid @RequestBody ProductRequest request // 觸發(fā)驗(yàn)證
) {
// ...
}2) 類級別約束
在實(shí)體類上標(biāo)注了類級約束,且Controller 方法參數(shù)使用了 @Valid,觸發(fā)MethodArgumentNotValidException
@ValidInventory // 類級別約束
public class Product {
private int stockQuantity;
private int minStockLevel;
}
@PostMapping("/products")
public ResponseEntity<?> createProduct(
@Valid @RequestBody Product product // 觸發(fā)驗(yàn)證
) {
// ...
}注意:類級別約束應(yīng)該加在需要驗(yàn)證的目標(biāo)類(實(shí)體類/DTO)的類聲明上,且在 Controller 方法參數(shù)添加 @Valid 和在 Service 方法參數(shù)添加 @Valid
3. 異常處理
建議通過 @RestControllerAdvice 設(shè)置全局異常處理來對此類異常進(jìn)行處置。但獲取信息時(shí),這兩類約束的方法不同。
在 MethodArgumentNotValidException 中,字段級約束產(chǎn)生 FieldError,類級約束產(chǎn)生 ObjectError(也稱為全局錯誤)
1) 字段級別約束
獲取方式為 ex.getBindingResult().getFieldErrors(),包含字段名、錯誤消息、拒絕值。
ex.getMessage() 會返回詳細(xì)錯誤信息
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException ex) {
// 只獲取字段錯誤
String errorMsg = ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.joining("; "));
log.warn("請求值驗(yàn)證異常 | request={}", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMsg);
}2) 類級別約束
獲取方式為 ex.getBindingResult().getGlobalErrors(),包含對象名、錯誤消息。
ex.getMessage() 返回通用消息。
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException ex) {
// 只獲取字段錯誤
String errorMsg = ex.getBindingResult().getGlobalErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining("; "));
log.warn("請求值驗(yàn)證異常 | request={}", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMsg);
}在類級別約束的異常處理時(shí),ex.getBindingResult().getFieldErrors() 會返回空值;同樣的,ex.getBindingResult().getGlobalErrors()也會返回空值
因此,可以統(tǒng)一錯誤處理格式
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException ex) {
// 創(chuàng)建錯誤消息列表
List<String> errorMessages = new ArrayList<>();
// 處理字段錯誤
ex.getBindingResult().getFieldErrors().forEach(fieldError -> {
String msg = String.format("%s: %s",
fieldError.getField(),
fieldError.getDefaultMessage());
errorMessages.add(msg);
});
// 處理全局錯誤
ex.getBindingResult().getGlobalErrors().forEach(globalError -> {
String msg = String.format("全局錯誤: %s", globalError.getDefaultMessage());
errorMessages.add(msg);
});
// 合并所有錯誤消息
String errorMsg = String.join("; ", errorMessages);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMsg);
}4. 進(jìn)一步學(xué)習(xí)
值得注意的是, ex.getMessage() 給出的結(jié)果也是不一樣的,這個與代碼實(shí)現(xiàn)有關(guān)
public String getMessage() {
StringBuilder sb = new StringBuilder("Validation failed for argument at index ")
.append(getParameter().getParameterIndex()).append(" in method: ")
.append(getParameter().getExecutable().toGenericString());
BindingResult result = getBindingResult();
if (result.getErrorCount() > 0) {
sb.append(", with ").append(result.getErrorCount()).append(" error(s): ");
for (ObjectError error : result.getAllErrors()) {
sb.append('[').append(error).append("] ");
}
}
return sb.toString();
}當(dāng)只有類級約束失敗時(shí),result.getErrorCount() == 1(只有 ObjectError),因此,ex.getMessage() 輸出的內(nèi)容不同
- 另外,在調(diào)試的過程中還遇到了一個問題,在類級別約束中,考慮到已經(jīng)存在非空校驗(yàn)了,因此,在類約束中再重復(fù)做校驗(yàn)。但在測試過程中發(fā)現(xiàn),輸入屬性為空時(shí),會在類約束中報(bào)錯。后來經(jīng)過查驗(yàn),確定問題原因。
- 雖然對于 Bean 的校驗(yàn)按照 字段->類 的順序進(jìn)行,但校驗(yàn)過程會收集所有違反約束的情況,而不是在遇到第一個約束失敗時(shí)就停止。也就是說類級別的約束(即放在類上的注解)的校驗(yàn)器,在字段級別約束失敗時(shí),仍然會被執(zhí)行
到此這篇關(guān)于SpringBoot 異常處理/自定義格式校驗(yàn)的文章就介紹到這了,更多相關(guān)springboot異常處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java類加載機(jī)制實(shí)現(xiàn)流程及原理詳解
這篇文章主要介紹了Java類加載機(jī)制實(shí)現(xiàn)流程及原理詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
java使用POI實(shí)現(xiàn)html和word相互轉(zhuǎn)換
這篇文章主要為大家詳細(xì)介紹了java使用POI實(shí)現(xiàn)html和word的相互轉(zhuǎn)換,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
Java?nacos動態(tài)配置實(shí)現(xiàn)流程詳解
使用動態(tài)配置的原因是properties和yaml是寫到項(xiàng)目中的,好多時(shí)候有些配置需要修改,每次修改就要重新啟動項(xiàng)目,不僅增加了系統(tǒng)的不穩(wěn)定性,也大大提高了維護(hù)成本,非常麻煩,且耗費(fèi)時(shí)間2022-09-09
java多線程開發(fā)ScheduledExecutorService簡化方式
這篇文章主要為大家介紹了java多線程開發(fā)ScheduledExecutorService的簡化方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03
Java常見的數(shù)據(jù)結(jié)構(gòu)之棧和隊(duì)列詳解
這篇文章主要介紹了Java常見的數(shù)據(jù)結(jié)構(gòu)之棧和隊(duì)列詳解,棧(Stack) 是一種基本的數(shù)據(jù)結(jié)構(gòu),具有后進(jìn)先出(LIFO)的特性,類似于現(xiàn)實(shí)生活中的一疊盤子,棧用于存儲一組元素,但只允許在棧頂進(jìn)行插入(入棧)和刪除(出棧)操作,需要的朋友可以參考下2023-10-10
springboot后端使用LocalDate接收日期的問題解決
在做Java開發(fā)時(shí),肯定會碰到傳遞時(shí)間參數(shù)的情況,本文主要介紹了springboot后端使用LocalDate接收日期的問題解決,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09

