MeteorCat / Quarkus 官方扩展

Created Sun, 21 Sep 2025 14:50:52 +0800 Modified Wed, 29 Oct 2025 23:24:54 +0800

之前已经展示直接将 Actor System 提取成通用组件, 这部分作为通用模块是可以和 Quarkus 结合在一起, 这里就说明下怎么把 pekko-actor 作为组件封装成 quarkus-extension, 以下是必须条件:

  • JDK17+: 后续版本更迭之后可能 JDK 版本要求更高
  • Apache Maven 3.9+: 用于调用 mvn 命令拉取官方功能

同时需要注意, Quarkus 其实还有两种具体模式:

  • JVM: 比较常规的 JVM 启动方式, 调用运行的是经典的 JAR 应用程序(正常环境JRE启动)
  • Native: 将 JAVA 程序打包成可执行的二进制功能, 类似于 Golang|Rust 一样直接平台编译运行, 采用 GraalVM 技术

Native 技术采用 GraalVm, 其实就是封装裁剪成小型虚拟机启动

因为以上模式存在, 所以扩展之中对于两者处理可能需要单独分开(有的第三方可能并不支持原生模式打包, 需要编写时单独额外处理等)

Quarkus 扩展出来有以下部分, 这也是需要涉足到开发谋爱:

  • runtime: 运行时模块, 作为扩展开发者向应用程序开发者提供的功能, 也就是编写封装自己的功能
  • deployment: 部署时模块, 用于构建扩展时候的打包功能, JVM 可以发布全局 Bean, Native 可以为 GraalVM 的原生编译做准备

也就是 runtime 就是编写构建暴露自己功能代码, 而 deployment 则是打包编译发布和全局服务挂载

Quarkus 程序就是有以下启动阶段, 需要区分扩展编写之后需要在那个阶段嵌入启动:

  • Augmentation: 当处于构建期间, 扩展会加载并扫描应用程序的字节码(包括依赖项)和配置. 在此阶段, 扩展可以读取配置文件、扫描类以查找特定注解等. 收集所有元数据后, 扩展可以预处理库的引导操作(例如ORM、DI或REST控制器的配置). 引导的结果会直接记录到字节码中,并将成为最终应用程序包的一部分
  • Static Init: 当处于在运行时, Quarkus 将首先执行一个静态初始化方法, 该方法包含一些扩展操作/配置. 当你进行原生打包时, 这个静态方法会在构建时进行预处理, 其生成的对象会被序列化到最终的原生可执行文件中, 因此初始化代码不会在原生模式下执行(假设你在此阶段某个函数并生成结果, 该结果将直接记录在原生可执行文件中). 在JVM模式下运行应用程序时, 这个静态初始化阶段会在应用程序启动时执行
  • Runtime Init: 只有在程序运行的时候才会被唤醒的服务, 也就是我们日常编写代码这种, 只会在运行时启动

现在开始就是在准备编写之前需要知道扩展的类型, 官方文档之中扩展其实就以下几种:

  • 官方社区扩展: 需要把 groupId 设置为 io.quarkiverse.[extensionId], 用于申请成为官方正式集成组件
  • 自维护扩展: 这里不规定扩展并入官方组件, 而是自己单独维护开发, 当然可以联系官方将其添加到扩展商店用于提高曝光

比较合理的就是先可以自己来扩展维护, 后续如果成型没问题的话可以申请转化成官方内部扩展, 避免突然没时间跟进 Quarkus 维护

一旦加入官方社区扩展, 就需要保证跟进 Quarkus 新版本的维护和调试

这里一些概念已经说明, 现在就准备边编写边介绍相关知识点.

初始化项目

这里利用 maven 的命令行来初始化项目, 指令如下:

# 这里的 quarkus-maven-plugin 插件版本可以指定对应 Quarkus 版本
# 如果指向维护 LTS 版本, 可以查找官方 LTS 版本号修改
# 之后就是指定程序包的 分组 和 ID
# withoutTests 代表不生成具体的测试单元
mvn io.quarkus.platform:quarkus-maven-plugin:3.26.3:create-extension -N \
-DgroupId=io.fortress \
-DextensionId=quarkus-pekko-actor \
-DwithoutTests

# 如果你编写的是参与官方社区的扩展, 则 groupId 前缀必须为 groupId=io.quarkiverse.[你的扩展名]
mvn io.quarkus.platform:quarkus-maven-plugin:3.26.3:create-extension -N \
-DgroupId=io.quarkiverse.quarkus-pekko \
-DextensionId=quarkus-pekko-actor \
-DwithoutTests

注意以上命令在 window 平台可能会有问题, 需要合并成一行并且采用默认命令行而非 PowerShell

运行之后就会生成个 quarkus-pekko-actor 目录, 内部则是标准的 runtimedeployment(之前提到的概念)

第一次初始化最好 compile 编译生成下保证通过没问题, 有的 Github 拉取的扩展模板可能有问题, 确认没问题再继续后续功能.

还有一点需要注意, 那就是 不要去动下载扩展模板的 JDK 版本, 而是直接采用模板默认 JDK 版本来开发.

引入 pekko

现在就可以开始引入 pekko-actor 基础功能, 如果扩展比较单一且简单的情况, 直接 runtimedeployment 就可以.

但是功能很复杂甚至包含大量框架集成封装, 就再追加声明 api 子项目目录, 把功能全部放置到内部之中; 比如 pekko-actor 内部还包含大量工具类和方法类, 就需要追加个 pekko-actor-api 子项目去扩展再引入到 runtime.

回过头来现在就是在 根目录的 pom.xml 添加 pekko-bom 依赖, 让他能够自动做版本更新处理:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.fortress</groupId>
    <artifactId>quarkus-pekko-actor-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>Quarkus Pekko Actor - Parent</name>

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

    <properties>
        <compiler-plugin.version>3.14.0</compiler-plugin.version>
        <failsafe-plugin.version>${surefire-plugin.version}</failsafe-plugin.version>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <quarkus.version>3.26.3</quarkus.version>
        <surefire-plugin.version>3.5.2</surefire-plugin.version>

        <!-- pekko bom -->
        <pekko.platform.artifact-id>pekko-bom</pekko.platform.artifact-id>
        <pekko.platform.group-id>org.apache.pekko</pekko.platform.group-id>
        <pekko.platform.version>1.2.0</pekko.platform.version>
        <pekko.platform.scala-version>2.13</pekko.platform.scala-version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.quarkus</groupId>
                <artifactId>quarkus-bom</artifactId>
                <version>${quarkus.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>${pekko.platform.group-id}</groupId>
                <artifactId>${pekko.platform.artifact-id}_${pekko.platform.scala-version}</artifactId>
                <version>${pekko.platform.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 其他略 -->
</project>

这样直接不用去管后续的版本字段编写, 很简单的第三方库引入方式.

注意: 后续的 runtime 变动最好针对 parent 执行下 install 安装到系统库当中, 否则可能找不到对应模块信息.

Runtime

首先编写运行时的代码, 默认 quarkus 扩展信息放置在以下文件:

  • runtime/src/main/resources/META-INF/quarkus-extension.yaml

这个文件就是具体的扩展内容信息, 我这边编写扩展内容如下:

name: Quarkus Pekko Actor
description: Integrates Pekko Actor with Quarkus.
metadata:
  # 关键字
  keywords:
    - pekko
    - actor
    - concurrency
  # 扩展的分类
  # Web, Database, Cloud, Monitoring, 可以 Quarkus 目录中归类选择
  categories:
    - "miscellaneous"
  # 文档目录
  guide: https://quarkiverse.github.io/xxxx/quarkus-pekko-acotr/dev/index.html
  # 配置依赖前缀
  config:
    - "quarkus.actor."

这就是比较简约的配置, runtime 是需要集成第三方依赖, 所以需要引入 pekko-actor 基础配置:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>io.fortress</groupId>
        <artifactId>quarkus-pekko-actor-parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <artifactId>quarkus-pekko-actor</artifactId>
    <name>Quarkus Pekko Actor - Runtime</name>


    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-arc</artifactId>
        </dependency>
        <dependency>
            <groupId>${pekko.platform.group-id}</groupId>
            <artifactId>pekko-actor_${pekko.platform.scala-version}</artifactId>
        </dependency>
        <dependency>
            <groupId>${pekko.platform.group-id}</groupId>
            <artifactId>pekko-slf4j_${pekko.platform.scala-version}</artifactId>
        </dependency>
    </dependencies>

    <!-- 其他略 -->
</project>

默认只需要引入基础的 pekko 对象, 其他依赖则是让外部集成的开发者去自助引入 pekko-cluster-sharding 之类集群服务.

后续生成 pekko 的配置类, 但是在此之前建议先看下 deployment 声明的 XXXProcessor.java 文件; 这里生成的 打包处理器(Processor) 名为 QuarkusPekkoActorProcessor.java, 为了统一命名规范, 后续文件前缀都要以 QuarkusPekkoActor 来声明.

按照上面规则我们这里声明的配置文件为 QuarkusPekkoActorConfiguration:

package io.fortress.quarkus.pekko.actor.runtime;

import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

import java.util.Map;

/**
 * io.fortress.quarkus.pekko.actor.runtime.QuarkusPekkoActorConfiguration.java
 * <p>
 * Quarkus 中 Pekko Actor 系统的核心配置接口。
 * 用于桥接 Quarkus 配置与 Pekko 的原生配置模型。
 */
@ConfigMapping(prefix = "quarkus.actor")
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
public interface QuarkusPekkoActorConfiguration {

    /**
     * Actor 系统的唯一标识符。
     * 用于区分多个 ActorSystem 实例。
     *
     * @return 系统名称,默认值:"default"
     */
    @WithDefault("default")
    String name();

    /**
     * 定义 Pekko 配置加载优先级。
     * 控制默认框架配置与自定义设置的交互方式。
     *
     * @return 引用类型,默认值:Reference
     */
    @WithDefault("Reference")
    QuarkusPekkoActorReference reference();

    /**
     * 额外的 Pekko 配置项
     * <p>
     * 用于以键值对形式覆盖或补充基础配置。
     * 键名必须与 Pekko 的原生配置路径完全匹配。
     * 例如:"pekko.actor.default-dispatcher.fork-join-executor.parallelism-max"
     *
     * @return 自定义配置映射,默认返回空映射
     */
    Map<String, String> settings();
}

这里的 QuarkusPekkoActorReference 默认启动依赖如下, 用于加载 Actor System 系统默认配置:

package io.fortress.quarkus.pekko.actor.runtime;

import com.typesafe.config.ConfigFactory;

/**
 * io.fortress.quarkus.pekko.actor.runtime.QuarkusPekkoActorReference.java
 * <p>
 * Pekko配置加载类型的枚举,对应{@link ConfigFactory}提供的四种默认配置加载方式。
 * 用于在不同场景下切换基础配置。
 * <p>
 * 枚举值与ConfigFactory方法的映射关系:
 * - Reference: 加载Pekko框架的默认参考配置({@link ConfigFactory#defaultReference()});
 * - Application: 加载应用程序特定的自定义配置({@link ConfigFactory#defaultApplication()});
 * - Overrides: 加载覆盖配置(优先级最高,用于临时覆盖默认配置,{@link ConfigFactory#defaultOverrides()});
 * - ReferenceUnresolved: 加载未解析的默认参考配置(需要手动调用resolve()方法,{@link ConfigFactory#defaultReferenceUnresolved()})。
 */
public enum QuarkusPekkoActorReference {
    /**
     * 对应{@link ConfigFactory#defaultReference()};加载Pekko框架内置的默认参考配置。
     */
    Reference,
    /**
     * 对应{@link ConfigFactory#defaultApplication()};加载应用级别的自定义配置(例如application.conf)。
     */
    Application,
    /**
     * 对应{@link ConfigFactory#defaultOverrides()};加载覆盖配置(优先级高于Reference和Application)。
     */
    Overrides,
    /**
     * 对应{@link ConfigFactory#defaultReferenceUnresolved()};加载未解析的默认参考配置(需要后续手动解析)。
     */
    ReferenceUnresolved
}

编写完成就是生成全局 Actor SystemBean 并且暴露给打包处理器处理, 这里需要声明 Recorder 工具来处理.

@Recorder: 标记对应类, 在构建时(Build Time)执行代码, 并将需要在运行时(Runtime)使用的结果 “记录”(序列化)下来, 供运行时使用

可以简单理解成用于全局实例化初始化对象, 文件内容如下:

package io.fortress.quarkus.pekko.actor.runtime;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.ShutdownContext;
import io.quarkus.runtime.annotations.Recorder;
import org.apache.pekko.actor.ActorSystem;
import org.jboss.logmanager.LogContext;
import org.jboss.logmanager.Logger;

import java.util.Map;

/**
 * io.fortress.quarkus.pekko.actor.runtime.QuarkusPekkoActorRecorder.java
 * <p>
 * 用于在Quarkus中处理Pekko ActorSystem的运行时初始化和管理的记录器类。
 * 负责ActorSystem的创建、配置和清理工作,使其与Quarkus的生命周期保持一致。
 */
@Recorder
public class QuarkusPekkoActorRecorder {

    /**
     * 存储已解析的Pekko配置的RuntimeValue对象(在构建时计算,运行时使用)。
     */
    private final RuntimeValue<QuarkusPekkoActorConfiguration> configuration;

    /**
     * 初始化记录器并传入配置的构造函数。
     * 在构建阶段由Quarkus扩展框架调用(配置已预先解析)。
     *
     * @param configuration 包含Pekko配置的RuntimeValue对象
     */
    public QuarkusPekkoActorRecorder(RuntimeValue<QuarkusPekkoActorConfiguration> configuration) {
        this.configuration = configuration;
    }


    /**
     * 初始化Pekko ActorSystem并将其注册到Quarkus的关闭生命周期中。
     * 应用启动时创建ActorSystem的主要入口点。
     *
     * @param shutdownContext 用于注册清理任务的Quarkus上下文
     * @return 包含已初始化的ActorSystem的RuntimeValue对象
     */
    public RuntimeValue<ActorSystem> createActorSystem(ShutdownContext shutdownContext) {
        QuarkusPekkoActorConfiguration config = configuration.getValue();
        ActorSystem system = createActorSystem(config);

        // 注册关闭任务以优雅终止ActorSystem
        shutdownContext.addShutdownTask(() -> {
            system.terminate();
            system.getWhenTerminated().toCompletableFuture().join(); // 阻塞等待完全清理完成
        });

        // 记录初始化详情日志
        system.log().info("ActorSystem name: {}", system.name());
        system.log().info("ActorSystem reference: {}", config.reference().toString());
        config.settings().forEach((key, value) -> system.log().info("Actor config: {} = {}", key, value));

        // 将ActorSystem包装在RuntimeValue中以便Quarkus管理
        return new RuntimeValue<>(system);
    }

    /**
     * 创建和配置Pekko ActorSystem的核心方法。
     * 合并配置(默认配置 + 自定义配置)并应用Quarkus特定的日志集成。
     *
     * @param configuration 已解析的Pekko配置
     * @return 完全配置好的ActorSystem
     */
    public ActorSystem createActorSystem(QuarkusPekkoActorConfiguration configuration) {
        Config originalConfig = ConfigFactory.load();
        switch (configuration.reference()) {
            case Application:
                originalConfig = originalConfig.withFallback(ConfigFactory.defaultApplication());
                break;
            case Overrides:
                originalConfig = originalConfig.withFallback(ConfigFactory.defaultOverrides());
                break;
            case ReferenceUnresolved:
                originalConfig = originalConfig.withFallback(ConfigFactory.defaultReferenceUnresolved());
                break;
            default:
                originalConfig = originalConfig.withFallback(ConfigFactory.defaultReference());
        }
        // 获取自定义覆盖设置
        Map<String, String> settings = configuration.settings();

        // 如果用户未配置,则设置Pekko使用SLF4J(Quarkus日志)
        if (!settings.containsKey("pekko.logging-filter")) {
            settings.put("pekko.use-slf4j", "on");
        }

        // 如果用户未配置,则同步Pekko日志级别与Quarkus根日志器
        if (!settings.containsKey("pekko.log-level")) {
            Logger rootLogger = LogContext.getLogContext().getLogger("");
            settings.put("pekko.loglevel", rootLogger.getLevel().toString());
        }

        // 将自定义设置(最高优先级)合并到最终配置中
        originalConfig = ConfigFactory.parseMap(settings).withFallback(originalConfig);

        // 创建并返回ActorSystem
        return ActorSystem.create(configuration.name(), originalConfig);
    }
}

至此 runtime 的功能已经初步完成, 接下来就是 部署端(deployment) 的配置, 现在就需要挂起这个全局 ActorSystem 服务

还需要注意: 不要在 runtime 内部编写测试单元功能, 要么在 deployment 编写, 要么另开子目录归类到一起

Deployment

打包功能集中在 XXXProcessor.java 文件之中, 这里需要引入一些测试单元功能, 也就是在 deployment/pom.xml 文件追加 Actor 测试模块:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>io.fortress</groupId>
        <artifactId>quarkus-pekko-actor-parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <artifactId>quarkus-pekko-actor-deployment</artifactId>
    <name>Quarkus Pekko Actor - Deployment</name>

    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-arc-deployment</artifactId>
        </dependency>
        <dependency>
            <groupId>io.fortress</groupId>
            <artifactId>quarkus-pekko-actor</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit5-internal</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>${pekko.platform.group-id}</groupId>
            <artifactId>pekko-testkit_${pekko.platform.scala-version}</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- 其他略 -->
</project>

现在就是正式编写打包配置, 注册全局 Actor System 句柄:

package io.fortress.quarkus.pekko.actor.deployment;

import io.fortress.quarkus.pekko.actor.runtime.QuarkusPekkoActorRecorder;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.runtime.RuntimeValue;
import org.apache.pekko.actor.ActorSystem;

/**
 * io.fortress.quarkus.pekko.actor.deployment.QuarkusPekkoActorProcessor.java
 * <p>
 * Pekko扩展的Quarkus部署处理器。
 * 处理构建阶段的任务,如功能注册、ActorSystem初始化以及创建合成Bean,
 * 以将Pekko与Quarkus的运行时环境集成。
 */
class QuarkusPekkoActorProcessor {

    /**
     * 在Quarkus中标识Pekko扩展功能(用于功能激活和日志记录)
     */
    private static final String FEATURE = "quarkus-pekko-actor";

    /**
     * 将Pekko扩展注册为Quarkus功能的构建步骤。
     * 这会通知Quarkus"pekko"功能已存在,从而启用特定于扩展的生命周期管理。
     *
     * @return Pekko扩展的FeatureBuildItem,将其标记为活动的Quarkus功能
     */
    @BuildStep
    FeatureBuildItem feature() {
        return new FeatureBuildItem(FEATURE);
    }


    /**
     * 初始化Pekko ActorSystem并将其作为Quarkus合成Bean暴露的构建步骤。
     * 在运行时初始化阶段({@link ExecutionTime#RUNTIME_INIT})执行,以确保在创建ActorSystem之前,
     * 配置和关闭上下文已完全解析。
     *
     * @param recorder        用于运行时ActorSystem设置的QuarkusPekkoActorRecorder实例(处理实际的ActorSystem创建/清理)
     * @param syntheticBeans  用于创建合成Bean的生产者(将ActorSystem注册为CDI Bean)
     * @param shutdownContext 提供Quarkus的关闭钩子,以确保ActorSystem的优雅终止
     * @return 包含初始化的ActorSystem的QuarkusPekkoActorBuildItem(供下游扩展使用)
     */
    @BuildStep
    @Record(ExecutionTime.RUNTIME_INIT)
    QuarkusPekkoActorBuildItem initializeActorSystem(QuarkusPekkoActorRecorder recorder,
                                                     BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
                                                     ShutdownContextBuildItem shutdownContext) {
        // 委托给记录器创建ActorSystem(封装运行时初始化逻辑)
        RuntimeValue<ActorSystem> holder = recorder.createActorSystem(shutdownContext);

        // 为ActorSystem创建合成CDI Bean:使其可通过@Inject在Quarkus应用中注入
        syntheticBeans.produce(
                SyntheticBeanBuildItem.configure(ActorSystem.class)
                        .runtimeValue(holder) // 将Bean绑定到运行时初始化的ActorSystem
                        .setRuntimeInit() // 将Bean标记为运行时初始化(与执行时间对齐)
                        .done());

        // 返回构建项以向其他扩展组件暴露ActorSystem
        return new QuarkusPekkoActorBuildItem(holder);
    }
}

保存的服务容器 QuarkusPekkoActorBuildItem 定义如下:

package io.fortress.quarkus.pekko.actor.deployment;

import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.runtime.RuntimeValue;
import org.apache.pekko.actor.ActorSystem;

/**
 * io.fortress.quarkus.pekko.actor.deployment.QuarkusPekkoActorBuildItem.java
 * <p>
 * 注意: 声明构建完成对象必须将类声明为 public final class
 * 作为容器,用于在Quarkus构建步骤之间或向其他依赖扩展传递由运行时管理的{@link ActorSystem}实例(包装在{@link RuntimeValue}中)。
 * 确保在Pekko扩展初始化期间创建的ActorSystem可用于下游部署逻辑(例如,Bean注册、附加配置)。
 */
public final class QuarkusPekkoActorBuildItem extends SimpleBuildItem {

    private final RuntimeValue<ActorSystem> value;

    /**
     * 使用指定的运行时管理的ActorSystem创建{@link QuarkusPekkoActorBuildItem}。
     *
     * @param value 包含已初始化的Pekko {@link ActorSystem}的{@link RuntimeValue};
     *              不能为null(确保有效的ActorSystem传播)
     */
    public QuarkusPekkoActorBuildItem(RuntimeValue<ActorSystem> value) {
        this.value = value;
    }

    /**
     * 获取由运行时包装的Pekko {@link ActorSystem}实例。
     * <p>
     * 供下游构建步骤或扩展使用,以访问ActorSystem进行进一步集成(例如,绑定到CDI、附加额外的生命周期钩子)。
     *
     * @return 包含已初始化的{@link ActorSystem}的{@link RuntimeValue}
     */
    public RuntimeValue<ActorSystem> getValue() {
        return value;
    }
}

全局编译打包一次查看下是否有问题, 没有问题就可以准备编写测试单元.

单元测试

现在功能已经注入完成了, 这里就要在 deployment 之下创建具体的测试单元文件, 首先编写测试用的配置(deployment/src/test/resources/application.properties, 文件不存在手动创建)

quarkus.log.level=DEBUG
quarkus.actor.name=pekko-tests
quarkus.actor.reference=reference
# settings
quarkus.actor.settings.pekko.actor.provider=local
quarkus.actor.settings.pekko.cluster.downing-provider-class=org.apache.pekko.cluster.sbr.SplitBrainResolverProvider
quarkus.actor.settings.pekko.cluster.split-brain-resolver.active-strategy=keep-majority

之后就是测试下是否能够挂载起对应的 Actor System, 追加测试单元 (deployment/src/test/java/io/fortress/quarkus/pekko/actor/deployment/QuarkusPekkoActorProcessorTest.java):

package io.fortress.quarkus.pekko.actor.deployment;

import io.fortress.quarkus.pekko.actor.runtime.QuarkusPekkoActorConfiguration;
import io.quarkus.test.QuarkusUnitTest;
import io.smallrye.common.constraint.Assert;
import jakarta.inject.Inject;
import org.apache.pekko.actor.AbstractActor;
import org.apache.pekko.actor.ActorRef;
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.actor.Props;
import org.apache.pekko.testkit.TestKit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import scala.concurrent.duration.Duration;

import java.util.concurrent.TimeUnit;

class QuarkusPekkoActorProcessorTest {

    // Quarkus测试扩展配置
    @RegisterExtension
    static final QuarkusUnitTest quarkusUnitTest = new QuarkusUnitTest()
            .withConfigurationResource("application.properties");


    @Inject
    QuarkusPekkoActorConfiguration configuration;

    /**
     * 验证Pekko配置是否能正确注入
     */
    @Test
    public void testActorConfiguration() {
        Assert.assertNotNull(configuration);
    }


    @Inject
    ActorSystem actorSystem;

    /**
     * 验证ActorSystem是否能正确注入并初始化
     */
    @Test
    public void testActorSystem() {
        Assert.assertNotNull(actorSystem);
    }


    /**
     * 用于基础消息处理的测试Actor类
     */
    public static class TestActor extends AbstractActor {

        @Override
        public Receive createReceive() {
            return receiveBuilder()
                    .match(String.class, msg -> {
                        // 若存在发送方,则将消息回显回去
                        if (ActorRef.noSender() != sender()) {
                            sender().tell(msg, self());
                        }
                    })
                    .build();
        }
    }

    /**
     * 测试基础的Actor创建与消息传递功能
     */
    @Test
    public void testActorNode() {
        // 创建TestActor实例,指定名称为"testActorNode"
        ActorRef address = actorSystem.actorOf(Props.create(TestActor.class), "testActorNode");
        // 创建用于测试的TestKit(Pekko测试工具,用于接收Actor消息)
        TestKit testKit = new TestKit(actorSystem);

        String message = "hello.world";
        // 向TestActor发送消息,并指定测试工具的Actor作为回复接收方
        address.tell(message, testKit.testActor());

        // 在超时时间内验证消息是否回显成功
        String response = testKit.expectMsg(Duration.create(3, TimeUnit.SECONDS), message);
        Assert.assertNotNull(response);
        Assert.assertTrue(message.equals(response));
    }
}

跑一下测试没问题即可, 这样就代表生成全局 Actor SystemQuarkus 注入, 可以直接 @Inject ActorSystem actorSystem 引用

扩展知识

目前已经全局生成了 Actor System 并确保服务已经挂载起来, 虽然不影响日常使用但是没办法完全集成到 Quarkus 内部, 最明显的就是 actorOf 构建的对象内部没办法做 @Inject 注入从而捕获到 Arc 容器的 Bean 对象.

所以这里在 runtime 扩展创建工具二次生成 Props 注入对象:

package io.fortress.quarkus.pekko.actor.extension;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import jakarta.inject.Inject;
import org.apache.pekko.actor.Actor;
import org.apache.pekko.actor.Props;
import org.jboss.logging.Logger;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;

/**
 * io.fortress.quarkus.pekko.actor.extension.PropsFactory.java
 * <p>
 * Quarkus环境下的Pekko Actor Props工具类,支持为Actor注入CDI依赖
 */
public final class PropsFactory {

    /**
     * 禁止实例化
     */
    private PropsFactory() {
    }

    /**
     * 日志句柄
     */
    static Logger logger = Logger.getLogger(PropsFactory.class);

    /**
     * 获取当前运行的Quarkus Arc容器(CDI容器)
     *
     * @return 存在且运行中的Arc容器,否则返回空Optional
     */
    private static Optional<ArcContainer> container() {
        ArcContainer container = Arc.container();
        if (!Objects.isNull(container) && container.isRunning()) return Optional.of(container);
        return Optional.empty();
    }


    /**
     * 为Actor实例的@Inject字段注入CDI依赖(含父类字段)
     *
     * @param actor     目标Actor实例
     * @param container Quarkus Arc容器
     * @param <T>       Actor类型
     * @return 注入依赖后的Actor实例
     * @throws IllegalAccessException 字段访问权限异常
     */
    private static <T extends Actor> T injectFields(T actor, ArcContainer container) throws IllegalAccessException {
        Class<?> clazz = actor.getClass();
        while (clazz != Object.class) {
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(Inject.class)) {
                    field.setAccessible(true);
                    Object dependency = container.select(field.getType()).get();
                    field.set(actor, dependency);
                    logger.debugf("Successfully injected dependency into field [%s] of Actor [%s]",
                            actor.self().path().name(), field.getName());
                }
            }
            clazz = clazz.getSuperclass();
        }
        return actor;
    }


    /**
     * 创建支持CDI注入的Actor Props(通过默认构造器实例化Actor)
     *
     * @param actor Actor的Class对象
     * @param <T>   Actor类型
     * @return 带CDI注入能力的Pekko Props
     */
    public static <T extends Actor> Props create(Class<T> actor) {
        if (container().isEmpty()) return Props.create(actor);
        ArcContainer container = container().get();

        return Props.create(actor, () -> {
            try {
                Constructor<T> constructor = actor.getDeclaredConstructor();
                constructor.setAccessible(true);
                T actorInstance = constructor.newInstance();
                return injectFields(actorInstance, container);
            } catch (Exception e) {
                logger.warn("Failed to create and inject actor [actorClass: " + actor.getName() + "]", e);
                throw new RuntimeException("Failed to create and inject actor [actorClass: " + actor.getName() + "]", e);
            }
        });
    }

    /**
     * 创建支持CDI注入的Actor Props(通过自定义Supplier实例化Actor)
     *
     * @param actor   Actor的Class对象
     * @param creator 自定义Actor实例创建器
     * @param <T>     Actor类型
     * @return 带CDI注入能力的Pekko Props
     */
    public static <T extends Actor> Props create(Class<T> actor, Supplier<T> creator) {
        if (container().isEmpty()) return Props.create(actor, () -> creator.get());
        ArcContainer container = container().get();
        return Props.create(actor, () -> {
            try {
                T actorInstance = creator.get();
                return injectFields(actorInstance, container);
            } catch (Exception e) {
                logger.warn("Failed to create and inject actor [actorClass: " + actor.getName() + "]", e);
                throw new RuntimeException("Failed to create and inject actor [actorClass: " + actor.getName() + "]", e);
            }
        });
    }

}

注: 扩展功能挂在另外的 io.fortress.quarkus.pekko.actor.extension 命名空间, 不要放在 runtime 之中

编译一下之后重新构建测试单元样例, 追加支持 @InjectActor 对象

class QuarkusPekkoActorProcessorTest {

    // 其他略

    // Quarkus测试扩展配置
    @RegisterExtension
    static final QuarkusUnitTest quarkusUnitTest = new QuarkusUnitTest()
            .withConfigurationResource("application.properties");


    @Inject
    QuarkusPekkoActorConfiguration configuration;


    @Inject
    ActorSystem actorSystem;

    /**
     * 容器注入 Actor
     */
    public static class TestInstanceActor extends AbstractActor {

        @Inject
        QuarkusPekkoActorConfiguration configuration;

        @Override
        public Receive createReceive() {
            return receiveBuilder()
                    .match(String.class, msg -> {
                        // 验证注入配置
                        Assert.assertNotNull(configuration);
                        Assert.assertTrue(configuration.name().equals(msg));

                        // 返回注入的数据
                        if (ActorRef.noSender() != sender()) {
                            sender().tell(msg, self());
                        }
                    })
                    .build();
        }
    }

    /**
     * 测试生成支持 @Inject 做注入
     */
    @Test
    public void testInstanceActor() {
        ActorRef address = actorSystem.actorOf(PropsFactory.create(TestInstanceActor.class), "testInstanceActor");
        TestKit testKit = new TestKit(actorSystem);

        String message = configuration.name();
        address.tell(message, testKit.testActor());

        // 验证配置是否一致
        String response = testKit.expectMsg(Duration.create(3, TimeUnit.SECONDS), message);
        Assert.assertNotNull(response);
        Assert.assertTrue(message.equals(response));
    }

}

这里就是基于 Quarkus 的扩展开发, 后续如果扩展比较频繁被引用到的时候, 就可以开始向官方提交并入基础扩展的流程.

自动化流程

这里主要是提交给 Github 做自动化构建和测试, 可以参考 Quarkus 官方当中的配置项来复用就行了, 提取出来对应目录和文件:

  • .github/: GitHub 仓库的核心配置目录, 存放所有与仓库管理和自动化流程相关的配置文件
  • .github/workflows/: 存放 Actions 工作流配置文件的目录. 所有以 .yml|.yaml 结尾的文件都会被识别为自动化流程从而执行
  • .github/workflows/build.yml: 定义项目的构建和基础测试流程, 拉取代码配置编译环境(如 JDKMaven)
  • .github/workflows/pre-release.yml: 预发布验证流程, 用于在正式发布前进行最终检查
  • .github/workflows/quarkus-snapshot.yaml: 针对 Quarkus 框架的快照版本(开发中的不稳定版本)构建流程
  • .github/workflows/release-perform.yml: 正式发布执行流程, 负责将验证通过的版本发布到公开仓库(Maven Central)
  • .github/workflows/release-prepare.yml: 发布准备流程, 为正式发布做前期配置, 自动更新项目触发 release-perform.yml
  • .github/dependabot.yml: 配置依赖自动更新工具(Dependabot)的行为, 监听每次提交并且触发自动化测试流程
  • .github/project.yml: GitHub Projects(项目管理面板) 的配置文件

默认直接用官方文档的自动化流程就行了, 复制过去即可直接使用, 后续在 build.yml 文件按自己要求修改就行.

project.yml

# 若要发布 1.0.0,则将 current-version 设为 1.0.0,next-version 设为 1.1.0-SNAPSHOT 这样就代表版本更迭
# 后续如果 1.1.0 版本准备上线就将 current-version 设为 1.1.0, 并且做好下次 next-version 预版本规划  
release:
  current-version: "1.0.0"
  next-version: "1.1.0-SNAPSHOT"

dependabot.yml

# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
  - package-ecosystem: "maven" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "daily"
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: daily

build.yml

name: Build

on:
  push:
    branches:
      - "main"
    paths-ignore:
      - '.gitignore'
      - 'CODEOWNERS'
      - 'LICENSE'
      - '*.md'
      - '*.adoc'
      - '*.txt'
      - '.all-contributorsrc'
      - '.github/project.yml'
  pull_request:
    paths-ignore:
      - '.gitignore'
      - 'CODEOWNERS'
      - 'LICENSE'
      - '*.md'
      - '*.adoc'
      - '*.txt'
      - '.all-contributorsrc'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash

jobs:
  build:
    name: Build on ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        #        os: [windows-latest, macos-latest, ubuntu-latest]
        os: [ ubuntu-latest ]
    runs-on: ${{ matrix.os }}
    steps:
      - name: Prepare git
        run: git config --global core.autocrlf false
        if: startsWith(matrix.os, 'windows')

      - uses: actions/checkout@v5
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17
          cache: 'maven'

      - name: Build with Maven
        run: mvn -B clean install -Dno-format

      - name: Build with Maven (Native)
        run: mvn -B install -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip

pre-release.yml

name: Quarkiverse Pre Release

on:
  pull_request:
    paths:
      - '.github/project.yml'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  pre-release:
    name: Pre-Release
    uses: quarkiverse/.github/.github/workflows/pre-release.yml@main
    secrets: inherit

quarkus-snapshot.yaml

name: "Quarkus ecosystem CI"
on:
  workflow_dispatch:
  watch:
    types: [ started ]

  # For this CI to work, ECOSYSTEM_CI_TOKEN needs to contain a GitHub with rights to close the Quarkus issue that the user/bot has opened,
  # while 'ECOSYSTEM_CI_REPO_PATH' needs to be set to the corresponding path in the 'quarkusio/quarkus-ecosystem-ci' repository

env:
  ECOSYSTEM_CI_REPO: quarkusio/quarkus-ecosystem-ci
  ECOSYSTEM_CI_REPO_FILE: context.yaml
  JAVA_VERSION: 17

  #########################
  # Repo specific setting #
  #########################

  ECOSYSTEM_CI_REPO_PATH: quarkiverse-pekko

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash

jobs:
  build:
    name: "Build against latest Quarkus snapshot"
    runs-on: ubuntu-latest

    steps:
      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: ${{ env.JAVA_VERSION }}

      - name: Checkout repo
        uses: actions/checkout@v5
        with:
          path: current-repo

      - name: Checkout Ecosystem
        uses: actions/checkout@v5
        with:
          repository: ${{ env.ECOSYSTEM_CI_REPO }}
          path: ecosystem-ci

      - name: Setup and Run Tests
        run: ./ecosystem-ci/setup-and-test
        env:
          ECOSYSTEM_CI_TOKEN: ${{ secrets.ECOSYSTEM_CI_TOKEN }}

release-perform.yml

name: Quarkiverse Perform Release
run-name: Perform ${{github.event.inputs.tag || github.ref_name}} Release
on:
  push:
    tags:
      - '*'
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to release'
        required: true

permissions:
  attestations: write
  id-token: write
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  perform-release:
    name: Perform Release
    uses: quarkiverse/.github/.github/workflows/perform-release.yml@main
    secrets: inherit
    with:
      version: ${{github.event.inputs.tag || github.ref_name}}

release-prepare.yml

name: Quarkiverse Prepare Release

on:
  workflow_dispatch:
  pull_request:
    types: [ closed ]
    paths:
      - '.github/project.yml'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  prepare-release:
    name: Prepare Release
    if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true}}
    uses: quarkiverse/.github/.github/workflows/prepare-release.yml@main
    secrets: inherit

日常如果计划发布新版本应该这样处理:

  1. 修改 project.yml 配置, current-version 设为上线版本, next-version 设下个开发版本
  2. 功能代码随着 project.yml 同步修改到 main 分支. PR 合并后, release-prepare.yml 会自动运行
  3. release-prepare.yml 会自动生成 发布说明|发布标签|更新项目版本号, 触发后续 release-perform.yml
  4. release-perform.yml 触发完成自动在 GitHub 仓库创建 Release 页面, 附带发布说明和产物

这就是整体的自动化发布流程, 这个发布流程也是很值得推广的正确步骤.

具体的发布模板可以参考官方: github自动化

SVN 集成开发, 我这边个人是采用 svn 做总的内部版本管理, 通过自带的 svn-branch(SVN也有自带的分支功能) 做对外版本:

  • 内部版本的 .git 也会参与托管, 但是 clone|pull|push 的账号是低权限只能推送到 dev 分支之中
  • 正式对外版本需要通过 SVN 构建分支版本, 比如 v20250917 做成个 SVN 分支版本推送
  • 可以编写 hook 脚本自动同步提交到 git, 然后发起 PR 合并 devmain 准备出版本
  • 审核合并通过触发自动化测试, 最后自动构建对外扩展版本发布

这里有小技巧, 能够看到 GITHUBReadme.md 很多项目都可以看到构建单元执行是否成功(也就是有个红色|绿色标记), 可以直接按照以下方式声明自己的自动化状态:

# 利用 workflows 自动化生成图标声明状态
# Github 用于声明打包状态图片标识, 只有配置 workflows 生成自动化流程才支持
https://github.com/quarkiverse/quarkus-neo4j/actions/workflows/build.yml/badge.svg
[![Build]({你的仓库地址}/actions/workflows/Build/badge.svg?branch=main)]({你的仓库地址}/actions?query=workflow%3ABuild)
# 例子如下
[![Build](https://github.com/quarkiverse/quarkus-neo4j/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/quarkiverse/quarkus-neo4j/actions?query=workflow%3ABuild)


# 利用 https://img.shields.io/github 网址来索引生成地址小图标状态
# Github 用于声明开源许可图片标识
[![License](https://img.shields.io/github/license/{你的仓库地址})](http://www.apache.org/licenses/LICENSE-2.0)
# 例子如下
[![License](https://img.shields.io/github/license/quarkiverse/.github)](http://www.apache.org/licenses/LICENSE-2.0)


# 生成当前的仓库版本图片标识
[![Central](https://img.shields.io/maven-central/v/{插件gorup-id}/{插件artifact-id}?color=green)](https://search.maven.org/search?q=g:{插件gorup-id}%20AND%20a:{插件artifact-id})
# 例子如下
[![Central](https://img.shields.io/maven-central/v/io.quarkiverse.neo4j/quarkus-neo4j-parent?color=green)](https://search.maven.org/search?q=g:io.quarkiverse.neo4j%20AND%20a:quarkus-neo4j-parent)

还有些图标展示小技巧, 可以参考其他 Github 仓库来配置.