Spring Boot REST API 最佳实践 - 第三章
在本章节教程中,我们将了解如何实现 FindById 和 DeleteById 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 仓库中找到本教程的示例代码。
实现 GET /api/bookmarks/{id} API 端点
我们希望实现一个根据指定 id 获取单个资源的 API 端点,如果找到,以 HTTP 状态码 200 返回。如果未找到,则返回 HTTP 状态码 404,可选择返回包含异常信息的响应体。
实现 GET /api/bookmarks/{id} API
端点,如下:
BookmarkRepository.java
:
interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
@Query("""
SELECT
new com.sivalabs.bookmarks.domain.BookmarkDTO(b.id, b.title, b.url, b.createdAt)
FROM Bookmark b
WHERE b.id = ?1
""")
Optional<BookmarkDTO> findBookmarkById(Long id);
}
BookmarkService.java
:
@Service
@Transactional(readOnly = true)
public class BookmarkService {
private final BookmarkRepository repo;
//...
//...
public Optional<BookmarkDTO> findById(Long id) {
return repo.findBookmarkById(id);
}
}
BookmarkController.java
:
@RestController
@RequestMapping("/api/bookmarks")
class BookmarkController {
private final BookmarkService bookmarkService;
//...
//...
@GetMapping("/{id}")
ResponseEntity<BookmarkDTO> findById(@PathVariable(name = "id") Long id) {
return bookmarkService.findById(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
}
现在,你可以启动应用,并使用 CURL 调用 API 端点,如下:
$ curl --location 'http://localhost:8080/api/bookmarks/1'
// response
{"id":1,"title":"SivaLabs blog","url":"https://sivalabs.in","createdAt":"2023-08-23T04:24:17.975268Z"}
实现 DELETE /api/bookmarks/{id} API 端点
我们将遵循类似的模式,实现用于删除书签的 DELETE /api/bookmarks/{id}
API 端点。
BookmarkService.java
:
@Service
@Transactional(readOnly = true)
public class BookmarkService {
private final BookmarkRepository repo;
//...
//...
@Transactional
public void delete(Long postId) {
Bookmark entity = repo.findById(postId)
.orElseThrow(()-> BookmarkNotFoundException.of(postId));
repo.delete(entity);
}
}
BookmarkController.java
:
@RestController
@RequestMapping("/api/bookmarks")
class BookmarkController {
private final BookmarkService bookmarkService;
//...
//...
@DeleteMapping("/{id}")
void delete(@PathVariable(name = "id") Long id) {
bookmarkService.delete(id);
}
}
使用 CURL 调用 API 端点,如下:
$ curl --location --request DELETE 'http://localhost:8080/api/bookmarks/17'
使用 RestAssured 和 Testcontainers 测试 API 端点
为 API 端点编写测试,如下:
package com.sivalabs.bookmarks.api.controllers;
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class BookmarkControllerTests {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>(DockerImageName.parse("postgres:15.4-alpine"));
@LocalServerPort
private Integer port;
@Autowired
private BookmarkService bookmarkService;
@BeforeEach
void setUp() {
RestAssured.port = port;
}
//...
//...
@Test
void shouldGetBookmarkByIdSuccessfully() {
CreateBookmarkCommand cmd = new CreateBookmarkCommand("SivaLabs blog", "https://sivalabs.in");
BookmarkDTO bookmark = bookmarkService.create(cmd);
given().contentType(ContentType.JSON)
.when()
.get("/api/bookmarks/{id}", bookmark.id())
.then()
.statusCode(200)
.body("id", equalTo(bookmark.id()))
.body("title", equalTo("SivaLabs blog"))
.body("url", equalTo("https://sivalabs.in"))
.body("createdAt", notNullValue())
.body("updatedAt", nullValue());
}
@Test
void shouldGet404WhenBookmarkNotExists() {
Long nonExistingId = 99999L;
given().contentType(ContentType.JSON)
.when()
.get("/api/bookmarks/{id}", nonExistingId)
.then()
.statusCode(404);
}
@Test
void shouldDeleteBookmarkByIdSuccessfully() {
CreateBookmarkCommand cmd = new CreateBookmarkCommand("SivaLabs blog", "https://sivalabs.in");
BookmarkDTO bookmark = bookmarkService.create(cmd);
given().contentType(ContentType.JSON)
.when()
.delete("/api/bookmarks/{id}", bookmark.id())
.then()
.statusCode(200);
Optional<BookmarkDTO> optionalBookmark = bookmarkService.findById(bookmark.id());
assertThat(optionalBookmark).isEmpty();
}
}
现在,你可以使用 ./mvnw test
命令行运行测试。
异常处理 - 待续
你应该有注意,到目前为止我们还没有处理过出现异常的情况。例如,如果你尝试创建一个新书签,但没有传递必填字段(title
、url
),或尝试删除一个不存在 id
的书签,那么你将得到类似下面的响应:
$ curl --location --request DELETE 'http://localhost:8080/api/bookmarks/99999'
{
"timestamp": "2023-08-23T12:37:37.772+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "com.sivalabs.bookmarks.domain.BookmarkNotFoundException: Bookmark with id=17 not found
at com.sivalabs.bookmarks.domain.BookmarkNotFoundException.of(BookmarkNotFoundException.java:9)
at com.sivalabs.bookmarks.domain.BookmarkService.lambda$delete$1(BookmarkService.java:65)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at com.sivalabs.bookmarks.domain.BookmarkService.delete(BookmarkService.java:65)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
...
at java.base/java.lang.Thread.run(Thread.java:833)",
"message": "Bookmark with id=17 not found",
"path": "/api/bookmarks/17"
}
这是 Spring Boot 返回的默认异常响应。但是,我们一般都需要自定义异常响应。我们将在 下一章 探讨如何处理异常。
总结
在本章节中,我们学习了如何实现通过指定 id 来查找和删除资源的端点。
你可以在此 GitHub 仓库中找到本教程的示例代码。
参考:https://www.sivalabs.in/spring-boot-rest-api-best-practices-part-3/