1. 概述
本教程将探索如何使用 Micronaut 和 MongoDB 创建响应式 REST API。
Micronaut 是一个用于在 Java 虚拟机 (JVM) 上构建微服务和无服务器应用的框架。我们将学习如何使用 Micronaut 创建实体、仓库、服务和控制器。
2. 项目搭建
我们将创建一个 CRUD 应用,用于存储和检索 MongoDB 数据库中的书籍。首先使用 Micronaut Launch 创建 Maven 项目,配置依赖项和数据库。
2.1. 初始化项目
通过 Micronaut Launch 创建新项目,选择以下配置:
- 应用类型:Micronaut Application
- Java 版本:17
- 构建工具:Maven
- 语言:Java
- 测试框架:JUnit
还需提供 Micronaut 版本、基础包名和项目名称。为包含 MongoDB 和响应式支持,添加以下特性:
- reactor – 启用响应式支持
- mongo-reactive – 启用 MongoDB Reactive Streams 支持
- data-mongodb-reactive – 启用响应式 MongoDB 仓库
选择上述特性后,生成并下载项目,然后导入 IDE。
2.2. 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 注解表示该类可序列化和反序列化。由于实体将在请求和响应中传递,必须可序列化(相当于实现 Serializable 接口)。
@MappedEntity 注解将类映射到数据库集合。Micronaut 使用该注解在数据库读写时转换文档与 Java 对象,类似于 Spring Data MongoDB 的 @Document 注解。
@Id 标记主键字段,*@Generated* 表示值由数据库生成。**@Nullable* 表示字段可为空(创建实体时 id 为空)。
类似地创建 Author 实体:
@Serdeable
public class Author {
private String firstName;
private String lastName;
}
无需添加 @MappedEntity,因为该类将嵌入 Book 实体。
4. 仓库层
创建仓库存储和检索书籍。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<Book> findByYearGreaterThan(int year) {
return bookRepository.findByYearGreaterThan(year);
}
}
通过构造函数注入 BookRepository。**@Singleton* 注解表示仅创建一个服务实例**(类似于 Spring Boot 的 @Component)。
save(), findById(), update() 和 deleteById() 方法处理数据库操作。block() 方法阻塞执行直到结果可用。
findByYearGreaterThan() 方法查找指定年份后出版的书籍。
6. 控制器层
控制器处理传入请求并返回响应。在 Micronaut 中,使用注解创建控制器并配置路由。
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 注解)
- @Body 注解将请求体转换为 Book 对象
- 保存成功时返回 ObjectId,@Nullable 表示可能为空
- 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 表示值来自查询参数,需指定参数名作为注解值。请求时在 URL 后附加 year
参数。
7. 测试
使用 curl 或 Postman 测试应用。这里使用 curl。
7.1. 创建书籍
通过 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
设置请求头(Content-Type: application/json) -
--data
指定请求体
示例响应:
Book created with id: 650e86a7f0f1884234c80e3f
7.2. 查找书籍
查找刚创建的书籍:
curl --request GET \
--url http://localhost:8080/books/650e86a7f0f1884234c80e3f
返回 ID 为 650e86a7f0f1884234c80e3f
的书籍。
7.3. 更新书籍
修正作者姓氏拼写错误:
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'
返回 1940 年后出版的书籍 JSON 数组:
[
{
"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* 表示作者不能为空。
在控制器中使用 @Valid 启用输入验证:
@Post
public String createBook(@Valid @Body Book book) {
// ...
}
输入无效时返回 400 Bad Request,响应体包含错误详情:
{
"_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. 自定义错误处理器
创建自定义错误处理器改变默认行为。验证错误是 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 时,返回 400 Bad Request 和错误详情:
[
"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:
Book with id 650e86a7f0f1884234c80e3f not found
9. 总结
本文介绍了如何使用 Micronaut 和 MongoDB 创建 REST API。首先创建 MongoDB 仓库和简单控制器,然后使用路径变量和查询参数。接着用 curl 测试应用,最后处理控制器中的错误。
完整源代码可在 GitHub 获取。