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-restio.quarkus:quarkus-rest-jacksonio.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 基础上,增加(或保留)如下:
<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 等对应驱动扩展)。
# 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)
package com.litongjava.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)
package com.litongjava.quarkus.repo;
import com.litongjava.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 的事务细节
package com.litongjava.quarkus.service;
import java.util.Map;
import com.litongjava.quarkus.entity.User;
import com.litongjava.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)
package com.litongjava.quarkus.handler;
import com.litongjava.quarkus.service.UserService;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.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 路由即可:
package com.litongjava.quarkus.config;
import com.litongjava.context.BootConfiguration;
import com.litongjava.quarkus.handler.UserHandler;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.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。
@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
期望返回(示例):
{"ok":true,"id":1,"username":"alice"}
11.2 查询用户
GET http://127.0.0.1/user/get?id=1
期望返回(示例):
{"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 的使用依赖一个“上下文环境”来绑定资源和管理生命周期,至少需要满足以下之一:
- 存在事务上下文(Transaction Context)
- 存在 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
只要方法中使用了:
PanacheRepositoryEntityManager- Hibernate Session
- 或任何数据库访问
就应当在 Service 方法上加 @Transactional,包含查询与写入。
你的 UserService 正确写法如下(读写都在事务中):
package com.litongjava.quarkus.service;
import java.util.Map;
import com.litongjava.quarkus.entity.User;
import com.litongjava.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 查询或持久化,都加
@Transactionalhandler 只负责:
- 参数解析
- 调用 service
- 返回 JSON
事务边界只存在于 service(不要放在 handler)
这样可以避免:
ContextNotActiveException- 懒加载/Session 生命周期问题
- 资源释放不确定导致的隐性问题
13. 最终结论
通过以上整合,得到的是:
- tio-boot:唯一对外 HTTP
- Quarkus:只提供 CDI + 事务 + ORM + 数据源
- 同 JVM 内直接调用,无 RPC、无二次序列化
- handler 只是适配层,业务与数据库完全在 Quarkus Service/Repository/Entity 内闭环
