之前已经展示直接将 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 目录, 内部则是标准的 runtime 和 deployment(之前提到的概念)
第一次初始化最好 compile 编译生成下保证通过没问题, 有的 Github 拉取的扩展模板可能有问题, 确认没问题再继续后续功能.
还有一点需要注意, 那就是
不要去动下载扩展模板的 JDK 版本, 而是直接采用模板默认 JDK 版本来开发.
引入 pekko
现在就可以开始引入 pekko-actor 基础功能, 如果扩展比较单一且简单的情况, 直接 runtime 和 deployment 就可以.
但是功能很复杂甚至包含大量框架集成封装, 就再追加声明 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 System 的 Bean 并且暴露给打包处理器处理, 这里需要声明 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 System 被 Quarkus 注入, 可以直接 @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之中
编译一下之后重新构建测试单元样例, 追加支持 @Inject 的 Actor 对象
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: 定义项目的构建和基础测试流程, 拉取代码配置编译环境(如JDK和Maven).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
日常如果计划发布新版本应该这样处理:
- 修改
project.yml配置,current-version设为上线版本,next-version设下个开发版本 - 功能代码随着
project.yml同步修改到main分支.PR合并后,release-prepare.yml会自动运行 release-prepare.yml会自动生成发布说明|发布标签|更新项目版本号, 触发后续release-perform.ymlrelease-perform.yml触发完成自动在GitHub仓库创建Release页面, 附带发布说明和产物
这就是整体的自动化发布流程, 这个发布流程也是很值得推广的正确步骤.
具体的发布模板可以参考官方: github自动化
SVN 集成开发, 我这边个人是采用 svn 做总的内部版本管理, 通过自带的 svn-branch(SVN也有自带的分支功能) 做对外版本:
- 内部版本的
.git也会参与托管, 但是clone|pull|push的账号是低权限只能推送到dev分支之中 - 正式对外版本需要通过
SVN构建分支版本, 比如v20250917做成个SVN分支版本推送 - 可以编写
hook脚本自动同步提交到git, 然后发起PR合并dev到main准备出版本 - 审核合并通过触发自动化测试, 最后自动构建对外扩展版本发布
这里有小技巧, 能够看到 GITHUB 的 Readme.md 很多项目都可以看到构建单元执行是否成功(也就是有个红色|绿色标记),
可以直接按照以下方式声明自己的自动化状态:
# 利用 workflows 自动化生成图标声明状态
# Github 用于声明打包状态图片标识, 只有配置 workflows 生成自动化流程才支持
https://github.com/quarkiverse/quarkus-neo4j/actions/workflows/build.yml/badge.svg
[]({你的仓库地址}/actions?query=workflow%3ABuild)
# 例子如下
[](https://github.com/quarkiverse/quarkus-neo4j/actions?query=workflow%3ABuild)
# 利用 https://img.shields.io/github 网址来索引生成地址小图标状态
# Github 用于声明开源许可图片标识
[](http://www.apache.org/licenses/LICENSE-2.0)
# 例子如下
[](http://www.apache.org/licenses/LICENSE-2.0)
# 生成当前的仓库版本图片标识
[](https://search.maven.org/search?q=g:{插件gorup-id}%20AND%20a:{插件artifact-id})
# 例子如下
[](https://search.maven.org/search?q=g:io.quarkiverse.neo4j%20AND%20a:quarkus-neo4j-parent)
还有些图标展示小技巧, 可以参考其他 Github 仓库来配置.