1. 概述

本教程将探索如何使用 MicronautMongoDB 创建响应式 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 仓库

Micronaut Launch 网页已选择所需项目选项

选择上述特性后,生成并下载项目,然后导入 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. 测试

使用 curlPostman 测试应用。这里使用 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 获取。


原始标题:Creating Reactive APIs With Micronaut and MongoDB | Baeldung