Skip to content

tio-boot + Quarkus + Hibernate ORM Panache ​

1. 设计目标回顾 ​

  • tio-boot

    • 唯一对外 HTTP/网络入口
    • 管端口、路由、handler、协议
  • Quarkus

    • 只做后台运行时容器
    • 提供 CDI(Arc)、事务(@Transactional)、ORM(Hibernate + Panache)、数据源/连接池
    • 不对外启动 HTTP 端口
  • 业务代码

    • Service/Repository/Entity 全部使用 Quarkus/Panache 编程模型
    • 不感知 tio-boot

这意味着: handler 只负责把 HTTP 请求转换成业务调用;业务逻辑、事务、数据库访问都在 Quarkus Bean 内完成。


2. 必须明确移除的依赖(否则 Quarkus 一定会起 HTTP) ​

目标是“Quarkus 不启动任何 HTTP 端口”,那么只要引入了 Quarkus 的服务端 Web 能力,都会把 HTTP Server 拉起来(默认 8080),即使没有写任何 Controller。

2.1 必须移除(典型触发 HTTP 的依赖) ​

以下任意一类依赖一旦出现,就会引入 HTTP Server 能力(或非常容易触发):

  • Quarkus REST(服务端)

    • io.quarkus:quarkus-rest
    • io.quarkus:quarkus-rest-jackson
    • io.quarkus:quarkus-resteasy* / io.quarkus:quarkus-resteasy-reactive*(不同版本命名不同)
  • Servlet/Undertow

    • io.quarkus:quarkus-undertow
  • Vert.x HTTP(服务端 HTTP 层)

    • io.quarkus:quarkus-vertx-http
  • 依赖“通过 HTTP 暴露端点”的扩展(常见例子)

    • health / metrics / openapi / swagger-ui 等(这些通常依赖主 HTTP server 或 management server)

这两个依赖必须删掉,因为它们属于 Quarkus REST 服务端能力,会直接带来 HTTP:(同时也与“tio-boot 唯一 HTTP”目标冲突)

Quarkus REST/Jackson 之类属于服务端 HTTP 能力扩展,删除是必要条件。(Quarkus)


3. 数据库整合需要新增/保留的依赖(Panache 必需) ​

3.1 必需依赖 ​

在现有 quarkus-arc 基础上,增加(或保留)如下:

xml
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>

<!-- 按的数据库选择一个 JDBC 驱动扩展 -->
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

Panache 的编程模型与配置方式可参考官方指南。(Quarkus)


4. application.properties(数据源、ORM、建表策略) ​

在 src/main/resources/application.properties 配置数据库连接与 ORM 行为。

下面以 PostgreSQL 为例(也可以替换为 MySQL/MariaDB/Oracle 等对应驱动扩展)。

properties
# datasource
quarkus.console.color=false
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=postgres
quarkus.datasource.password=postgres
quarkus.datasource.jdbc.url=jdbc:postgresql://127.0.0.1:5432/demo

# 连接池(按需)
quarkus.datasource.jdbc.min-size=2
quarkus.datasource.jdbc.max-size=16

# ORM
quarkus.hibernate-orm.database.generation=update
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.log.format-sql=true

说明:

  • database.generation=update 适合开发期快速验证;生产更建议用迁移(Flyway/Liquibase)或 validate。
  • 事务由 @Transactional 控制,建议放在 Service 层。

5. 数据模型:Entity(Panache) ​

这里给一个最小可用的实体示例:User。

可以选择两种 Panache 风格:

  • Active Record:实体自己带 CRUD 方法(更省代码)
  • Repository:实体是纯数据结构,CRUD 在 Repository(更分层)

本方案更符合“业务不感知框架细节、结构清晰”的方向,推荐 Repository 风格。

5.1 Entity(使用 PanacheEntityBase) ​

java
package nexus.io.quarkus.entity;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "app_demo_user")
public class User extends PanacheEntityBase {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  public Long id;

  @Column(nullable = false, unique = true, length = 64)
  public String username;

  @Column(nullable = false, length = 128)
  public String password;

  @Column(nullable = false)
  public Long createdAt;
}

注意: 注意 启动时会自动创建数据表 ​

6. Repository(PanacheRepository) ​

java
package nexus.io.quarkus.repo;

import nexus.io.quarkus.entity.User;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class UserRepo implements PanacheRepository<User> {

  public User findByUsername(String username) {
    return find("username", username).firstResult();
  }
}

7. Service(事务边界在这里) ​

关键点:

  • 把 @Transactional 放在 Service 层
  • handler 只调用 service,不直接碰 EntityManager/Repository 的事务细节
java
package nexus.io.quarkus.service;

import java.util.Map;

import nexus.io.quarkus.entity.User;
import nexus.io.quarkus.repo.UserRepo;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

@ApplicationScoped
public class UserService {

  @Inject
  UserRepo userRepo;

  @Transactional
  public Map<String, Object> createUser(String username, String password) {
    User exists = userRepo.findByUsername(username);
    if (exists != null) {
      return Map.of("ok", false, "msg", "username already exists");
    }

    User u = new User();
    u.username = username;
    u.password = password;
    u.createdAt = System.currentTimeMillis();
    userRepo.persist(u);

    return Map.of("ok", true, "id", u.id, "username", u.username);
  }

  @Transactional
  public Map<String, Object> getUser(Long id) {
    User u = userRepo.findById(id);
    if (u == null) {
      return Map.of("ok", false, "msg", "not found");
    }
    return Map.of("ok", true, "id", u.id, "username", u.username, "createdAt", u.createdAt);
  }
}

Panache 的 Repository/Entity 用法与事务/查询模式,官方指南有完整说明。(Quarkus)


8. tio-boot Handler:只做 HTTP 适配,不做业务与事务 ​

下面示例新增一个 /user/create 和 /user/get:

  • handler 仍然由 Quarkus CDI 管理(@ApplicationScoped)
  • 必要时标记 @Unremovable,避免被构建期“未使用 Bean 移除”误伤 Quarkus 对“未使用 Bean 移除”机制与 @Unremovable 的说明见官方文档/博客。(Quarkus)
java
package nexus.io.quarkus.handler;

import nexus.io.quarkus.service.UserService;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;

import io.quarkus.arc.Unremovable;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
@Unremovable
public class UserHandler {

  @Inject
  UserService userService;

  public HttpResponse createUser(HttpRequest httpRequest) throws Exception {
    String username = httpRequest.getParam("username");
    String password = httpRequest.getParam("password");
    return TioRequestContext.getResponse().setJson(userService.createUser(username, password));
  }

  public HttpResponse getUser(HttpRequest httpRequest) throws Exception {
    Long id = httpRequest.getLong("id");
    return TioRequestContext.getResponse().setJson(userService.getUser(id));
  }
}

说明:

  • @Transactional 在 UserService.createUser() 上生效,事务会包裹 persist()。
  • handler 线程来自 tio-boot 的工作线程,但事务拦截器工作在方法调用边界上,推荐保持“事务只在 service 层”这一条不变。

9. 路由注册:从 CDI 容器拿 handler 实例 ​

原来的方式保持不变,新增 UserHandler 路由即可:

java
package nexus.io.quarkus.config;

import nexus.io.context.BootConfiguration;
import nexus.io.quarkus.handler.UserHandler;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.server.router.HttpRequestRouter;

public class WebHelloConfig implements BootConfiguration {

  public void config() {

    TioBootServer server = TioBootServer.me();
    HttpRequestRouter requestRouter = server.getRequestRouter();

    UserHandler userHandler = QuarkusBeans.get(UserHandler.class);
    requestRouter.add("/user/create", userHandler::createUser);
    requestRouter.add("/user/get", userHandler::getUser);
  }
}

10. 启动顺序(保证 DataSource/ORM 就绪后再起 tio-boot) ​

TioBootLifecycle 在 StartupEvent 后启动 tio-boot。

java
@ApplicationScoped
public class TioBootLifecycle {
  private Context context;

  void onStart(@Observes StartupEvent ev) {
    context = TioApplication.run(new WebHelloConfig());
  }

  void onStop(@Observes ShutdownEvent ev) {
    if (context != null) context.close();
  }
}

这样保证:

  • Quarkus runtime 已完成
  • Arc 容器可用
  • DataSource、Hibernate、Panache 已就绪
  • handler 从 CDI 获取时不会遇到“容器未启动”的问题

11. 验证方式 ​

11.1 创建用户 ​

GET http://127.0.0.1/user/create?username=alice&password=123

期望返回(示例):

json
{"ok":true,"id":1,"username":"alice"}

11.2 查询用户 ​

GET http://127.0.0.1/user/get?id=1

期望返回(示例):

json
{"ok":true,"id":1,"username":"alice","createdAt":1770...}

下面是一段可以直接补进那篇 README/架构说明文档里的“问题复盘与原因解释”章节,基于现在的 UserService 代码与实际报错场景整理,读者看完能理解为什么会报错、怎么修、背后的机制是什么。


12. 常见问题复盘:查询接口报 ContextNotActiveException ​

12.1 现象 ​

当访问:

GET http://127.0.0.1/user/get?id=1

出现错误:

  • jakarta.enterprise.context.ContextNotActiveException
  • 关键提示:

Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active.

这类错误通常发生在:

  • Quarkus 没有对外启动 HTTP(没有 Quarkus 的请求链路)
  • 请求由 tio-boot 线程处理
  • 业务层调用了 Panache / EntityManager / Hibernate Session

12.2 根因(核心机制) ​

在 Quarkus + Hibernate ORM(Panache)里,EntityManager/Session 的使用依赖一个“上下文环境”来绑定资源和管理生命周期,至少需要满足以下之一:

  1. 存在事务上下文(Transaction Context)
  2. 存在 CDI Request Context(通常由 Quarkus HTTP 自动激活)

而本方案中:

  • HTTP 入口是 tio-boot
  • 请求线程是 tio-boot worker thread
  • 不走 Quarkus HTTP/Vert.x
  • 因此 Quarkus 不会自动激活 Request Context
  • 如果业务方法也没有 @Transactional,那么同时也没有事务上下文

结果就是:

  • Hibernate 找不到可用的 Session/EntityManager 生命周期边界
  • 抛出 ContextNotActiveException

一句话总结:

在“非 Quarkus HTTP 线程”里使用 Panache,必须显式提供事务或请求上下文,否则 Hibernate 无法工作。


12.3 为什么“查询”也需要事务 ​

这里的“事务”并不等价于“写操作才需要”,而是 ORM 的运行边界:

  • Hibernate 需要在一个可控生命周期内创建/绑定 Session

  • Session 内部涉及:

    • 一级缓存(Persistence Context)
    • 脏检查(dirty checking)
    • 连接绑定与释放
    • 延迟加载(lazy loading)等

在 Quarkus HTTP 场景中,这些通常由请求上下文隐式托管,因此很多人误以为查询不需要事务;但在 tio-boot 线程中,这种隐式托管不存在,所以必须显式设置边界。


12.4 解决方式 ​

推荐做法:Service 层统一用 @Transactional ​

只要方法中使用了:

  • PanacheRepository
  • EntityManager
  • Hibernate Session
  • 或任何数据库访问

就应当在 Service 方法上加 @Transactional,包含查询与写入。

的 UserService 正确写法如下(读写都在事务中):

java
package nexus.io.quarkus.service;

import java.util.Map;

import nexus.io.quarkus.entity.User;
import nexus.io.quarkus.repo.UserRepo;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

@ApplicationScoped
public class UserService {

  @Inject
  UserRepo userRepo;

  @Transactional
  public Map<String, Object> createUser(String username, String password) {
    User exists = userRepo.findByUsername(username);
    if (exists != null) {
      return Map.of("ok", false, "msg", "username already exists");
    }

    User u = new User();
    u.username = username;
    u.password = password;
    u.createdAt = System.currentTimeMillis();
    userRepo.persist(u);

    return Map.of("ok", true, "id", u.id, "username", u.username);
  }

  @Transactional
  public Map<String, Object> getUser(Long id) {
    User u = userRepo.findById(id);
    if (u == null) {
      return Map.of("ok", false, "msg", "not found");
    }
    return Map.of("ok", true, "id", u.id, "username", u.username, "createdAt", u.createdAt);
  }
}

12.5 推荐规范(放进团队开发约定) ​

在本方案(tio-boot 线程 + Quarkus 后台容器)下,建议固定一条规则:

  • 凡是 Service 方法里调用了 repo / entityManager / Panache 查询或持久化,都加 @Transactional

  • handler 只负责:

    • 参数解析
    • 调用 service
    • 返回 JSON
  • 事务边界只存在于 service(不要放在 handler)

这样可以避免:

  • ContextNotActiveException
  • 懒加载/Session 生命周期问题
  • 资源释放不确定导致的隐性问题

13. 最终结论 ​

通过以上整合,得到的是:

  • tio-boot:唯一对外 HTTP
  • Quarkus:只提供 CDI + 事务 + ORM + 数据源
  • 同 JVM 内直接调用,无 RPC、无二次序列化
  • handler 只是适配层,业务与数据库完全在 Quarkus Service/Repository/Entity 内闭环

源码

MIT Licensed