Hibernate Reactive 和 Quarkus 开发高性能应用

在本文中,我们探讨使用 Hibernate Reactive 和 Quarkus 进行反应式编程的概念。

使用 Hibernate Reactive 和 Quarkus 进行反应式编程可实现高效、无阻塞的数据库操作,使应用程序更具响应性和可扩展性。通过利用这些工具,我们可以构建满足当今高性能环境需求的现代云原生应用程序。

  • Hibernate Reactive 是 Hibernate ORM 的反应式扩展,旨在与非阻塞数据库驱动程序无缝协作。
  • Quarkus 是一个针对 GraalVM 和 OpenJDK HotSpot 优化的 Kubernetes 原生 Java 框架,专为构建响应式应用程序而量身定制。
它们共同为创建高性能、可扩展且响应式的 Java 应用程序提供了一个强大的平台。


Quarkus 中的反应式编程
Quarkus 以响应式框架而闻名,它从一开始就将响应式作为其架构的基本元素。该框架拥有众多响应式功能,并由强大的生态系统提供支持。

值得注意的是,Quarkus 通过 Mutiny 提供的Uni和Multi类型利用了反应式概念,展示了对异步和事件驱动编程范式的坚定承诺。

Quarkus 的Mutiny
Mutiny 是 Quarkus 中用于处理反应式功能的主要 API。大多数扩展通过提供返回Uni和Multi的 API 来支持 Mutiny ,该 API 以非阻塞背压处理异步数据流。

我们的应用程序通过 Quarkus 提供的Uni和Multi类型利用了反应式概念。Multi表示可以异步发出多个项目的类型,类似于java.util.stream.Stream ,但具有背压处理功能。

在处理可能不受限制的数据流时,我们会使用Multi,例如实时流式传输多个银行存款。

Uni表示最多发出一个项目或一个错误的类型,类似于java.util.concurrent.CompletableFuture,但具有更强大的组合运算符。Uni用于我们期望单个结果或错误的场景,例如从数据库中提取单个银行存款。

了解PanacheEntity
当我们将 Quarkus 与 Hibernate Reactive 结合使用时,PanacheEntity类提供了一种简化的方法来使用最少的样板代码定义 JPA 实体。通过从 Hibernate 的PanacheEntityBase扩展,PanacheEntity获得了反应能力,从而能够以非阻塞方式管理实体。

这样可以有效地处理数据库操作而不会阻塞应用程序的执行,从而提高整体性能。

Maven 依赖
首先,我们将quarkus-hibernate-reactive-panache依赖项添加到我们的pom.xml中:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-reactive-panache</artifactId>
    <version>3.11.0</version>
</dependency>

现在我们已经配置了依赖项,我们可以继续在示例实现中使用它。

真实世界示例代码
银行系统通常要求较高。我们可以使用 Quarkus、Hibernate 和响应式编程等技术来实现关键服务,以解决这一问题。

在这个例子中,我们将重点实现两项特定的服务:创建银行存款以及列出和流式传输所有银行存款。

 创建银行存款实体
ORM (对象关系映射)实体对于每个基于 CRUD 的系统都至关重要。此实体允许将数据库对象映射到软件中的对象模型,从而方便数据操作。此外,正确定义存款实体对于确保系统平稳运行和准确的数据管理至关重要:

@Entity
public class Deposit extends PanacheEntity {
    public String depositCod;
    public String currency;
    public String amount;  
    // standard setters and getters
}

在这个特定的例子中, Deposit类扩展了PanacheEntity类,有效地使其成为由 Hibernate Reactive 管理的反应实体。

通过此扩展,Deposit类继承了 CRUD(创建、读取、更新、删除)操作的方法并获得了查询功能,从而大大减少了应用程序内对手动 SQL 或 JPQL 查询的需求。这种方法简化了数据库操作管理并提高了系统的整体效率。

实现存储库
在大多数情况下,我们通常利用存款实体进行所有 CRUD 操作。但是,在这个特定场景中,我们创建了一个专用的DepositRepository:

@ApplicationScoped
public class DepositRepository {
    @Inject
    Mutiny.SessionFactory sessionFactory;
    @Inject
    JDBCPool client;
    public Uni<Deposit> save(Deposit deposit) {
        return sessionFactory.withTransaction((session, transaction) -> session.persist(deposit)
          .replaceWith(deposit));
    }
    public Multi<Deposit> streamAll() {
        return client.query("SELECT depositCode, currency,amount FROM Deposit ")
          .execute()
          .onItem()
          .transformToMulti(set -> Multi.createFrom()
            .iterable(set))
          .onItem()
          .transform(Deposit::from);
    }
}

该存储库创建了一个自定义的save()方法和一个streamAll()方法,允许我们以Multi格式检索所有存款。

实现 REST 端点
现在是时候使用REST端点公开我们的反应方法了:

@Path("/deposits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class DepositResource {
    @Inject
    DepositRepository repository;
    @GET
    public Uni<Response> getAllDeposits() {
        return Deposit.listAll()
          .map(deposits -> Response.ok(deposits)
            .build());
    }
    @POST
    public Uni<Response> createDeposit(Deposit deposit) {
        return deposit.persistAndFlush()
          .map(v -> Response.status(Response.Status.CREATED)
            .build());
    }
    @GET
    @Path(
"stream")
    public Multi<Deposit> streamDeposits() {
      return repository.streamAll();
    }
}

我们可以看到,REST 服务有三种响应式方法:getAllDeposits() ,以Uni类型返回所有​​存款;还有createDeposit()方法,用于创建存款。这两种方法的返回类型都是Uni,而streamDeposits()返回的是Multi

测试
为了确保应用程序的准确性和可靠性,我们将使用JUnit 和@QuarkusTest进行集成测试。此方法涉及创建测试来验证软件的各个代码或组件,以验证其是否具有正确的功能和性能。这些测试可帮助我们在开发早期识别和纠正任何问题,最终打造出更强大、更可靠的应用程序:

@QuarkusTest
public class DepositResourceIntegrationTest {
    @Inject
    DepositRepository repository;
    @Test
    public void givenAccountWithDeposits_whenGetDeposits_thenReturnAllDeposits() {
        given().when()
          .get("/deposits")
          .then()
          .statusCode(200);
   }
}

我们讨论的测试仅侧重于验证与 REST 端点的成功连接并创建存款。但是,必须注意并非所有测试都如此简单。与测试常规 Panache 实体相比,在 @QuarkusTest 中测试反应式 Panache 实体会增加复杂性。

这种复杂性源于 API 的异步特性以及所有操作必须在 Vert.x 事件循环中执行的严格要求。

首先,我们将具有测试范围的quarkus-test-hibernate-reactive-panache依赖项添加到我们的pom.xml中:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-hibernate-reactive-panache</artifactId>
    <version>3.3.3</version>
    <scope>test</scope>
</dependency>

集成测试方法应该用@RunOnVertxContext注释,这允许它们在 Vert.x 线程而不是主线程上运行。

此注释对于测试必须在事件循环上执行的组件特别有用,可以更准确地模拟真实世界的条件。

TransactionalUniAsserter是单元测试方法的预期输入类型。它的功能类似于拦截器,将每个断言方法包装在其自己的反应事务中。这允许更精确地管理测试环境,并确保每个断言方法在其自己的隔离上下文中运行:

@Test
@RunOnVertxContext
public void givenDeposit_whenSaveDepositCalled_ThenCheckCount(TransactionalUniAsserter asserter){
    asserter.execute(() -> repository.save(new Deposit("DEP20230201","10","USD")));
    asserter.assertEquals(() -> Deposit.count(), 2l);
}

现在,我们需要为streamDeposit ()编写一个测试,它返回Multi

@Test
public void givenDepositsInDatabase_whenStreamAllDeposits_thenDepositsAreStreamedWithDelay() {
    Deposit deposit1 = new Deposit("67890", "USD", "200.0");
    Deposit deposit2 = new Deposit(
"67891", "USD", "300.0");
    repository.save(deposit1)
      .await()
      .indefinitely();
    repository.save(deposit2)
      .await()
      .indefinitely();
    Response response = RestAssured.get(
"/deposits/stream")
      .then()
      .extract()
      .response();
   
// Then: the response contains the streamed books with a delay
    response.then()
      .statusCode(200);
    response.then()
      .body(
"$", hasSize(2));
    response.then()
      .body(
"[0].depositCode", equalTo("67890"));
    response.then()
      .body(
"[1].depositCode", equalTo("67891"));
}

此测试的目的是验证流式存款的功能,以被动方式使用Multi类型检索所有账户。

​​​​​​​