使用 Micronaut 和 MongoDB 建立響應式 API
1. 概述
在本教程中,我們將探索如何使用 Micronaut 和 MongoDB 建立反應式 REST API。
Micronaut 是一個用於在 Java 虛擬機器 (JVM) 上建立微服務和無伺服器應用程式的框架。
我們將了解如何使用 Micronaut 建立實體、儲存庫、服務和控制器。
2. 項目設定
對於我們的程式碼範例,我們將建立一個 CRUD 應用程序,用於從 MongoDB 資料庫儲存和檢索書籍。首先,我們使用 Micronaut Launch 建立一個 Maven 項目,設定依賴項並配置資料庫。
2.1.初始化專案
讓我們先使用Micronaut Launch建立一個新專案。我們將選擇以下設定:
- 應用程式類型:Micronaut 應用程式
- Java版本:17
- 建置工具:Maven
- 語言:Java
- 測試框架:JUnit
此外,我們需要提供 Micronaut 版本、基礎套件和專案名稱。為了包含 MongoDB 和反應式支持,我們將添加以下功能:
-
reactor
啟用反應性支援。 -
mongo-reactive
– 啟用 MongoDB Reactive Streams 支援。 -
data-mongodb-reactive
– 啟用反應式 MongoDB 儲存庫。
一旦我們選擇了上述功能,我們就可以產生並下載專案。然後,我們可以將專案匯入到我們的IDE中。
2.2. MongoDB 設定
設定 MongoDB 資料庫的方法有多種。例如,我們可以在本地安裝 MongoDB,使用 MongoDB Atlas 等雲端服務,或使用 Docker 容器。
之後,我們需要在已經產生的application.properties
檔案中設定連線詳細資訊:
mongodb.uri=mongodb://${MONGO_HOST:localhost}:${MONGO_PORT:27017}/someDb
在這裡,我們將資料庫的預設主機和連接埠分別新增為localhost
和27017
。
3. 實體
現在我們已經設定了項目,讓我們看看如何建立實體。我們將建立一個映射到資料庫中的集合的Book
實體:
@Serdeable @MappedEntity public class Book { @Id @Generated @Nullable private ObjectId id; private String title; private Author author; private int year; }
@Serdeable註解**@Serdeable
該類別可以被序列化和反序列化**。由於我們將在請求和回應中傳遞此實體,因此需要使其可序列化。這與實作Serializable
介面相同。
要將類別對應到資料庫集合,我們使用@MappedEntity
註解。在寫入或讀取資料庫時,Micronaut 使用此類將資料庫文件轉換為 Java 對象,反之亦然。這與 Spring Data MongoDB 中的@Document
註解並行。
我們用@Id
註釋id
字段,以表明它是實體的主鍵。此外,我們用@Generated
對其進行註釋,以指示資料庫產生該值。 @Nullable
註解用於指示該欄位可以為null
,因為在建立實體時id
欄位將為null
。
同樣,讓我們建立一個Author
實體:
@Serdeable public class Author { private String firstName; private String lastName; }
我們不需要使用@MappedEntity
註解此類,因為它將嵌入到Book
實體中。
4. 儲存庫
接下來,讓我們建立一個儲存庫來儲存 MongoDB 資料庫中的書籍並從中檢索書籍。 Micronaut 提供了幾個預先定義的介面來建立儲存庫。
我們將使用ReactorCrudRepository
介面來建立反應式儲存庫。此介面擴展了CrudRepository
介面並添加了對反應式流的支援。
此外,我們將使用@MongoRepository
註解該儲存庫,以表示它是 MongoDB 儲存庫。這也指示 Micronaut 為此類建立一個 bean:
@MongoRepository public interface BookRepository extends ReactorCrudRepository<Book, ObjectId> { @MongoFindQuery("{year: {$gt: :year}}") Flux<Book> findByYearGreaterThan(int year); }
我們擴展了ReactorCrudRepository
接口,並提供了Book
實體和 ID 的類型作為通用參數。
Micronaut 在編譯時產生介面的實作。它包含從資料庫中保存、檢索和刪除書籍的方法。我們新增了一個自訂方法來尋找給定年份之後出版的書籍。 @MongoFindQuery
註解用於指定自訂查詢。
在我們的查詢中,我們使用:year
佔位符來指示該值將在運行時提供。 $gt
運算子類似於 SQL 中的>
運算子。
5. 服務
服務用於封裝業務邏輯,並且通常注入到控制器中。此外,它們還可能包含其他功能,例如驗證、錯誤處理和日誌記錄。
我們將使用BookRepository
建立一個BookService
來儲存和擷取書籍:
`@Singleton
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public ObjectId save(Book book) {
Book savedBook = bookRepository.save(book).block();
return null != savedBook ? savedBook.getId() : null;
}
public Book findById(String id) {
return bookRepository.findById(new ObjectId(id)).block();
}
public ObjectId update(Book book) {
Book updatedBook = bookRepository.update(book).block();
return null != updatedBook ? updatedBook.getId() : null;
}
public Long deleteById(String id) {
return bookRepository.deleteById(new ObjectId(id)).block();
}
public Flux
return bookRepository.findByYearGreaterThan(year);
}
}`
在這裡,我們使用建構函式註入將BookRepository
注入到建構函式中。 @Singleton
註解表示只會建立一個服務實例。這和Spring Boot的@Component
註解類似。
接下來,我們使用save()
、 findById()
、 update()
和deleteById()
方法來儲存、尋找、更新和刪除資料庫中的書籍。 block()
方法會阻塞執行,直到結果可用。
最後,我們有一個findByYearGreaterThan()
方法來尋找給定年份之後出版的書籍。
6. 控制器
控制器用於處理傳入請求並回傳回應。在 Micronaut 中,我們可以使用註解來建立控制器並根據不同的路徑和 HTTP 方法配置路由。
6.1.控制器
我們將建立一個BookController
來處理與書籍相關的請求:
`@Controller("/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@Post
public String createBook(@Body Book book) {
@Nullable ObjectId bookId = bookService.save(book);
if (null == bookId) {
return "Book not created";
} else {
return "Book created with id: " + bookId.getId();
}
}
@Put
public String updateBook(@Body Book book) {
@Nullable ObjectId bookId = bookService.update(book);
if (null == bookId) {
return "Book not updated";
} else {
return "Book updated with id: " + bookId.getId();
}
}
}`
我們用@Controller
註解該類別以表明它是一個控制器。我們也將控制器的基本路徑指定為/books
。
讓我們來看看控制器的一些重要部分:
- 首先,我們將
BookService
注入到建構函式中。 - 然後,我們有一個
createBook()
方法來建立一本新書。@Post
註解指示該方法處理 POST 要求。 - 由於我們要將**傳入的請求正文轉換為
Book
對象,因此我們使用了@Body
註釋。** - 書籍保存成功後,會回傳一個
ObjectId
。我們使用@Nullable
註解來指示該值可以為 null,以防書籍未儲存。 - 類似地,我們有一個
updateBook()
方法來更新現有的書籍。我們使用@Put
註釋,因為該方法處理 PUT 請求。 - 這些方法傳回字串回應,指示圖書是否已成功建立或更新。
6.2.路徑變數
要從路徑中提取值,我們可以使用路徑變數。為了示範這一點,讓我們新增透過 ID 來尋找和刪除書籍的方法:
`@Delete("/{id}")
public String deleteBook(String id) {
Long bookId = bookService.deleteById(id);
if (0 == bookId) {
return "Book not deleted";
} else {
return "Book deleted with id: " + bookId;
}
}
@Get("/{id}")
public Book findById(@PathVariable("id") String identifier) {
return bookService.findById(identifier);
}`
路徑變數在路徑中使用花括號表示。在此範例中, {id}
是一個路徑變量,將從路徑中提取並傳遞給方法。
預設情況下,路徑變數的名稱應與方法參數的名稱相符。 deleteBook()
方法就是這種情況。如果不匹配,我們可以使用@PathVariable
註解為路徑變數指定不同的名稱。 findById()
方法就是這種情況。
6.3.查詢參數
我們可以使用查詢參數從查詢字串中提取值。讓我們添加一個方法來查找給定年份之後出版的書籍:
@Get("/published-after") public Flux<Book> findByYearGreaterThan(@QueryValue("year") int year) { return bookService.findByYearGreaterThan(year); }
@QueryValue
指示該值將作為查詢參數提供。此外,我們需要將查詢參數的名稱指定為註解的值。
當我們向此方法發出請求時,我們會將year
參數附加到 URL 並提供其值。
7. 測試
我們可以使用curl
或Postman
之類的應用程式來測試應用程式。讓我們使用curl
來測試該應用程式。
7.1.創建一Book
讓我們使用 POST 請求來建立一本書:
curl --request POST \ --url http://localhost:8080/books \ --header 'Content-Type: application/json' \ --data '{ "title": "1984", "year": 1949, "author": { "firstName": "George", "lastName": "Orwel" } }'
首先,我們使用-request POST
選項來指示該請求是POST請求。然後我們使用-header
選項提供標頭。在這裡,我們將內容類型設定為application/json
。最後,我們使用-data
選項來指定請求正文。
這是一個範例回應:
Book created with id: 650e86a7f0f1884234c80e3f
7.2.找一Book
接下來,讓我們找到我們剛剛創建的書:
curl --request GET \ --url http://localhost:8080/books/650e86a7f0f1884234c80e3f
這將返回 ID 為650e86a7f0f1884234c80e3f
的書籍。
7.3.更新一Book
接下來,我們來更新這本書。作者的姓氏有一個拼字錯誤。那麼讓我們修復它:
`curl --request PUT
--url http://localhost:8080/books
--header 'Content-Type: application/json'
--data '{
"id": {
"$oid": "650e86a7f0f1884234c80e3f"
},
"title": "1984",
"author": {
"firstName": "George",
"lastName": "Orwell"
},
"year": 1949
}'`
如果我們嘗試再次尋找這本書,我們會看到作者的姓氏現在是Orwell
。
7.4.自訂查詢
接下來,我們來找出1940年後出版的所有書籍:
curl --request GET \ --url 'http://localhost:8080/books/published-after?year=1940'
當我們執行此命令時,它會呼叫我們的 API 並以 JSON 數組傳回1940
之後出版的所有書籍的清單:
[ { "id": { "$oid": "650e86a7f0f1884234c80e3f" }, "title": "1984", "author": { "firstName": "George", "lastName": "Orwell" }, "year": 1949 } ]
同樣,如果我們嘗試尋找 1950 年之後出版的所有書籍,我們將獲得一個空數組:
curl --request GET \ --url 'http://localhost:8080/books/published-after?year=1950' []
8. 錯誤處理
接下來,讓我們來看看處理應用程式中的錯誤的幾種方法。我們將看看兩種常見的場景:
- 嘗試取得、更新或刪除該書時找不到該書。
- 創建或更新書籍時提供了錯誤的輸入。
8.1. Bean 驗證
首先,讓我們看看如何處理錯誤的輸入。為此,我們可以使用Java的Bean Validation API。
讓我們在Book
類別中加入一些約束:
public class Book { @NotBlank private String title; @NotNull private Author author; // ... }
@NotBlank
註解表示標題不能為空。同樣,我們使用@NotNull
註解來表示作者不能為null。
然後,要在控制器中啟用輸入驗證,我們需要使用@Valid
註解:
@Post public String createBook(@Valid @Body Book book) { // ... }
當輸入無效時,控制器會傳回 400 Bad Request
回應,其中包含包含錯誤詳細資訊的 JSON 正文:
{
"_links": {
"self": [
{
"href": "/books",
"templated": false
}
]
},
"_embedded": {
"errors": [
{
"message": "book.author: must not be null"
},
{
"message": "book.title: must not be blank"
}
]
},
"message": "Bad Request"
}
8.2.自訂錯誤處理程序
在上面的範例中,我們可以看到 Micronaut 預設如何處理錯誤。但是,如果我們想改變這種行為,我們可以建立一個自訂錯誤處理程序。
由於驗證錯誤是ConstraintViolation
類別的實例,因此我們建立一個處理ConstraintViolationException
的自訂錯誤處理方法:
@Error(exception = ConstraintViolationException.class) public MutableHttpResponse<String> onSavedFailed(ConstraintViolationException ex) { return HttpResponse.badRequest(ex.getConstraintViolations().stream() .map(cv -> cv.getPropertyPath() + " " + cv.getMessage()) .toList().toString()); }
當任何控制器拋出ConstraintViolationException,
Micronaut 都會呼叫此方法。然後,它會傳回 400 Bad Request 回應,其中包含包含錯誤詳細資訊的 JSON 正文:
[
"createBook.book.author must not be null",
"createBook.book.title must not be blank"
]
8.3.自訂異常
接下來我們來看看找不到書的情況下該如何處理。在這種情況下,我們可以建立一個自訂異常:
public class BookNotFoundException extends RuntimeException { public BookNotFoundException(long id) { super("Book with id " + id + " not found"); } }
然後我們可以從控制器拋出這個異常:
@Get("/{id}") public Book findById(@PathVariable("id") String identifier) throws BookNotFoundException { Book book = bookService.findById(identifier); if (null == book) { throw new BookNotFoundException(identifier); } else { return book; } }
當找不到這本書時,控制器會拋出BookNotFoundException
。
最後,我們可以建立一個自訂錯誤處理方法來處理BookNotFoundException
:
@Error(exception = BookNotFoundException.class) public MutableHttpResponse<String> onBookNotFound(BookNotFoundException ex) { return HttpResponse.notFound(ex.getMessage()); }
當提供不存在的圖書 ID 時,控制器將傳回 404 Not Found 回應,其中包含包含錯誤詳細資訊的 JSON 正文:
Book with id 650e86a7f0f1884234c80e3f not found
9. 結論
在本文中,我們了解如何使用 Micronaut 和 MongoDB 建立 REST API。首先,我們了解如何建立 MongoDB 儲存庫、一個簡單的控制器,以及如何使用路徑變數和查詢參數。然後,我們使用curl
測試了該應用程式。最後,我們研究瞭如何處理控制器中的錯誤。
該應用程式的完整原始程式碼可 在 GitHub 上取得。