1. 簡介
在本教程中,我們將探索 Spring 框架中不同的錯誤響應格式。我們還將瞭解如何引發和處理 RFC7807 ProblemDetail 並帶有自定義屬性,以及如何在 Spring WebFlux 中引發自定義異常。
2. Spring Boot 3 中異常響應格式
讓我們瞭解 Spring Boot 3 中支持的各種異常響應格式。
默認情況下,Spring Framework 提供 <a href="https://docs.enterprise.spring.io/spring-boot/docs/3.2.17.1/api/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.html">DefaultErrorAttributes</a> 類,該類實現了 <a href="https://docs.enterprise.spring.io/spring-boot/api/java/org/springframework/boot/web/servlet/error/ErrorAttributes.html">ErrorAttributes</a> 接口,以便在未處理的錯誤發生時生成異常響應。對於默認錯誤,系統會生成一個 JSON 響應結構,我們可以在下面更詳細地進行檢查:
{
"timestamp": "2023-04-01T00:00:00.000+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/example"
}雖然這個錯誤響應包含一些關鍵屬性,但可能對調查問題沒有太大幫助。 幸運的是,我們可以通過在我們的 Spring WebFlux 應用程序中創建 ErrorAttributes 接口的自定義實現來修改此默認行為。
從 Spring Framework 6 開始,ProblemDetail 對於 RFC7807 規範的表示已得到支持。 ProblemDetail 包含幾個標準屬性,用於定義錯誤詳情,以及自定義詳情的選項。支持的屬性列表如下:
- type (字符串) – 標識問題的 URI 引用
- title (字符串) – 問題的簡短摘要
- status (數字) – HTTP 狀態碼
- detail (字符串) – 應包含異常的詳細信息。
- instance (字符串) – 用於標識問題的 URI 引用。例如,它可以引用導致問題的問題屬性。
除了上述標準屬性之外,ProblemDetail 還包含一個 Map<String, Object> ,用於添加自定義參數,以提供有關問題的更多詳細信息。
讓我們來看一個帶有自定義對象 errors 的示例錯誤響應結構:
{
"type": "https://example.com/probs/email-invalid",
"title": "Invalid email address",
"detail": "The email address 'john.doe' is invalid.",
"status": 400,
"timestamp": "2023-04-07T12:34:56.789Z",
"errors": [
{
"code": "123",
"message": "Error message",
"reference": "https//error/details#123"
}
]
}<p>Spring Framework 還提供了一個基於實現,稱為 <em><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api//org/springframework/web/ErrorResponseException.html">ErrorResponseException</a></em>。這個異常封裝了一個 <em>ProblemDetail</em> 對象,該對象生成了關於發生的錯誤的額外信息。我們可以擴展這個異常以自定義並添加屬性。</p>
3. 如何實現 ProblemDetail RFC 7807 異常
雖然 Spring 6+ / Spring Boot 3+ 應用默認支持 ProblemDetail 異常,但您需要通過以下方式之一啓用它。
3.1. 通過屬性文件啓用 ProblemDetail 異常
可以通過添加一個屬性來啓用 ProblemDetail 異常。
spring:
mvc:
problemdetails:
enabled: true3.2. 通過添加異常處理程序啓用 ProblemDetail 異常
ProblemDetail 異常也可以通過擴展 ResponseEntityExceptionHandler 並添加自定義異常處理程序(即使沒有覆蓋任何方法)來啓用。
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
//...
}我們將會採用這種方法來編寫這篇文章,因為我們需要添加自定義異常處理程序。
3.3. 實現 ProblemDetail 異常
讓我們研究一下如何引發和處理帶有自定義屬性的 ProblemDetail 異常,通過考慮一個提供少量創建和檢索 User 信息端點的簡單應用程序。
我們的控制器有一個 GET /v1/users/{userId} 端點,該端點根據提供的 userId 檢索用户信息。
如果無法找到任何記錄,則代碼會拋出一個簡單的自定義異常,名為 UserNotFoundException:
@GetMapping("/v1/users/{userId}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable Long userId) {
return Mono.fromCallable(() -> {
User user = userMap.get(userId);
if (user == null) {
throw new UserNotFoundException("User not found with ID: " + userId);
}
return new ResponseEntity<>(user, HttpStatus.OK);
});
}我們的 <em >UserNotFoundException</em > 繼承自 <em >RunTimeException</em >>:
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}由於我們擁有一個 GlobalExceptionHandler 自定義處理程序,該處理程序繼承了 ResponseEntityExceptionHandler,因此 ProblemDetail 成為默認的異常格式。為了測試這一點,我們可以嘗試通過不支持的 HTTP 方法訪問應用程序,例如 POST,以查看異常格式。
當拋出 MethodNotAllowedException 時,ResponseEntityExceptionHandler 將處理該異常並以 ProblemDetail 格式生成響應:
curl --location --request POST 'localhost:8080/v1/users/1'這會導致響應中出現 ProblemDetail 對象:
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Supported methods: [GET]",
"instance": "/users/1"
}3.4. 擴展 ProblemDetail 異常,在 Spring WebFlux 中添加自定義屬性
讓我們通過為 UserNotFoundException 提供異常處理程序來擴展示例,該程序將向 ProblemDetail 響應中添加自定義對象。
ProblemDetail 對象包含一個 properties 屬性,該屬性接受一個 String 作為鍵和值作為任何 Object。
我們將添加一個名為 ErrorDetails 的自定義對象。此對象包含錯誤代碼和消息,以及帶有附加詳細信息和解決問題説明的錯誤參考 URL:
@JsonSerialize(using = ErrorDetailsSerializer.class)
public enum ErrorDetails {
API_USER_NOT_FOUND(123, "User not found", "http://example.com/123");
@Getter
private Integer errorCode;
@Getter
private String errorMessage;
@Getter
private String referenceUrl;
ErrorDetails(Integer errorCode, String errorMessage, String referenceUrl) {
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.referenceUrl = referenceUrl;
}
}為了覆蓋 <em UserNotException</em> 的錯誤行為,我們需要在 <em GlobalExceptionHandler</em> 類中提供一個錯誤處理程序。該處理程序應將 <em API_USER_NOT_FOUND</em> 屬性設置到 <em ErrorDetails</em> 對象的,以及從 <em ProblemDetail</em> 對象提供的任何其他錯誤詳情:
@ExceptionHandler(UserNotFoundException.class)
protected ProblemDetail handleNotFound(RuntimeException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problemDetail.setTitle("User not found");
problemDetail.setType(URI.create("https://example.com/problems/user-not-found"));
problemDetail.setProperty("errors", List.of(ErrorDetails.API_USER_NOT_FOUND));
return problemDetail;
}我們還需要一個 ErrorDetailsSerializer 和 ProblemDetailSerializer 以自定義響應格式。
ErrorDetailsSerializer 負責格式化我們的自定義錯誤對象,包括錯誤代碼、錯誤消息和引用詳情:
public class ErrorDetailsSerializer extends JsonSerializer<ErrorDetails> {
@Override
public void serialize(ErrorDetails value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeStringField("code", value.getErrorCode().toString());
gen.writeStringField("message", value.getErrorMessage());
gen.writeStringField("reference", value.getReferenceUrl());
gen.writeEndObject();
}
}ProblemDetailSerializer 負責格式化ProblemDetail 對象以及自定義對象(藉助ErrorDetailsSerializer):
public class ProblemDetailsSerializer extends JsonSerializer<ProblemDetail> {
@Override
public void serialize(ProblemDetail value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeObjectField("type", value.getType());
gen.writeObjectField("title", value.getTitle());
gen.writeObjectField("status", value.getStatus());
gen.writeObjectField("detail", value.getDetail());
gen.writeObjectField("instance", value.getInstance());
gen.writeObjectField("errors", value.getProperties().get("errors"));
gen.writeEndObject();
}
}現在,當我們嘗試使用無效的 userId 訪問該端點時,我們應該收到包含自定義屬性的錯誤消息:
$ curl --location 'localhost:8080/v1/users/1'這會導致 ProblemDetail 對象以及自定義屬性生成。
{
"type": "https://example.com/problems/user-not-found",
"title": "User not found",
"status": 404,
"detail": "User not found with ID: 1",
"instance": "/users/1",
"errors": [
{
"errorCode": 123,
"errorMessage": "User not found",
"referenceUrl": "http://example.com/123"
}
]
}我們還可以使用 ErrorResponseException,它實現了 ErrorResponse,以暴露 HTTP 狀態碼、響應頭和內容,遵循 RFC 7807 ProblemDetail 協議。
在這些示例中,我們使用 ResponseEntityExceptionHandler 來處理全局異常。 此外,AbstractErrorWebExceptionHandler 也可以用於處理全局 WebFlux 異常。
4. 自定義異常
儘管 <em >ProblemDetail</em> 格式在添加自定義屬性時很有幫助且靈活,但在某些情況下,我們可能更傾向於拋出一個自定義錯誤對象,其中包含所有錯誤詳情。 <strong >在這種情況下,在 Spring 中使用自定義異常可以提供一種清晰、更具體、更一致的方法來處理代碼中的錯誤和異常</strong >。
5. 在 Spring WebFlux 中實現自定義異常
讓我們考慮使用自定義對象作為響應,而不是使用 ProblemDetail:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CustomErrorResponse {
private String traceId;
private OffsetDateTime timestamp;
private HttpStatus status;
private List<ErrorDetails> errors;
}為了拋出此自定義對象,我們需要一個自定義異常:
public class CustomErrorException extends RuntimeException {
@Getter
private CustomErrorResponse errorResponse;
public CustomErrorException(String message, CustomErrorResponse errorResponse) {
super(message);
this.errorResponse = errorResponse;
}
}讓我們創建一個新的版本,v2,該版本拋出此自定義異常。為了簡化,一些字段,如traceId,將被填充為隨機值:
@GetMapping("/v2/users/{userId}")
public Mono<ResponseEntity<User>> getV2UserById(@PathVariable Long userId) {
return Mono.fromCallable(() -> {
User user = userMap.get(userId);
if (user == null) {
CustomErrorResponse customErrorResponse = CustomErrorResponse
.builder()
.traceId(UUID.randomUUID().toString())
.timestamp(OffsetDateTime.now().now())
.status(HttpStatus.NOT_FOUND)
.errors(List.of(ErrorDetails.API_USER_NOT_FOUND))
.build();
throw new CustomErrorException("User not found", customErrorResponse);
}
return new ResponseEntity<>(user, HttpStatus.OK);
});
}我們需要在 GlobalExceptionHandler 中添加一個處理程序,以格式化輸出響應中的異常:
@ExceptionHandler({CustomErrorException.class})
protected ResponseEntity<CustomErrorResponse> handleCustomError(RuntimeException ex) {
CustomErrorException customErrorException = (CustomErrorException) ex;
return ResponseEntity.status(customErrorException.getErrorResponse().getStatus()).body(customErrorException.getErrorResponse());
}如果嘗試使用無效的 userId 訪問該端點,我們應該收到帶有自定義屬性的錯誤:
$ curl --location 'localhost:8080/v2/users/1'這會導致 CustomErrorResponse 對象作為響應:
{
"traceId": "e3853069-095d-4516-8831-5c7cfa124813",
"timestamp": "2023-04-28T15:36:41.658289Z",
"status": "NOT_FOUND",
"errors": [
{
"code": "123",
"message": "User not found",
"reference": "http://example.com/123"
}
]
}6. 結論
本文介紹瞭如何啓用和使用 Spring Framework 提供的 ProblemDetail RFC7807 異常格式,並學習瞭如何在 Spring WebFlux 中創建和處理自定義異常。