Spring Boot REST API 最佳实践 - 第四章
在 “Spring Boot REST API 最佳实践” 系列教程的前几章中,我们已经学习了如何实现 CRUD 操作。在本章节教程中,我们将了解如何为 API 实现异常处理。
- Spring Boot REST API 最佳实践 - 第一章:实现 Get Collection API
- Spring Boot REST API 最佳实践 - 第二章:实现 Create 和 Update API
- Spring Boot REST API 最佳实践 - 第三章:实现 FindById 和 DeleteById API
- Spring Boot REST API 最佳实践 - 第四章:REST API 的异常处理(本文)
你可以在此 GitHub 仓库中找到本教程的示例代码。
如 第三章节 所述,如果 Controller 中的请求处理方法抛出异常,Spring Boot 将使用默认的异常处理机制进行处理并返回响应。
如果你只关心在抛出异常时返回正确的 HTTP 状态码,你可以异常类上使用 @ResponseStatus
注解来指定使用哪个 HTTP 状态码,而不是默认的 INTERNAL_SERVER_ERROR - 500
。
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class BookmarkNotFoundException extends RuntimeException {
}
// --------------------------------------
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public class InvalidBookmarkUrlException extends RuntimeException {
}
但大多数情况下,我们需要返回一个自定义的错误响应体,并使用适当的 HTTP 状态码。让我们来看看处理异常和返回错误响应的几种方法。
处理异常的不同方法
在 Spring Boot 中有多种方式来处理异常,你可以按需使用:
- 在 Controller Handler 方法中处理异常。
- 在 Controller 层使用
@ExceptionHandler
。 - 使用
@RestControllerAdvice
处理全局异常。- Spring 的 HTTP API
ProblemDetails
(RFC 7807)。
- Spring 的 HTTP API
在 Controller Handler 方法中处理异常
如果你只想控制特定 API 端点的异常处理逻辑,这是最好的方法。
例如,在 POST /api/bookmarks
API 端点实现中,如果书签 URL 已经存在,BookmarkService
可能会抛出 DuplicateBookmarkException
(重复书签异常)。如果书签 title 包含某些被屏蔽的文字,那么 BookmarkService
可能会抛出 BookmarkTitleNotAllowedException
。因此,如果你想在 controller handler 方法中处理所有这些不同的异常,那么你可以采用这种方法。
BookmarkController.java
:
@RestController
@RequestMapping("/api/bookmarks")
class BookmarkController {
private final BookmarkService bookmarkService;
//...
//...
@PostMapping
ResponseEntity<BookmarkDTO> create(@RequestBody @Validated CreateBookmarkRequest request) {
CreateBookmarkCommand cmd = new CreateBookmarkCommand(
request.title(),
request.url()
);
try {
BookmarkDTO bookmark = bookmarkService.create(cmd);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/api/bookmarks/{id}")
.buildAndExpand(bookmark.id()).toUri();
return ResponseEntity.created(location).body(bookmark);
} catch(DuplicateBookmarkException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
} catch(BookmarkTitleNotAllowedException e) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build();
}
}
}
在 Controller 层使用 @ExceptionHandler
有时候,我们可能会在 controller 的多个 API handler 方法中以相同的方式处理相同类型的异常。例如,重复的 url
检查和 title
验证逻辑适用于创建和更新 API 端点。在这种情况下,我们可以在 controller 层使用 @ExceptionHandler
,而不是在多个地方重复编写 try-catch
逻辑,具体如下:
@RestController
@RequestMapping("/api/bookmarks")
class BookmarkController {
private final BookmarkService bookmarkService;
//...
//...
@PostMapping
ResponseEntity<BookmarkDTO> create(@RequestBody @Validated CreateBookmarkRequest request) {
CreateBookmarkCommand cmd = new CreateBookmarkCommand(request.title(), request.url());
BookmarkDTO bookmark = bookmarkService.create(cmd);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/api/bookmarks/{id}")
.buildAndExpand(bookmark.id()).toUri();
return ResponseEntity.created(location).body(bookmark);
}
@PutMapping("/{id}")
void update(@PathVariable(name = "id") Long id,
@RequestBody @Validated UpdateBookmarkRequest request) {
UpdateBookmarkCommand cmd = new UpdateBookmarkCommand(id, request.title(), request.url());
bookmarkService.update(cmd);
}
@ExceptionHandler(DuplicateBookmarkException.class)
public ResponseEntity<ApiError> handleDuplicateBookmarkException(DuplicateBookmarkException e) {
ApiError error = new ApiError(e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(BookmarkTitleNotAllowedException.class)
public ResponseEntity<ApiError> handleBookmarkTitleNotAllowedException(BookmarkTitleNotAllowedException e) {
ApiError error = new ApiError(e.getMessage());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
}
}
采用这种方法,就不必在 controller 的多个 handler 方法中重复异常处理逻辑。如果 create(...)
或 update(...)
方法抛出 BookmarkTitleNotAllowedException
或 DuplicateBookmarkException
,它们将由相应的 @ExceptionHandler
方法处理。
你也可以使用 @ExceptionHandler({DuplicateBookmarkException.class, BookmarkTitleNotAllowedException.class})
在同一个 ExceptionHandler
方法中处理多种类型的异常。在这种情况下,ExceptionHandler
方法应使用 DuplicateBookmarkException
和 BookmarkTitleNotAllowedException
的通用基本异常类 Exception
作为方法参数。
使用 @RestControllerAdvice 处理全局异常
在上一节中,我们了解了如何在 controller 层使用 @ExceptionHandler
。如果同一类型的异常可能发生在不同的 controller 中,而我们希望以相同的方式处理这些异常,该怎么办?在这种情况下,我们可以使用 @RestControllerAdvice
来处理全局异常。
创建 GlobalExceptionHandler
类,如下:
package com.sivalabs.bookmarks.api;
@RestControllerAdvice // 使用 @RestControllerAdvice 注解
public class GlobalExceptionHandler {
@ExceptionHandler(DuplicateBookmarkException.class)
public ResponseEntity<ApiError> handleDuplicateBookmarkException(DuplicateBookmarkException e) {
ApiError error = new ApiError(e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(BookmarkTitleNotAllowedException.class)
public ResponseEntity<ApiError> handleBookmarkTitleNotAllowedException(BookmarkTitleNotAllowedException e) {
ApiError error = new ApiError(e.getMessage());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
}
}
通过这种方式,我们不必在多个 controller 中重复相同的 @ExceptionHandler
逻辑。
注意:
如果在 Controller 和 GlobalExceptionHandler 中都有一个 @ExceptionHandler
来处理同一类型异常,那么 Controller 级别的 @ExceptionHandler
方法将优先处理。
使用 Problem Details 响应错误
Spring 6 实现了 HTTP API 的 Problem Details 规范(RFC 7807)。
你可以阅读《Spring Boot 3:使用 HTTP API 的 Problem Details 进行错误响应》一文,了解如何使用 ProblemDetails API 处理异常。
我们可以通过添加 spring.mvc.problemdetails.enabled=true
属性或通过继承 ResponseEntityExceptionHandler
使用 @ControllerAdvice
创建全局异常 handler 来启用 RFC 7807 响应。
以下是如何使用 ProblemDetails
API 以 RFC 7807
格式返回错误响应的快速演示。
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(BookmarkNotFoundException.class)
ProblemDetail handleBookmarkNotFoundException(BookmarkNotFoundException e) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
problemDetail.setTitle("Bookmark Not Found");
problemDetail.setType(URI.create("https://api.bookmarks.com/errors/not-found"));
problemDetail.setProperty("errorCategory", "Generic");
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
}
现在,当抛出未处理的 BookmarkNotFoundException
时,将返回以下响应:
{
"type": "https://api.bookmarks.com/errors/not-found",
"title": "Bookmark Not Found",
"status": 404,
"detail": "Bookmark with id=111 not found",
"instance": "/api/bookmarks/111",
"errorCategory": "Generic",
"timestamp": "2023-08-30T05:21:59.828411Z"
}
通过继承 ResponseEntityExceptionHandler
,你可以利用 Spring 的默认异常处理功能来处理各种常见异常,如 MethodArgumentNotValidException
、BindException
、MissingServletRequestParameterException
等。如果你想自定义其中任何一种异常的处理方法,那么你可以覆写这些方法并实现自己的逻辑。
你可以在此 GitHub 仓库 中找到本教程的示例代码。
总结
在 “Spring Boot REST API 最佳实践系列” 的最后一章中,我们了解了如何使用不同的方法实现异常处理。
希望本系列教程能够帮助到你。
参考:https://www.sivalabs.in/spring-boot-rest-api-best-practices-part-4/