1. Overview
1.概述
In this tutorial, we’ll explore how to create reactive REST APIs with Micronaut and MongoDB.
在本教程中,我们将探讨如何使用 Micronaut 和 MongoDB 创建反应式 REST API。
Micronaut is a framework for constructing microservices and serverless applications on the Java Virtual Machine (JVM).
Micronaut 是一个在 Java 虚拟机(JVM)上构建微服务和无服务器应用程序的框架。
We’ll look at how to create entities, repositories, services, and controllers using Micronaut.
我们将了解如何使用 Micronaut 创建实体、存储库、服务和控制器。
2. Project Setup
2.项目设置
For our code example, we’ll create a CRUD application that stores and retrieves books from a MongoDB database. To start with, let’s create a Maven project using Micronaut Launch, set up the dependencies, and configure the database.
在代码示例中,我们将创建一个 CRUD 应用程序,用于从 MongoDB 数据库中存储和检索书籍。首先,让我们使用 Micronaut Launch 创建一个 Maven 项目,设置依赖关系并配置数据库。
2.1. Initializing the Project
2.1.初始化项目
Let’s start by creating a new project using Micronaut Launch. We’ll select the settings below:
首先,让我们使用 Micronaut Launch 创建一个新项目。我们将选择以下设置:
- Application Type: Micronaut Application
- Java Version: 17
- Build Tool: Maven
- Language: Java
- Test Framework: JUnit
Additionally, we need to provide the Micronaut version, base package, and a name for our project. To include MongoDB and reactive support, we’ll add the following features:
此外,我们还需要提供 Micronaut 版本、基础软件包和项目名称。为了包含 MongoDB 和反应式支持,我们将添加以下功能:
- reactor – to enable reactive support.
- mongo-reactive – to enable MongoDB Reactive Streams support.
- data-mongodb-reactive – to enable reactive MongoDB repositories.
Once we’ve selected the above features, we can generate and download the project. Then, we can import the project into our IDE.
选择上述功能后,我们就可以生成并下载项目。然后,我们就可以将项目导入集成开发环境。
2.2. MongoDB Setup
2.2.MongoDB 设置
There are multiple ways to set up a MongoDB database. For instance, we may install MongoDB locally, use a cloud service like MongoDB Atlas, or use a Docker container.
建立 MongoDB 数据库有多种方法。例如,我们可以在本地安装MongoDB,使用 MongoDB Atlas 等云服务,或使用Docker 容器。
After this, we need to configure the connection details in the already generated application.properties file:
之后,我们需要在已生成的 application.properties 文件中配置连接详细信息:
mongodb.uri=mongodb://${MONGO_HOST:localhost}:${MONGO_PORT:27017}/someDb
Here, we have added the default host and port for the database as localhost and 27017 respectively.
在这里,我们将数据库的默认主机和端口分别添加为 localhost 和 27017 。
3. Entities
3.实体
Now that we have our project set up, let’s look at how to create entities. We’ll create a Book entity that maps to a collection in the database:
现在我们的项目已经建立,让我们来看看如何创建实体。我们将创建一个 Book 实体,该实体将映射到数据库中的一个集合:
@Serdeable
@MappedEntity
public class Book {
@Id
@Generated
@Nullable
private ObjectId id;
private String title;
private Author author;
private int year;
}
The @Serdeable annotation indicates that the class can be serialized and deserialized. Since we’ll pass this entity in our request and response, it needs to be made serializable. This is the same as implementing the Serializable interface.
@Serdeable注解表示该类可以被序列化和反序列化。由于我们将在请求和响应中传递此实体,因此需要使其可序列化。这等同于实现 Serializable 接口。
To map the class to a database collection, we use the @MappedEntity annotation. While writing or reading from the database, Micronaut uses this class to convert the database document into a Java object and vice-versa. This is parallel to the @Document annotation in Spring Data MongoDB.
为了将类映射到数据库集合,我们使用了@MappedEntity注解。在写入或读取数据库时,Micronaut 使用该类将数据库文档转换为 Java 对象,反之亦然。这与 Spring Data MongoDB 中的 @Document 注解类似。
We annotate the id field with @Id to indicate that it’s the primary key for the entity. Additionally, we annotate it with @Generated to indicate that the database generates the value. The @Nullable annotation is used to indicate that the field can be null as the id field will be null when the entity is created.
我们用 @Id 对 id 字段进行注释,以表明它是实体的主键。此外,我们还用 @Generated 对其进行注释,以表示数据库生成了该值。@Nullable注解用于表示字段可以为空,因为在创建实体时,id字段将为空。
Similarly, let’s create an Author entity:
同样,让我们创建一个 Author 实体:
@Serdeable
public class Author {
private String firstName;
private String lastName;
}
We don’t need to annotate this class with @MappedEntity as it will be embedded in the Book entity.
我们不需要用 @MappedEntity 来注解这个类,因为它将被嵌入到 Book 实体中。
4. Repositories
4.资料库
Next, let’s create a repository to store and retrieve the books from the MongoDB database. Micronaut provides several pre-defined interfaces to create repositories.
接下来,让我们创建一个存储库,用于从 MongoDB 数据库中存储和检索图书。Micronaut 提供了多个预定义接口来创建存储库。
We’ll use the ReactorCrudRepository interface to create a reactive repository. This interface extends the CrudRepository interface and adds support for reactive streams.
我们将使用 ReactorCrudRepository 接口来创建反应式存储库。该接口扩展了 CrudRepository 接口,并增加了对反应流的支持。
Additionally, we’ll annotate the repository with @MongoRepository to indicate that it’s a MongoDB repository. This also directs Micronaut to create a bean for this class:
此外,我们将用 @MongoRepository 对版本库进行注解,以表明它是 MongoDB 版本库。这也会指示 Micronaut 为该类创建一个 Bean:
@MongoRepository
public interface BookRepository extends ReactorCrudRepository<Book, ObjectId> {
@MongoFindQuery("{year: {$gt: :year}}")
Flux<Book> findByYearGreaterThan(int year);
}
We’ve extended the ReactorCrudRepository interface and provided the Book entity and the type of the ID as generic parameters.
我们扩展了 ReactorCrudRepository 接口,并提供了 Book 实体和 ID 类型作为通用参数。
Micronaut generates an implementation of the interface at compile time. It contains methods to save, retrieve, and delete books from the database. We’ve added a custom method to find books published after a given year. The @MongoFindQuery annotation is used to specify a custom query.
Micronaut会在编译时生成接口的实现。我们添加了一个自定义方法来查找给定年份之后出版的书籍。@MongoFindQuery注解用于指定自定义查询。
In our query, we use the :year placeholder to indicate that the value will be provided at runtime. The $gt operator is similar to the > operator in SQL.
在我们的查询中,我们使用 :year 占位符来表示值将在运行时提供。$gt 操作符类似于 SQL 中的 > 操作符。
5. Services
5.服务
Services are employed to encapsulate the business logic and are typically injected into the controllers. Additionally, they may encompass other functionalities such as validation, error handling, and logging.
服务用于封装业务逻辑,通常注入到控制器中。此外,它们还可能包含其他功能,如验证、错误处理和日志记录。
We’ll create a BookService using the BookRepository to store and retrieve books:
我们将使用 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);
}
}
Here, we inject the BookRepository into the constructor using the constructor injection. The @Singleton annotation indicates that only one instance of the service will be created. This is similar to the @Component annotation of Spring Boot.
在这里,我们使用构造器注入将 BookRepository 注入到构造器中。@Singleton注解表示将只创建一个服务实例。这与 Spring Boot 的 @Component 注解类似。
Next, we have the save(), findById(), update(), and deleteById() methods to save, find, update, and delete books from the database. The block() method blocks the execution until the result is available.
接下来,我们将使用 save()、findById()、update() 和 deleteById() 方法来保存、查找、更新和删除数据库中的图书。
Finally, we have a findByYearGreaterThan() method to find books published after a given year.
最后,我们有一个findByYearGreaterThan()方法来查找特定年份之后出版的图书。
6. Controllers
6.控制器
Controllers are used to handle the incoming requests and return the response. In Micronaut, we can use annotations to create controllers and configure routing based on different paths and HTTP methods.
控制器用于处理传入的请求并返回响应。在 Micronaut 中,我们可以使用注解创建控制器,并根据不同的路径和 HTTP 方法配置路由。
6.1. Controller
6.1.控制器
We’ll create a BookController to handle the requests related to books:
我们将创建一个 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();
}
}
}
We have annotated the class with @Controller to indicate it is a controller. We have also specified the base path for the controller as /books.
我们用 @Controller 对该类进行了注解,以表明它是一个控制器。我们还指定了控制器的基本路径 /books。
Let’s look at some important parts of the controller:
让我们来看看控制器的几个重要部分:
- First, we inject the BookService into the constructor.
- Then, we have a createBook() method to create a new book. The @Post annotation indicates the method handles the POST requests.
- Since we want to convert the incoming request body to a Book object, we’ve used the @Body annotation.
- When the book is saved successfully, an ObjectId will be returned. We’ve used the @Nullable annotation to indicate that the value can be null in case the book isn’t saved.
- Similarly, we have an updateBook() method to update an existing book. We used the @Put annotation since the method handles the PUT requests.
- The methods return a string response indicating whether the book was created or updated successfully.
6.2. Path Variables
6.2.路径变量
To extract values from the path, we can use path variables. To demonstrate this, let’s add methods to find and delete a book by its ID:
要从路径中提取值,我们可以使用路径变量。为了演示这一点,让我们添加通过 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);
}
Path variables are indicated using curly braces in the path. In this example, {id} is a path variable that will be extracted from the path and passed to the method.
路径变量在路径中使用大括号表示。在本例中,{id} 是一个路径变量,将从路径中提取并传递给方法。
By default, the name of the path variable should match the name of the method parameter. This is the case with the deleteBook() method. In case it doesn’t match, we can use the @PathVariable annotation to specify a different name for the path variable. This is the case with the findById() method.
默认情况下,路径变量的名称应与方法参数的名称一致。deleteBook() 方法就是这种情况。如果不匹配,我们可以使用 @PathVariable 注解为路径变量指定一个不同的名称。findById() 方法就是这种情况。
6.3. Query Parameters
6.3.查询参数
We can use query parameters to extract values from the query string. Let’s add a method to find books published after a given year:
我们可以使用查询参数从查询字符串中提取值。让我们添加一个方法来查找给定年份之后出版的图书:
@Get("/published-after")
public Flux<Book> findByYearGreaterThan(@QueryValue("year") int year) {
return bookService.findByYearGreaterThan(year);
}
@QueryValue indicates that the value will be provided as a query parameter. Additionally, we need to specify the query parameter’s name as the annotation’s value.
@QueryValue 表示该值将作为查询参数提供。此外,我们还需要指定查询参数的名称作为注解的值。
When we make a request to this method, we’ll append a year parameter to the URL and provide its value.
当我们向该方法发出请求时,我们将在 URL 中附加一个 year 参数,并提供其值。
7. Testing
7.测试
We can test the application using either curl or an application like Postman. Let’s use curl to test the application.
我们可以使用 curl 或类似 Postman 的应用程序来测试应用程序。 让我们使用 curl 测试应用程序。
7.1. Create a Book
7.1.创建图书</em
Let’s create a book using a POST request:
让我们使用 POST 请求创建一本书:
curl --request POST \
--url http://localhost:8080/books \
--header 'Content-Type: application/json' \
--data '{
"title": "1984",
"year": 1949,
"author": {
"firstName": "George",
"lastName": "Orwel"
}
}'
First, we use the -request POST option to indicate that the request is a POST request. Then we provide headers using the -header option. Here, we set the content type as application/json. Finally, we have used the -data option to specify the request body.
首先,我们使用 -request POST 选项来表示该请求是一个 POST 请求。然后,我们使用 -header 选项提供头信息。在这里,我们将内容类型设置为 application/json。最后,我们使用 -data 选项指定请求主体。
Here’s a sample response:
下面是一个答复样本:
Book created with id: 650e86a7f0f1884234c80e3f
7.2. Find a Book
7.2.查找图书</em
Next, let’s find the book we just created:
接下来,让我们找到刚刚创建的图书:
curl --request GET \
--url http://localhost:8080/books/650e86a7f0f1884234c80e3f
This returns the book with the ID 650e86a7f0f1884234c80e3f .
返回 ID 为 650e86a7f0f1884234c80e3f 的图书。
7.3. Update a Book
7.3.更新图书</em
Next, let’s update the book. We have a typo in the author’s last name. So let’s fix it:
接下来,让我们更新这本书。我们在作者的姓氏中发现了一个错字。因此,让我们来修正它:
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
}'
If we try to find the book again, we’ll see that the author’s last name is now Orwell.
如果我们再试着找到这本书,就会发现作者现在姓Orwell。
7.4. Custom Query
7.4 自定义查询
Next, let’s find all the books published after 1940:
接下来,让我们找出 1940 年之后出版的所有书籍:
curl --request GET \
--url 'http://localhost:8080/books/published-after?year=1940'
When we execute this command, it calls our API and returns a list of all the books published after 1940 in a JSON array:
当我们执行该命令时,它会调用我们的 API,并以 JSON 数组的形式返回 1940 之后出版的所有书籍的列表:
[
{
"id": {
"$oid": "650e86a7f0f1884234c80e3f"
},
"title": "1984",
"author": {
"firstName": "George",
"lastName": "Orwell"
},
"year": 1949
}
]
Similarly, if we try to find all the books published after 1950, we’ll get an empty array:
同样,如果我们试图查找 1950 年以后出版的所有书籍,我们将得到一个空数组:
curl --request GET \
--url 'http://localhost:8080/books/published-after?year=1950'
[]
8. Error Handling
8. 错误处理
Next, let’s look at a few ways to handle errors in the application. We’ll look at two common scenarios:
接下来,我们来看看处理应用程序错误的几种方法。我们将了解两种常见的情况:
- The book isn’t found when trying to get, update, or delete it.
- Wrong input is provided when creating or updating a book.
8.1. Bean Validation
8.1.Bean验证
Firstly, let’s look at how to handle wrong input. For this, we can use the Bean Validation API of Java.
首先,让我们看看如何处理错误输入。为此,我们可以使用 Java 的 Bean Validation API。
Let’s add a few constraints to the Book class:
让我们为 Book 类添加几个约束条件:
public class Book {
@NotBlank
private String title;
@NotNull
private Author author;
// ...
}
The @NotBlank annotation indicates that the title cannot be blank. Similarly, we use the @NotNull annotation to indicate that the author cannot be null.
@NotBlank 注解表示标题不能为空。同样,我们使用 @NotNull 注解表示作者不能为空。
Then, to enable input validation in our controller, we need to use the @Valid annotation:
然后,要在控制器中启用输入验证,我们需要使用 @Valid 注解:
@Post
public String createBook(@Valid @Body Book book) {
// ...
}
When the input is invalid, the controller returns a 400 Bad Request response with a JSON body containing the details of the error:
当输入无效时,控制器会返回一个 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. Custom Error Handler
8.2 自定义错误处理程序
In the above example, we can see how Micronaut handles errors by default. However, if we want to change this behavior, we can create a custom error handler.
在上面的例子中,我们可以看到 Micronaut 默认是如何处理错误的。不过,如果我们想改变这种行为,可以创建一个自定义错误处理程序。
Since the validation errors are instances of the ConstraintViolation class, let’s create a custom error handling method that handles ConstraintViolationException:
由于验证错误是 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());
}
When any controller throws a ConstraintViolationException, Micronaut invokes this method. It then returns a 400 Bad Request response with a JSON body containing the details of the error:
当任何控制器抛出 ConstraintViolationException 时, Micronaut 会调用此方法。然后,它会返回一个 400 Bad Request 响应,其中的 JSON 主体包含错误的详细信息:
[
"createBook.book.author must not be null",
"createBook.book.title must not be blank"
]
8.3. Custom Exception
8.3.自定义异常
Next, let’s look at how to handle the case when the book isn’t found. In this case, we can create a custom exception:
接下来,让我们看看如何处理找不到图书的情况。在这种情况下,我们可以创建一个自定义异常:
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(long id) {
super("Book with id " + id + " not found");
}
}
We can then throw this exception from the controller:
然后,我们就可以从控制器中抛出这个异常:
@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;
}
}
When the book isn’t found, the controller throws a BookNotFoundException.
找不到图书时,控制器会抛出 BookNotFoundException 异常。
Finally, we can create a custom error-handling method that handles BookNotFoundException:
最后,我们可以创建一个自定义错误处理方法,用于处理 BookNotFoundException 异常:
@Error(exception = BookNotFoundException.class)
public MutableHttpResponse<String> onBookNotFound(BookNotFoundException ex) {
return HttpResponse.notFound(ex.getMessage());
}
When a non-existing book ID is provided, the controller returns a 404 Not Found response with a JSON body containing the details of the error:
如果提供的图书 ID 不存在,控制器会返回 404 Not Found(未找到)响应,其中的 JSON 主体包含错误的详细信息:
Book with id 650e86a7f0f1884234c80e3f not found
9. Conclusion
9.结论
In this article, we looked at how to create a REST API using Micronaut and MongoDB. First, we looked at how to create a MongoDB repository, a simple controller, and how to use path variables and query parameters. Then, we tested the application using curl. Finally, we looked at how to handle errors in the controllers.
在本文中,我们探讨了如何使用 Micronaut 和 MongoDB 创建 REST API。首先,我们了解了如何创建 MongoDB 存储库、简单控制器以及如何使用路径变量和查询参数。然后,我们使用 curl 测试了应用程序。最后,我们了解了如何处理控制器中的错误。
The complete source code for the application is available over on GitHub.
该应用程序的完整源代码可在 GitHub 上获取。