1. 概述

Quarkus 是一个由核心模块和一系列扩展(extension)构成的框架。其核心基于上下文与依赖注入(CDI),而扩展的作用通常是将第三方框架集成进来,并将其核心组件以 CDI Bean 的形式暴露出来。

本文将介绍如何开发一个 Quarkus 扩展,假设你已经具备基本的 Quarkus 使用经验。

2. 什么是 Quarkus 扩展

Quarkus 扩展本质上是一个可以运行在 Quarkus 应用之上的模块。Quarkus 自身也是由核心模块 + 多个扩展组成的。

最常见的场景是:你想把某个第三方框架(比如 Liquibase)无缝集成进 Quarkus,这时候就需要写一个扩展。

3. 在普通 Java 应用中运行 Liquibase

我们以集成 Liquibase(数据库变更管理工具)为例来实现一个扩展。

在动手之前,先回顾一下如何在标准 Java 程序中执行 Liquibase 迁移。这有助于我们理解后续扩展的实现逻辑。

Liquibase 的入口是其 Java API,需要三个关键组件:

  • 一个 changelog 文件
  • 用于加载该文件的 ClassLoader
  • 一个数据库连接 Connection

代码如下:

Connection c = DriverManager.getConnection("jdbc:h2:mem:testdb", "user", "password");
ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor();
String changLogFile = "db/liquibase-changelog-master.xml";
Liquibase liquibase = new Liquibase(changLogFile, resourceAccessor, new JdbcConnection(c));

拿到 Liquibase 实例后,调用 update() 方法即可执行数据库变更:

liquibase.update(new Contexts());

我们的目标就是把这个流程封装成一个 Quarkus 扩展 ✅:

  • 通过 Quarkus 配置机制传入数据库连接和 changelog 路径
  • Liquibase 实例作为 CDI Bean 暴露
  • 支持在应用启动时自动执行迁移

4. 如何编写 Quarkus 扩展

一个标准的 Quarkus 扩展通常是一个 Maven 多模块项目,包含两个子模块:

  1. runtime 模块:定义运行时逻辑,比如配置类、Bean 生产者等
  2. deployment 模块:负责处理构建时逻辑,比如读取配置、生成字节码

我们创建一个名为 quarkus-liquibase-parent 的父项目,包含 runtimedeployment 两个子模块:

<modules>
    <module>runtime</module>
    <module>deployment</module>
</modules>

5. 实现 runtime 模块

runtime 模块主要做三件事:

✅ 提供配置类
✅ 提供 CDI 生产者(Producer)
✅ 提供 Recorder(记录器),用于延迟执行方法调用

5.1 Maven 依赖与插件

runtime 模块需要以下依赖:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-core</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-agroal</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>4.26.0</version>
</dependency>

⚠️ 注意:

  • quarkus-agroal 提供了 DataSource 支持
  • Liquibase 依赖必须显式引入

可选:添加 quarkus-bootstrap-maven-plugin 自动生成扩展描述文件:

<plugin>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-bootstrap-maven-plugin</artifactId>
    <version>${quarkus.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>extension-descriptor</goal>
            </goals>
        </execution>
    </executions>
</plugin>

最终生成的描述文件位于 META-INF/quarkus-extension.properties

deployment-artifact=com.baeldung.quarkus.liquibase:deployment:1.0-SNAPSHOT

5.2 配置类定义

使用 @ConfigRoot@ConfigItem 定义配置项:

@ConfigRoot(name = "liquibase", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public final class LiquibaseConfig {
    @ConfigItem
    public String changeLog;
}

这样用户就可以在 application.properties 中配置:

quarkus.liquibase.change-log=db/liquibase-changelog-master.xml

📌 说明:

  • BUILD_AND_RUN_TIME_FIXED 表示该配置在构建期解析,运行期不可变
  • 字段名 changeLog 对应配置项 change-log(驼峰转连字符)

5.3 将 Liquibase 暴露为 CDI Bean

通过 CDI Producer 创建 Liquibase 实例:

@Produces
public Liquibase produceLiquibase() throws Exception {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(classLoader);
    DatabaseConnection jdbcConnection = new JdbcConnection(dataSource.getConnection());
    Liquibase liquibase = new Liquibase(liquibaseConfig.changeLog, resourceAccessor, jdbcConnection);
    return liquibase;
}

⚠️ 注意:这里的 dataSourceliquibaseConfig 需要通过注入方式获取。

5.4 Recorder:记录运行时调用

Recorder 是 Quarkus 构建时的核心机制之一,它不直接执行方法,而是“记录”调用,留到运行时再执行。

@Recorder
public class LiquibaseRecorder {

    public BeanContainerListener setLiquibaseConfig(LiquibaseConfig liquibaseConfig) {
        return beanContainer -> {
            LiquibaseProducer producer = beanContainer.instance(LiquibaseProducer.class);
            producer.setLiquibaseConfig(liquibaseConfig);
        };
    }

    public void migrate(BeanContainer container) throws LiquibaseException {
        Liquibase liquibase = container.instance(Liquibase.class);
        liquibase.update(new Contexts());
    }
}

关键点:

  • setLiquibaseConfig:在 BeanContainer 初始化后设置配置
  • migrate:记录 update() 调用,用于启动时自动迁移
  • ❌ 这些方法在构建期不会真正执行,只是生成字节码记录调用

6. 实现 deployment 模块

deployment 模块的核心是 Build Step Processor,即用 @BuildStep 注解的方法。它们在构建阶段执行,生成运行时代码。

6.1 Maven 依赖

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-core-deployment</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-arc-deployment</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-agroal-deployment</artifactId>
    <version>${quarkus.version}</version>
</dependency>

<dependency>
    <groupId>com.baeldung.quarkus.liquibase</groupId>
    <artifactId>runtime</artifactId>
    <version>${project.version}</version>
</dependency>

⚠️ 注意:必须依赖 runtime 模块,且版本一致。

6.2 实现 Build Step 处理器

构建期初始化(STATIC_INIT)

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void build(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
  BuildProducer<FeatureBuildItem> featureProducer,
  LiquibaseRecorder recorder,
  BuildProducer<BeanContainerListenerBuildItem> containerListenerProducer,
  DataSourceInitializedBuildItem dataSourceInitializedBuildItem) {

    featureProducer.produce(new FeatureBuildItem("liquibase"));

    AdditionalBeanBuildItem beanBuilItem = AdditionalBeanBuildItem.unremovableOf(LiquibaseProducer.class);
    additionalBeanProducer.produce(beanBuilItem);

    containerListenerProducer.produce(
      new BeanContainerListenerBuildItem(recorder.setLiquibaseConfig(liquibaseConfig)));
}

功能说明:

  • FeatureBuildItem:标记扩展名称,可用于条件化配置
  • AdditionalBeanBuildItem:确保 LiquibaseProducer 被注册为 Bean
  • BeanContainerListenerBuildItem:在容器启动后注入配置

运行时初始化(RUNTIME_INIT)

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
void processMigration(LiquibaseRecorder recorder, 
  BeanContainerBuildItem beanContainer) throws LiquibaseException {
    recorder.migrate(beanContainer.getValue());
}

作用:

  • 使用 RUNTIME_INIT 标记,表示该调用在应用启动时执行
  • 调用 recorder.migrate(),实际会触发 liquibase.update()

📌 执行顺序:

  1. 构建期:@BuildStep 方法被调用,生成字节码
  2. 运行时:生成的代码按 STATIC_INITRUNTIME_INIT 顺序执行

7. 测试 Liquibase 扩展

7.1 创建 Quarkus 应用

mvn io.quarkus:quarkus-maven-plugin:3.15.0:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=quarkus-app \
-DclassName="com.example.GreetingResource" \
-Dpath="/hello"

7.2 添加依赖

<dependency>
    <groupId>com.baeldung.quarkus.liquibase</groupId>
    <artifactId>runtime</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-h2</artifactId>
</dependency>

7.3 配置 application.properties

# 数据库配置
quarkus.datasource.url=jdbc:h2:mem:testdb
quarkus.datasource.driver=org.h2.Driver
quarkus.datasource.username=user
quarkus.datasource.password=password

# Liquibase 配置
quarkus.liquibase.change-log=db/liquibase-changelog-master.xml

7.4 启动应用

开发模式:

mvn compile quarkus:dev

生产模式:

mvn clean package
java -jar target/quarkus-app-1.0-SNAPSHOT-runner.jar

✅ 启动时会自动执行 Liquibase 迁移。

8. 总结

本文通过实现 Liquibase 扩展,深入讲解了 Quarkus 扩展的开发流程:

  • ✅ 多模块结构:runtime + deployment
  • ✅ 配置系统:@ConfigRoot / @ConfigItem
  • ✅ CDI 集成:Producer 暴露 Bean
  • ✅ 构建时处理:@BuildStep + Recorder 实现延迟执行
  • ✅ BuildItem 机制:构建阶段的数据传递

掌握这套模式后,你可以为任何第三方库编写 Quarkus 扩展,真正实现“云原生优先”的开发体验。

完整代码见 GitHub


原始标题:How to Implement a Quarkus Extension