数据存储与缓存

在 Vert.x 中使用 Hibernate Reactive

本指南展示如何结合 Hibernate Reactive 构建高性能响应式 Vert.x 服务。

你将构建什么

前置要求

  • 文本编辑器或 IDE

  • Java 11 或更高版本

  • Apache Maven

  • Docker 环境

  • 用于发起命令行测试请求的 HTTPie 或 curl

创建工程项目

这是你应该使用的 pom.xml 文件配置:

该项目引入了 Vert.x Mutiny 绑定,因此你可以使用优雅的 Mutiny 响应式编程模型来组合异步操作。

服务端实现

核心服务仅由两个核心类构成:

  1. Product:标准 JPA 实体映射类;

  2. MainVerticle:配置 HTTP 路由与 Hibernate Reactive 会话工厂的核心 Verticle。

实体类定义

打开 src/main/java/io/vertx/howtos/hr/Product.java 查看 JPA 实体映射:

package io.vertx.howtos.hr;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.math.BigDecimal;

@Entity
public class Product {

  @Id
  @GeneratedValue
  private Long id;

  @Column(unique = true)
  private String name;

  @Column(nullable = false)
  private BigDecimal price;

  public Product() {
  }

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public BigDecimal getPrice() {
    return price;
  }

  public void setPrice(BigDecimal price) {
    this.price = price;
  }
}

核心 Verticle 实现

打开 src/main/java/io/vertx/howtos/hr/MainVerticle.java:

package io.vertx.howtos.hr;

import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.mutiny.core.Vertx;
import io.vertx.mutiny.core.http.HttpServer;
import io.vertx.mutiny.ext.web.Router;
import io.vertx.mutiny.ext.web.RoutingContext;
import io.vertx.mutiny.ext.web.handler.BodyHandler;
import org.hibernate.reactive.mutiny.Mutiny;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.PostgreSQLContainer;

import javax.persistence.Persistence;
import java.util.List;
import java.util.Map;

// tag::preamble[]
public class MainVerticle extends AbstractVerticle {

  private static final Logger logger = LoggerFactory.getLogger(MainVerticle.class);
  private Mutiny.SessionFactory emf;  (1)

  @Override
  public Uni<Void> asyncStart() {
// end::preamble[]

    // tag::hr-start[]
    Uni<Void> startHibernate = Uni.createFrom().deferred(() -> {
      var pgPort = config().getInteger("pgPort", 5432);
      var props = Map.of("javax.persistence.jdbc.url", "jdbc:postgresql://localhost:" + pgPort + "/postgres");  (1)

      emf = Persistence
        .createEntityManagerFactory("pg-demo", props)
        .unwrap(Mutiny.SessionFactory.class);

      return Uni.createFrom().voidItem();
    });

    startHibernate = vertx.executeBlocking(startHibernate)  (2)
      .onItem().invoke(() -> logger.info("✅ Hibernate Reactive is ready"));
    // end::hr-start[]

    // tag::routing[]
    Router router = Router.router(vertx);

    BodyHandler bodyHandler = BodyHandler.create();
    router.post().handler(bodyHandler::handle);

    router.get("/products").respond(this::listProducts);
    router.get("/products/:id").respond(this::getProduct);
    router.post("/products").respond(this::createProduct);
    // end::routing[]

    // tag::async-start[]
    Uni<HttpServer> startHttpServer = vertx.createHttpServer()
      .requestHandler(router)
      .listen(8080)
      .onItem().invoke(() -> logger.info("✅ HTTP server listening on port 8080"));

    return Uni.combine().all().unis(startHibernate, startHttpServer).discardItems();  (1)
    // end::async-start[]
  }

  // tag::crud-methods[]
  private Uni<List<Product>> listProducts(RoutingContext ctx) {
    return emf.withSession(session -> session
      .createQuery("from Product", Product.class)
      .getResultList());
  }

  private Uni<Product> getProduct(RoutingContext ctx) {
    long id = Long.parseLong(ctx.pathParam("id"));
    return emf.withSession(session -> session
      .find(Product.class, id))
      .onItem().ifNull().continueWith(Product::new);
  }

  private Uni<Product> createProduct(RoutingContext ctx) {
    Product product = ctx.body().asPojo(Product.class);
    return emf.withSession(session -> session.
      persist(product)
      .call(session::flush)
      .replaceWith(product));
  }
  // end::crud-methods[]

  public static void main(String[] args) {

    long startTime = System.currentTimeMillis();

    logger.info("🚀 Starting a PostgreSQL container");

    // tag::tc-start[]
    PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:11-alpine")
      .withDatabaseName("postgres")
      .withUsername("postgres")
      .withPassword("vertx-in-action");

    postgreSQLContainer.start();
    // end::tc-start[]

    long tcTime = System.currentTimeMillis();

    logger.info("🚀 Starting Vert.x");

    // tag::vertx-start[]
    Vertx vertx = Vertx.vertx();

    DeploymentOptions options = new DeploymentOptions().setConfig(new JsonObject()
      .put("pgPort", postgreSQLContainer.getMappedPort(5432))); (1)

    vertx.deployVerticle(MainVerticle::new, options).subscribe().with(  (2)
      ok -> {
        long vertxTime = System.currentTimeMillis();
        logger.info("✅ Deployment success");
        logger.info("💡 PostgreSQL container started in {}ms", (tcTime - startTime));
        logger.info("💡 Vert.x app started in {}ms", (vertxTime - tcTime));
      },
      err -> logger.error("🔥 Deployment failure", err));
    // end::vertx-start[]
  }
}

运行与测试

使用 Maven 启动程序:

mvn compile exec:java

Testcontainers 将自动拉起一个临时 PostgreSQL 容器并完成表结构初始化。

使用 HTTPie 进行 CRUD 测试:

  1. 创建新商品:

http POST :8080/products name="Vert.x in Action"
  1. 查询所有商品:

http GET :8080/products

总结

本文档展示了:

  • 如何在 Vert.x 中使用 Hibernate Reactive 取代传统阻塞式 Hibernate ORM;

  • 如何配合 Mutiny 响应式流处理异步持久化会话;

  • 如何基于 Testcontainers 实现零配置本地集成测试。