在 Spring Boot 中使用 ProblemDetail 傳回錯誤
1. 概述
在本文中,我們將探索使用ProblemDetail
在 Spring Boot 應用程式中傳回錯誤。無論我們處理 REST API 還是反應流,它都提供了一種向客戶端傳達錯誤的標準化方法。
讓我們深入探討為什麼我們會關心它。在引入之前,我們將探討錯誤處理是如何完成的,然後,我們還將討論這個強大工具背後的規範。最後,我們將學習如何使用它來準備錯誤回應。
2.為什麼我們要關心ProblemDetail
?
使用ProblemDetail
標準化錯誤回應對於任何 API 都至關重要。
它幫助客戶理解和處理錯誤,提高 API 的可用性和可調試性。這會帶來更好的開發人員體驗和更強大的應用程式。
採用它還可以幫助提供更多資訊豐富的錯誤訊息,這對於維護我們的服務和排除故障至關重要。
3. 傳統的錯誤處理方法
在ProblemDetail
之前,我們經常實作自訂例外處理程序和回應實體來處理 Spring Boot 中的錯誤。我們將建立自訂錯誤回應結構。這導致了不同 API 之間的不一致。
此外,這種方法需要大量樣板程式碼。而且,它缺乏標準化的錯誤表示方式,導致客戶端難以統一解析和理解錯誤訊息。
4. ProblemDetail
說明
ProblemDetail
規範是RFC 7807 標準的一部分。它為錯誤回應定義了一致的結構,包括type, title, status, detail
和instance
等欄位。這種標準化透過提供錯誤訊息的通用格式來幫助 API 開發人員和消費者。
實作ProblemDetail
可確保我們的錯誤回應是可預測且易於理解的。這反過來又改善了我們的 API 與其客戶端之間的整體通訊。
接下來,我們將著眼於在 Spring Boot 應用程式中實現它,從基本設定和配置開始。
5. 在 Spring Boot 中實作ProblemDetail
Spring Boot 中有多種方法可以實現問題細節。
5.1.使用應用程式屬性啟用ProblemDetail
首先,我們可以新增一個屬性來啟用它。對於 RESTful 服務,我們將以下屬性新增至application.properties
:
spring.mvc.problemdetails.enabled=true
此屬性允許自動使用ProblemDetail
在基於 MVC(servlet 堆疊)的應用程式中進行錯誤處理。
對於反應式應用程序,我們添加以下屬性:
spring.webflux.problemdetails.enabled=true
啟用後,Spring 使用ProblemDetail
報告錯誤:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Invalid request content.",
"instance": "/sales/calculate"
}
此屬性在錯誤處理中自動提供ProblemDetail
。另外,如果不需要,我們可以將其關閉。
5.2.在異常處理程序中實作ProblemDetail
全域異常處理程序在 Spring Boot REST 應用程式中實作集中式錯誤處理。
讓我們考慮一個簡單的 REST 服務來計算折扣價格。
它接受操作請求並傳回結果。此外,它還執行輸入驗證並強制執行業務規則。
我們來看看請求的實作:
public record OperationRequest(
@NotNull(message = "Base price should be greater than zero.")
@Positive(message = "Base price should be greater than zero.")
Double basePrice,
@Nullable @Positive(message = "Discount should be greater than zero when provided.")
Double discount) {}
以下是執行結果:
public record OperationResult(
@Positive(message = "Base price should be greater than zero.") Double basePrice,
@Nullable @Positive(message = "Discount should be greater than zero when provided.")
Double discount,
@Nullable @Positive(message = "Selling price should be greater than zero.")
Double sellingPrice) {}
並且,這是無效操作異常的實作:
public class InvalidInputException extends RuntimeException {
public InvalidInputException(String s) {
super(s);
}
}
現在,讓我們實作 REST 控制器來為端點提供服務:
@RestController
@RequestMapping("sales")
public class SalesController {
@PostMapping("/calculate")
public ResponseEntity<OperationResult> calculate(
@Validated @RequestBody OperationRequest operationRequest) {
OperationResult operationResult = null;
Double discount = operationRequest.discount();
if (discount == null) {
operationResult =
new OperationResult(operationRequest.basePrice(), null, operationRequest.basePrice());
} else {
if (discount.intValue() >= 100) {
throw new InvalidInputException("Free sale is not allowed.");
} else if (discount.intValue() > 30) {
throw new IllegalArgumentException("Discount greater than 30% not allowed.");
} else {
operationResult = new OperationResult(operationRequest.basePrice(),
discount,
operationRequest.basePrice() * (100 - discount) / 100);
}
}
return ResponseEntity.ok(operationResult);
}
}
SalesController
類別在“/sales/calculate”
端點處理 HTTP POST 要求。
它檢查並驗證OperationRequest
物件。如果請求有效,它會計算銷售價格,並考慮可選折扣。如果折扣無效(超過100%
或超過30%
),它會拋出例外。如果折扣有效,它會套用折扣來計算最終價格,並傳回包裝在 ResponseEntity 中的OperationResult
ResponseEntity.
現在讓我們看看如何在全域例外處理程序中實作ProblemDetail
:
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(InvalidInputException.class)
public ProblemDetail handleInvalidInputException(InvalidInputException e, WebRequest request) {
ProblemDetail problemDetail
= ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
problemDetail.setInstance(URI.create("discount"));
return problemDetail;
}
}
使用@RestControllerAdvice
註解的GlobalExceptionHandler
類別擴充了ResponseEntityExceptionHandler
以在 Spring Boot 應用程式中提供集中式例外處理。
它定義了一個方法來處理InvalidInputException
異常。發生此異常時,它會建立一個具有BAD_REQUEST
狀態和異常訊息的ProblemDetail
物件。此外,它將instance
設為 URI( “discount”
)以指示錯誤的特定上下文。
這種標準化的錯誤回應向客戶提供了有關問題所在的清晰詳細的資訊。
ResponseEntityExceptionHandler
是一個可以方便地跨應用程式以標準化方式處理例外狀況的類別。因此,將異常轉換為有意義的 HTTP 回應的過程得到了簡化。此外,它還提供了使用 ProblemDetail 處理常見 Spring MVC 例外的方法,例如MissingServletRequestParameterException, MethodArgumentNotValidException,
等ProblemDetail.
5.3.測試ProblemDetail
實施
現在讓我們測試一下我們的功能:
@Test
void givenFreeSale_whenSellingPriceIsCalculated_thenReturnError() throws Exception {
OperationRequest operationRequest = new OperationRequest(100.0, 140.0);
mockMvc
.perform(MockMvcRequestBuilders.post("/sales/calculate")
.content(toJson(operationRequest))
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpectAll(status().isBadRequest(),
jsonPath("$.title").value(HttpStatus.BAD_REQUEST.getReasonPhrase()),
jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()),
jsonPath("$.detail").value("Free sale is not allowed."),
jsonPath("$.instance").value("discount"))
.andReturn();
}
在此SalesControllerUnitTest
中,我們自動組裝了MockMvc
和ObjectMapper
以測試SalesController
。
測試方法givenFreeSale_whenSellingPriceIsCalculated_thenReturnError()
模擬對“/sales/calculate”
端點的 POST 請求,其中的OperationRequest
包含基本價格100.0
和折扣140.0
。因此,這應該會觸發控制器中的InvalidOperandException
。
最後,我們驗證BadRequest
類型的回應,其中ProblemDetail
指示“Free sale is not allowed.”
六,結論
在本教程中,我們探討了ProblemDetails,
其規格及其在 Spring Boot REST 應用程式中的實作。然後,我們討論了相對於傳統錯誤處理的優點,以及如何在 servlet 和反應式堆疊中使用它。
與往常一樣,原始碼可以在 GitHub 上取得。