编写你自己的扩展
1. 扩展理念
本节正在进行中,收集了扩展应该被设计和编写的理念。
1.1. 为什么需要扩展框架
Quarkus 的使命是将你的整个应用程序(包括它使用的库)转换成一个比传统方法使用更少资源的工件。然后可以使用 GraalVM 构建原生应用程序。为此,你需要分析和理解应用程序的完整“封闭世界”。如果没有完整和完整的上下文,最多只能实现部分和有限的通用支持。通过使用 Quarkus 扩展方法,我们可以使 Java 应用程序与 Kubernetes 或云平台等内存占用受限的环境保持一致。
即使不使用 GraalVM(例如,在 HotSpot 中),Quarkus 扩展框架也能显著提高资源利用率。让我们列出扩展执行的操作
-
收集构建时元数据并生成代码
-
这部分与 GraalVM 无关,这是 Quarkus “在构建时”启动框架的方式
-
扩展框架有助于读取元数据、扫描类以及根据需要生成类
-
扩展工作的一小部分通过生成的类在运行时执行,而大部分工作在构建时(称为部署时)完成
-
-
根据应用程序的封闭世界视图强制执行有主见和明智的默认值(例如,没有
@Entity
的应用程序不需要启动 Hibernate ORM) -
扩展托管 Substrate VM 代码替换,以便库可以在 GraalVM 上运行
-
大多数更改都被推送到上游,以帮助底层库在 GraalVM 上运行
-
并非所有更改都可以推送到上游,扩展托管 Substrate VM 替换 - 这是一种代码修补形式 - 以便库可以运行
-
-
托管 Substrate VM 代码替换以帮助基于应用程序需求的死代码消除
-
这取决于应用程序,实际上无法在库本身中共享
-
例如,Quarkus 优化了 Hibernate 代码,因为它知道它只需要特定的连接池和缓存提供程序
-
-
将元数据发送到 GraalVM,例如需要反射的类
-
此信息不是每个库(例如 Hibernate)都是静态的,但框架具有语义知识,并且知道哪些类需要进行反射(例如 @Entity 类)
-
1.2. 优先选择构建时工作而不是运行时工作
尽可能地优先在构建时(扩展的部署部分)完成工作,而不是让框架在启动时(运行时)完成工作。完成的工作越多,使用该扩展的 Quarkus 应用程序就越小,加载速度也就越快。
1.3. 如何公开配置
Quarkus 简化了最常见的用法。这意味着它的默认值可能与它集成的库不同。
为了使简单体验最容易,请通过 SmallRye Config 在 application.properties
中统一配置。避免使用特定于库的配置文件,或者至少使它们成为可选文件:例如,Hibernate ORM 的 persistence.xml
是可选的。
扩展应该将配置作为一个整体的 Quarkus 应用程序来看待,而不是专注于库体验。例如,quarkus.database.url
和相关属性在扩展之间共享,因为定义数据库访问是一项共享任务(而不是 hibernate.
属性)。最有用的配置选项应该公开为 quarkus.[extension].
,而不是库的自然命名空间。不太常见的属性可以存在于库命名空间中。
为了完全启用 Quarkus 可以最佳优化的封闭世界假设,最好将配置选项视为构建时确定的选项,而不是运行时可覆盖的选项。当然,像主机、端口、密码这样的属性应该可以在运行时覆盖。但是,许多属性(如启用缓存或设置 JDBC 驱动程序)可以安全地要求重建应用程序。
1.4. 通过 CDI 公开你的组件
由于 CDI 在组件组合方面是中心编程模型,因此框架和扩展应该将其组件公开为可以被用户应用程序轻松使用的 bean。例如,Hibernate ORM 公开 EntityManagerFactory
和 EntityManager
bean,连接池公开 DataSource
bean 等。扩展必须在构建时注册这些 bean 定义。
1.4.1. 由类支持的 Bean
扩展可以生成一个 AdditionalBeanBuildItem
来指示容器从类中读取 bean 定义,就好像它是原始应用程序的一部分一样
AdditionalBeanBuildItem
注册的 Bean 类@Singleton (1)
public class Echo {
public String echo(String val) {
return val;
}
}
1 | 如果由 AdditionalBeanBuildItem 注册的 bean 未指定作用域,则假定为 @Dependent 。 |
所有其他 bean 都可以注入这样的 bean
AdditionalBeanBuildItem
生成的 Bean 的 Bean@Path("/hello")
public class ExampleResource {
@Inject
Echo echo;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello(String foo) {
return echo.echo(foo);
}
}
反之亦然 - 扩展 bean 可以注入应用程序 bean 和其他扩展提供的 bean
@Singleton
public class Echo {
@Inject
DataSource dataSource; (1)
@Inject
Instance<List<String>> listsOfStrings; (2)
//...
}
1 | 注入其他扩展提供的 bean。 |
2 | 注入与类型 List<String> 匹配的所有 bean。 |
1.4.2. Bean 初始化
某些组件可能需要基于增强期间收集的信息进行额外的初始化。最直接的解决方案是获取 bean 实例并直接从构建步骤中调用一个方法。但是,在增强阶段获取 bean 实例是非法的。原因是 CDI 容器尚未启动。它在 静态初始化引导阶段 启动。
BUILD_AND_RUN_TIME_FIXED 和 RUN_TIME 配置根可以在任何 bean 中注入。但是,RUN_TIME 配置根应该只在引导后注入。 |
可以从 记录器方法 中调用 bean 方法。如果你需要在 @Record(STATIC_INIT)
构建步骤中访问一个 bean,那么它必须依赖于 BeanContainerBuildItem
或者将逻辑包装在 BeanContainerListenerBuildItem
中。原因是简单的 - 我们需要确保 CDI 容器已完全初始化并启动。但是,可以安全地期望 CDI 容器已完全初始化并在 @Record(RUNTIME_INIT)
构建步骤中运行。你可以通过 CDI.current()
或 Quarkus 特定的 Arc.container()
获取对容器的引用。
不要忘记确保 bean 状态保证可见性,例如通过 volatile 关键字。 |
这种“延迟初始化”方法有一个显着的缺点。未初始化的 bean 可能会被引导期间实例化的其他扩展或应用程序组件访问。我们将在 合成 bean 中介绍一个更强大的解决方案。 |
1.4.3. 默认 Bean
创建此类 bean 的一个非常有用的模式,同时也使应用程序代码能够轻松地使用自定义实现覆盖某些 bean,是使用 Quarkus 提供的 @DefaultBean
。最好用一个例子来解释这一点。
让我们假设 Quarkus 扩展需要提供一个 Tracer
bean,应用程序代码应该将其注入到自己的 bean 中。
@Dependent
public class TracerConfiguration {
@Produces
public Tracer tracer(Reporter reporter, Configuration configuration) {
return new Tracer(reporter, configuration);
}
@Produces
@DefaultBean
public Configuration configuration() {
// create a Configuration
}
@Produces
@DefaultBean
public Reporter reporter(){
// create a Reporter
}
}
例如,如果应用程序代码想要使用 Tracer
,还需要使用自定义 Reporter
bean,这样的要求可以使用类似这样的方法轻松完成
@Dependent
public class CustomTracerConfiguration {
@Produces
public Reporter reporter(){
// create a custom Reporter
}
}
1.4.4. 如何覆盖由不使用 @DefaultBean 的库/Quarkus 扩展定义的 Bean
虽然 @DefaultBean
是推荐的方法,但应用程序代码也可以通过将 bean 标记为 CDI @Alternative
并包含 @Priority
注解来覆盖扩展提供的 bean。让我们展示一个简单的例子。假设我们正在处理一个虚构的 "quarkus-parser" 扩展,并且我们有一个默认的 bean 实现
@Dependent
class Parser {
String[] parse(String expression) {
return expression.split("::");
}
}
我们的扩展也使用了这个解析器
@ApplicationScoped
class ParserService {
@Inject
Parser parser;
//...
}
现在,如果用户甚至其他一些扩展需要覆盖 Parser
的默认实现,最简单的解决方案是使用 CDI @Alternative
+ @Priority
@Alternative (1)
@Priority(1) (2)
@Singleton
class MyParser extends Parser {
String[] parse(String expression) {
// my super impl...
}
}
1 | MyParser 是一个替代 bean。 |
2 | 启用替代方案。优先级可以是任何数字来覆盖默认 bean,但如果有多个替代方案,则优先级最高的获胜。 |
CDI 替代方案仅在注入和类型安全解析期间考虑。例如,默认实现仍然会收到观察者通知。 |
1.4.5. 合成 Bean
有时能够注册一个合成 bean 非常有用。合成 bean 的 bean 属性不是从 java 类、方法或字段派生的。相反,这些属性由扩展指定。
由于 CDI 容器不控制合成 bean 的实例化,因此不支持依赖注入和其他服务(例如拦截器)。换句话说,由扩展来为合成 bean 实例提供所有必需的服务。 |
SyntheticBeanBuildItem
可用于注册合成 bean
-
其实例可以通过 记录器 轻松生成,
-
以提供一个“上下文”bean,该 bean 保存增强期间收集的所有信息,以便实际组件不需要任何“延迟初始化”,因为它们可以直接注入上下文 bean。
@BuildStep
@Record(STATIC_INIT)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
return SyntheticBeanBuildItem.configure(Foo.class).scope(Singleton.class)
.runtimeValue(recorder.createFoo("parameters are recorded in the bytecode")) (1)
.done();
}
1 | 字符串值记录在字节码中,用于初始化 Foo 的实例。 |
@BuildStep
@Record(STATIC_INIT)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
return SyntheticBeanBuildItem.configure(TestContext.class).scope(Singleton.class)
.runtimeValue(recorder.createContext("parameters are recorded in the bytecode")) (1)
.done();
}
1 | “实际”组件可以直接注入 TestContext 。 |
1.5. 扩展的一些类型
存在多种扩展模式,让我们列出几个。
- 裸库运行
-
这是不太复杂的扩展。它包含一组补丁,以确保库在 GraalVM 上运行。如果可能,将这些补丁向上游贡献,而不是在扩展中。第二好的方法是编写 Substrate VM 替换,这些替换是在原生镜像编译期间应用的补丁。
- 让框架运行
-
框架在运行时通常读取配置、扫描类路径和类以查找元数据(注解、getter 等)、在它之上构建元模型、通过服务加载器模式查找选项、准备调用调用(反射)、代理接口等。
这些操作应该在构建时完成,并且元模型应该传递给记录器 DSL,该 DSL 将生成将在运行时执行并启动框架的类。 - 让 CDI 可移植扩展运行
-
CDI 可移植扩展模型非常灵活。过于灵活,无法从 Quarkus 推广的构建时启动中受益。我们见过的大多数扩展都没有利用这些极端的灵活性功能。将 CDI 扩展移植到 Quarkus 的方法是将其重写为 Quarkus 扩展,该扩展将在构建时(扩展术语中的部署时)定义各种 bean。
1.6. 能力水平
Quarkus 扩展可以做很多事情。扩展成熟度矩阵 提出了一个通过各种能力的路径,并提供了建议的实现顺序。
2. 技术方面
2.1. 引导的三个阶段和 Quarkus 理念
Quarkus 应用程序有三个不同的引导阶段
- 增强
-
这是第一个阶段,由构建步骤处理器完成。这些处理器可以访问 Jandex 注解信息,并且可以解析任何描述符并读取注解,但不应尝试加载任何应用程序类。这些构建步骤的输出是一些记录的字节码,使用 ObjectWeb ASM 项目的一个扩展 Gizmo(ext/gizmo),用于实际引导运行时应用程序。根据与构建步骤关联的
io.quarkus.deployment.annotations.Record
注解的io.quarkus.deployment.annotations.ExecutionTime
值,该步骤可能在不同的 JVM 中运行,基于以下两种模式。 - 静态初始化
-
如果字节码以
@Record(STATIC_INIT)
记录,那么它将从主类上的静态初始化方法执行。对于原生可执行文件构建,此代码作为原生构建过程的一部分在普通的 JVM 中执行,并且在此阶段产生的任何保留对象都将通过镜像映射文件直接序列化到原生可执行文件中。这意味着如果一个框架可以在此阶段启动,那么它的启动状态将直接写入镜像,因此在启动镜像时不需要执行启动代码。在此阶段可以执行的操作有一些限制,因为 Substrate VM 不允许原生可执行文件中的某些对象。例如,你不应该尝试在此阶段监听端口或启动线程。此外,不允许在静态初始化期间读取运行时配置。
在非原生的纯 JVM 模式下,静态初始化和运行时初始化之间没有真正的区别,除了静态初始化总是先执行。此模式受益于与原生模式相同的构建阶段增强,因为描述符解析和注解扫描在构建时完成,并且任何相关的类/框架依赖项都可以从构建输出 jar 中删除。在像 WildFly 这样的服务器中,部署相关的类(例如 XML 解析器)会在应用程序的整个生命周期中存在,从而占用宝贵的内存。Quarkus 旨在消除这种情况,以便运行时加载的唯一类实际上在运行时使用。
例如,Quarkus 应用程序加载 XML 解析器的唯一原因是用户在其应用程序中使用 XML。配置的任何 XML 解析都应该在增强阶段完成。
- 运行时初始化
-
如果字节码以
@Record(RUNTIME_INIT)
记录,那么它将从应用程序的主方法执行。此代码将在原生可执行文件启动时运行。一般来说,在此阶段应该执行尽可能少的代码,并且应该限制为需要打开端口等的代码。
尽可能多地推送到 @Record(STATIC_INIT)
阶段允许两种不同的优化
-
在原生可执行文件和纯 JVM 模式下,这都允许应用程序尽可能快地启动,因为处理是在构建时完成的。这也最大限度地减少了应用程序中所需的类/原生代码,使其仅限于纯运行时相关的行为。
-
原生可执行文件模式的另一个好处是 Substrate 可以更容易地消除未使用的功能。如果功能通过字节码直接初始化,Substrate 可以检测到一个方法从未被调用并消除该方法。如果在运行时读取配置,Substrate 无法推断配置的内容,因此需要保留所有功能以防需要。
2.2. 项目设置
你的扩展项目应该设置为一个多模块项目,其中包含两个子模块
-
一个部署时子模块,用于处理构建时处理和字节码记录。
-
一个运行时子模块,其中包含运行时行为,该行为将在原生可执行文件或运行时 JVM 中提供扩展行为。
你的运行时工件应该依赖于 io.quarkus:quarkus-core
,如果你想使用它们提供的功能,也可以依赖于其他 Quarkus 模块的运行时工件。
你的部署时模块应该依赖于 io.quarkus:quarkus-core-deployment
、你的运行时工件以及你自己的扩展所依赖的任何其他 Quarkus 扩展的部署工件。这是必不可少的,否则任何传递性拉入的扩展将不会提供其全部功能。
Maven 和 Gradle 插件将为你验证这一点,并提醒你可能忘记添加的任何部署工件。 |
在任何情况下,运行时模块都不能依赖于部署工件。这将导致将所有部署时代码拉入运行时范围,这将破坏拆分的初衷。 |
2.2.1. 使用 Maven
你需要包含 io.quarkus:quarkus-extension-maven-plugin
并配置 maven-compiler-plugin
来检测 quarkus-extension-processor
注解处理器,以收集和生成必要的 Quarkus 扩展元数据,用于扩展工件,如果你使用 Quarkus 父 pom,它将自动继承正确的配置。
你可能想使用 io.quarkus.platform:quarkus-maven-plugin 的 create-extension mojo 来创建这些 Maven 模块 - 请参阅下一节。 |
按照惯例,部署时工件具有 -deployment 后缀,运行时工件没有后缀(并且是最终用户添加到他们的项目中的)。 |
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-maven-plugin</artifactId>
<!-- Executions configuration can be inherited from quarkus-build-parent -->
<executions>
<execution>
<goals>
<goal>extension-descriptor</goal>
</goals>
<configuration>
<deployment>${project.groupId}:${project.artifactId}-deployment:${project.version}</deployment>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
上面的 maven-compiler-plugin 配置需要 3.5+ 版本。 |
你还需要配置部署模块的 maven-compiler-plugin
以检测 quarkus-extension-processor
注解处理器。
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
2.2.1.1. 使用 Maven 创建新的 Quarkus Core 扩展模块
Quarkus 提供了 create-extension
Maven Mojo 来初始化你的扩展项目。
它将尝试自动检测其选项
-
从
quarkus
(Quarkus Core) 或quarkus/extensions
目录,它将使用 "Quarkus Core" 扩展布局和默认值。 -
使用
-DgroupId=io.quarkiverse.[extensionId]
,它将使用 "Quarkiverse" 扩展布局和默认值。 -
在其他情况下,它将使用 "Standalone" 扩展布局和默认值。
-
我们将来可能会引入其他布局类型。
你可能没有指定任何参数来使用交互模式:mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create-extension -N |
作为一个例子,让我们向 Quarkus 源代码树添加一个名为 my-ext
的新扩展
git clone https://github.com/quarkusio/quarkus.git
cd quarkus
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create-extension -N \
-DextensionId=my-ext \
-DextensionName="My Extension" \
-DextensionDescription="Do something useful."
默认情况下,groupId 、version 、quarkusVersion 、namespaceId 和 namespaceName 将与其他 Quarkus 核心扩展保持一致。 |
扩展描述很重要,因为它显示在 https://code.quarkus.io/ 上,当使用 Quarkus CLI 列出扩展时等等。 |
上面的命令序列执行以下操作
-
创建四个新的 Maven 模块
-
quarkus-my-ext-parent
在extensions/my-ext
目录中 -
quarkus-my-ext
在extensions/my-ext/runtime
目录中 -
quarkus-my-ext-deployment
在extensions/my-ext/deployment
目录中;一个基本的MyExtProcessor
类在此模块中生成。 -
quarkus-my-ext-integration-test
在integration-tests/my-ext/deployment
目录中;一个空的 Jakarta REST Resource 类和两个测试类(用于 JVM 模式和原生模式)在此模块中生成。
-
-
在必要时链接这三个模块
-
quarkus-my-ext-parent
被添加到quarkus-extensions-parent
的<modules>
中 -
quarkus-my-ext
被添加到 Quarkus BOM (材料清单)bom/application/pom.xml
的<dependencyManagement>
中 -
quarkus-my-ext-deployment
被添加到 Quarkus BOM (材料清单)bom/application/pom.xml
的<dependencyManagement>
中 -
quarkus-my-ext-integration-test
被添加到quarkus-integration-tests-parent
的<modules>
中
-
你还必须填写 quarkus-extension.yaml 模板文件,该文件描述了运行时模块 src/main/resources/META-INF 文件夹中的扩展。 |
这是 quarkus-agroal
扩展的 quarkus-extension.yaml
模板,你可以将其用作示例
name: "Agroal - Database connection pool" (1)
metadata:
keywords: (2)
- "agroal"
- "database-connection-pool"
- "datasource"
- "jdbc"
guide: "https://quarkus.net.cn/guides/datasource" (3)
categories: (4)
- "data"
status: "stable" (5)
1 | 将向用户显示的扩展的名称 |
2 | 可用于在扩展目录中查找扩展的关键字 |
3 | 链接到扩展的指南或文档 |
4 | 扩展应显示在 code.quarkus.io 上的类别,可以省略,在这种情况下,扩展仍将列出,但不在任何特定类别下 |
5 | 成熟度状态,可以是 stable 、preview 或 experimental ,由扩展维护者评估 |
mojo 的 name 参数是可选的。如果你没有在命令行上指定它,插件将通过将破折号替换为空格并大写每个标记来从 extensionId 中派生它。因此,你可以考虑在某些情况下省略显式 name 。 |
请参考 CreateExtensionMojo JavaDoc 了解 mojo 的所有可用选项。
2.2.2. 使用 Gradle
你需要在扩展项目的 runtime
模块中应用 io.quarkus.extension
插件。该插件包含 extensionDescriptor
任务,该任务将生成 META-INF/quarkus-extension.properties
和 META-INF/quarkus-extension.yml
文件。该插件还在 deployment
和 runtime
模块中启用 io.quarkus:quarkus-extension-processor
注解处理器,以收集和生成其余的 Quarkus 扩展元数据。可以通过设置 deploymentModule
属性在插件中配置部署模块的名称。默认情况下,该属性设置为 deployment
plugins {
id 'java'
id 'io.quarkus.extension'
}
quarkusExtension {
deploymentModule = 'deployment'
}
dependencies {
implementation platform('io.quarkus:quarkus-bom:3.24.4')
}
2.3. 构建步骤处理器
工作在增强时由构建步骤完成,这些步骤生成和消耗构建项。在与项目构建中的扩展相对应的部署模块中找到的构建步骤会自动连接在一起并执行,以生成最终的构建工件。
2.3.1. 构建步骤
构建步骤是用 @io.quarkus.deployment.annotations.BuildStep
注解的非静态方法。每个构建步骤都可以消耗先前阶段生成的项,并且可以生成可以被后续阶段消耗的项。构建步骤通常仅在它们生成最终被另一个步骤消耗的构建项时才运行。
构建步骤通常放置在扩展的部署模块中的普通类上。这些类在增强过程中会自动实例化,并利用注入。
2.3.2. 构建项
构建项是抽象类 io.quarkus.builder.item.BuildItem
的具体、最终子类。每个构建项表示必须从一个阶段传递到另一个阶段的一些信息单元。基本 BuildItem
类本身可能不会被直接子类化;相反,每个可能创建的构建项子类的类型都有抽象子类:简单、多和空。
将构建项视为不同的扩展彼此通信的一种方式。例如,构建项可以
-
公开数据库配置的存在
-
消耗该数据库配置(例如,连接池扩展或 ORM 扩展)
-
要求扩展为另一个扩展执行工作:例如,一个想要定义一个新的 CDI bean 的扩展,并要求 ArC 扩展这样做
这是一种非常灵活的机制。
BuildItem 实例应该是不可变的,因为生产者/消费者模型不允许正确排序突变。这不是强制执行的,但未能遵守此规则可能会导致竞争条件。 |
构建步骤仅在其生成构建项时执行,构建项必须被其他构建步骤(传递)需要。确保你的构建步骤生成一个构建项,否则你可能应该为构建验证生成 ValidationErrorBuildItem ,或者为生成的工件生成 ArtifactResultBuildItem 。 |
2.3.2.1. 简单构建项
简单构建项是扩展 io.quarkus.builder.item.SimpleBuildItem
的最终类。简单构建项只能由给定构建中的一个步骤生成;如果构建中的多个步骤声明它们生成相同的简单构建项,则会引发错误。任意数量的构建步骤可以消耗一个简单构建项。消耗简单构建项的构建步骤将始终在生成该项的构建步骤之后运行。
/**
* The build item which represents the Jandex index of the application,
* and would normally be used by many build steps to find usages
* of annotations.
*/
public final class ApplicationIndexBuildItem extends SimpleBuildItem {
private final Index index;
public ApplicationIndexBuildItem(Index index) {
this.index = index;
}
public Index getIndex() {
return index;
}
}
2.3.2.2. 多构建项
多个或“多”构建项是扩展 io.quarkus.builder.item.MultiBuildItem
的最终类。任何数量的给定类的多构建项可以由任意数量的步骤生成,但是消耗多构建项的任何步骤都只会在每个可以生成它们的步骤运行之后运行。
public final class ServiceWriterBuildItem extends MultiBuildItem {
private final String serviceName;
private final List<String> implementations;
public ServiceWriterBuildItem(String serviceName, String... implementations) {
this.serviceName = serviceName;
// Make sure it's immutable
this.implementations = Collections.unmodifiableList(
Arrays.asList(
implementations.clone()
)
);
}
public String getServiceName() {
return serviceName;
}
public List<String> getImplementations() {
return implementations;
}
}
/**
* This build step produces a single multi build item that declares two
* providers of one configuration-related service.
*/
@BuildStep
public ServiceWriterBuildItem registerOneService() {
return new ServiceWriterBuildItem(
Converter.class.getName(),
MyFirstConfigConverterImpl.class.getName(),
MySecondConfigConverterImpl.class.getName()
);
}
/**
* This build step produces several multi build items that declare multiple
* providers of multiple configuration-related services.
*/
@BuildStep
public void registerSeveralServices(
BuildProducer<ServiceWriterBuildItem> providerProducer
) {
providerProducer.produce(new ServiceWriterBuildItem(
Converter.class.getName(),
MyThirdConfigConverterImpl.class.getName(),
MyFourthConfigConverterImpl.class.getName()
));
providerProducer.produce(new ServiceWriterBuildItem(
ConfigSource.class.getName(),
MyConfigSourceImpl.class.getName()
));
}
/**
* This build step aggregates all the produced service providers
* and outputs them as resources.
*/
@BuildStep
public void produceServiceFiles(
List<ServiceWriterBuildItem> items,
BuildProducer<GeneratedResourceBuildItem> resourceProducer
) throws IOException {
// Aggregate all the providers
Map<String, Set<String>> map = new HashMap<>();
for (ServiceWriterBuildItem item : items) {
String serviceName = item.getName();
for (String implName : item.getImplementations()) {
map.computeIfAbsent(
serviceName,
(k, v) -> new LinkedHashSet<>()
).add(implName);
}
}
// Now produce the resource(s) for the SPI files
for (Map.Entry<String, Set<String>> entry : map.entrySet()) {
String serviceName = entry.getKey();
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
try (OutputStreamWriter w = new OutputStreamWriter(os, StandardCharsets.UTF_8)) {
for (String implName : entry.getValue()) {
w.write(implName);
w.write(System.lineSeparator());
}
w.flush();
}
resourceProducer.produce(
new GeneratedResourceBuildItem(
"META-INF/services/" + serviceName,
os.toByteArray()
)
);
}
}
}
2.3.2.3. 空构建项
空构建项是扩展 io.quarkus.builder.item.EmptyBuildItem
的最终(通常为空)类。它们表示实际上不携带任何数据的构建项,并允许在不必实例化空类的情况下生成和消耗此类项。它们本身不能被实例化。
由于它们无法实例化,它们无法通过任何方式注入,也无法被构建步骤(或通过 BuildProducer )返回。要生成一个空构建项,你必须用 @Produce(MyEmptyBuildItem.class) 注解构建步骤,并用 @Consume(MyEmptyBuildItem.class) 消耗它。 |
public final class NativeImageBuildItem extends EmptyBuildItem {
// empty
}
空构建项可以表示“屏障”,这可以在步骤之间施加排序。它们也可以以流行的构建系统使用“伪目标”的相同方式使用,也就是说,构建项可以表示没有具体表示的概念目标。
/**
* Contrived build step that produces the native image on disk. The main augmentation
* step (which is run by Maven or Gradle) would be declared to consume this empty item,
* causing this step to be run.
*/
@BuildStep
@Produce(NativeImageBuildItem.class)
void produceNativeImage() {
// ...
// (produce the native image)
// ...
}
/**
* This would always run after {@link #produceNativeImage()} completes, producing
* an instance of {@code SomeOtherBuildItem}.
*/
@BuildStep
@Consume(NativeImageBuildItem.class)
SomeOtherBuildItem secondBuildStep() {
return new SomeOtherBuildItem("foobar");
}
2.3.2.4. 验证错误构建项
它们表示具有验证错误的构建项,这会导致构建失败。这些构建项在 CDI 容器初始化期间被消耗。
@BuildStep
void checkCompatibility(Capabilities capabilities, BuildProducer<ValidationErrorBuildItem> validationErrors) {
if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)
&& capabilities.isPresent(Capability.RESTEASY)) {
validationErrors.produce(new ValidationErrorBuildItem(
new ConfigurationException("Cannot use both RESTEasy Classic and Reactive extensions at the same time")));
}
}
2.3.3. 注入
包含构建步骤的类支持以下类型的注入
-
构造函数参数注入
-
字段注入
-
方法参数注入(仅适用于构建步骤方法)
构建步骤类是为每个构建步骤调用实例化和注入的,并且之后会被丢弃。状态应该仅通过构建项在构建步骤之间传递,即使步骤位于同一类上。
最终字段不被考虑用于注入,但如果需要,可以通过构造函数参数注入来填充。静态字段永远不被考虑用于注入。 |
可以注入的值的类型包括
注入到构建步骤方法或其类中的对象不得在该方法执行之外使用。 |
注入在编译时通过注解处理器解析,并且生成的代码没有权限注入私有字段或调用私有方法。 |
2.3.4. 生成值
构建步骤可以通过以下几种可能的方式为后续步骤生成值
如果在构建步骤中声明了一个简单构建项,则必须在该构建步骤中生成它,否则将导致错误。注入到步骤中的构建生产者不得在该步骤之外使用。
请注意,只有当 @BuildStep
方法生成的内容是另一个使用者或最终输出所需要的时,该方法才会被调用。如果某个特定项目没有使用者,则不会生成该项目。所需的内容将取决于最终生成的目标。例如,在开发者模式下运行时,最终输出不会要求 GraalVM 特定的构建项目(例如 ReflectiveClassBuildItem
),因此只会生成这些项目的方法不会被调用。
2.3.5. 消费值
构建步骤可以通过以下方式消费先前步骤中的值
通常,如果一个被包含的步骤消费了一个没有被其他任何步骤产生的简单构建项目,则会发生错误。通过这种方式,可以保证当步骤运行时,所有声明的值都存在且非 null
。
有时,一个值对于构建完成不是必需的,但如果存在,可能会影响构建步骤的某些行为。在这种情况下,该值可以被选择性地注入。
多构建值始终被认为是可选的。如果不存在,则会注入一个空列表。 |
2.3.5.1. 弱值生成
通常,只要一个构建步骤生成了可以被其他任何构建步骤消费的构建项目,该构建步骤就会被包含。通过这种方式,只有生成最终工件所需的步骤会被包含,而那些与未安装的扩展相关的步骤,或只生成与给定工件类型无关的构建项目的步骤会被排除。
对于不希望出现此行为的情况,可以使用 @io.quarkus.deployment.annotations.Weak
注解。此注解表明,不应仅根据生成被注解的值来自动包含构建步骤。
/**
* This build step is only run if something consumes the ExecutorClassBuildItem.
*/
@BuildStep
void createExecutor(
@Weak BuildProducer<GeneratedClassBuildItem> classConsumer,
BuildProducer<ExecutorClassBuildItem> executorClassConsumer
) {
ClassWriter cw = new ClassWriter(Gizmo.ASM_API_VERSION);
String className = generateClassThatCreatesExecutor(cw); (1)
classConsumer.produce(new GeneratedClassBuildItem(true, className, cw.toByteArray()));
executorClassConsumer.produce(new ExecutorClassBuildItem(className));
}
1 | 此方法(未在此示例中提供)将使用 ASM API 生成类。 |
某些类型的构建项目通常总是被消费,例如生成的类或资源。一个扩展可能会生成一个构建项目以及一个生成的类,以方便该构建项目的使用。这样的构建步骤会在生成的类构建项目上使用 @Weak
注解,同时正常生成其他构建项目。如果其他构建项目最终被某些东西消费,那么该步骤将会运行,并且该类将被生成。如果没有东西消费其他构建项目,则该步骤将不会包含在构建过程中。
在上面的示例中,只有当 ExecutorClassBuildItem
被其他构建步骤消费时,才会生成 GeneratedClassBuildItem
。
请注意,当使用字节码记录时,可以通过使用 @io.quarkus.deployment.annotations.Record
注解的 optional
属性来声明隐式生成的类是弱的。
/**
* This build step is only run if something consumes the ExecutorBuildItem.
*/
@BuildStep
@Record(value = ExecutionTime.RUNTIME_INIT, optional = true) (1)
ExecutorBuildItem createExecutor( (2)
ExecutorRecorder recorder,
ThreadPoolConfig threadPoolConfig
) {
return new ExecutorBuildItem(
recorder.setupRunTime(
shutdownContextBuildItem,
threadPoolConfig,
launchModeBuildItem.getLaunchMode()
)
);
}
1 | 请注意 optional 属性。 |
2 | 此示例使用的是记录器代理;有关更多信息,请参见字节码记录一节。 |
2.3.6. 应用程序归档
@BuildStep
注解还可以注册标记文件,这些标记文件决定了类路径上的哪些归档被认为是“应用程序归档”,因此将被索引。这是通过 applicationArchiveMarkers
完成的。例如,ArC 扩展注册了 META-INF/beans.xml
,这意味着类路径上所有带有 beans.xml
文件的归档都将被索引。
2.3.7. 使用线程的上下文类加载器
构建步骤将在 TCCL 中运行,该 TCCL 可以以转换器安全的方式从部署中加载用户类。此类加载器仅在增强的生命周期内有效,之后将被丢弃。这些类将在运行时在不同的类加载器中再次加载。这意味着在增强期间加载类不会阻止其在开发/测试模式下运行时被转换。
2.3.8. 使用 IndexDependencyBuildItem 将外部 JAR 添加到索引器
扫描类的索引不会自动包含您的外部类依赖项。要添加依赖项,请创建一个 @BuildStep
,该步骤生成 IndexDependencyBuildItem
对象,用于 groupId
和 artifactId
。
重要的是指定所有需要添加到索引器的工件。不会隐式地添加任何传递工件。 |
Amazon Alexa
扩展添加了来自 Alexa SDK 的依赖库,这些库用于 Jackson JSON 转换,以便在 BUILD_TIME
识别并包含反射类。
@BuildStep
void addDependencies(BuildProducer<IndexDependencyBuildItem> indexDependency) {
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-runtime"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-model"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-lambda-support"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-servlet-support"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-dynamodb-persistence-adapter"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-apache-client"));
indexDependency.produce(new IndexDependencyBuildItem("com.amazon.alexa", "ask-sdk-model-runtime"));
}
将工件添加到 Jandex
索引器后,您现在可以搜索索引以识别实现接口的类、特定类的子类或带有目标注解的类。
例如,Jackson
扩展使用如下代码来搜索 JSON 反序列化中使用的注解,并将它们添加到 BUILD_TIME
分析的反射层次结构中。
DotName JSON_DESERIALIZE = DotName.createSimple(JsonDeserialize.class.getName());
IndexView index = combinedIndexBuildItem.getIndex();
// handle the various @JsonDeserialize cases
for (AnnotationInstance deserializeInstance : index.getAnnotations(JSON_DESERIALIZE)) {
AnnotationTarget annotationTarget = deserializeInstance.target();
if (CLASS.equals(annotationTarget.kind())) {
DotName dotName = annotationTarget.asClass().name();
Type jandexType = Type.create(dotName, Type.Kind.CLASS);
reflectiveHierarchyClass.produce(new ReflectiveHierarchyBuildItem(jandexType));
}
}
2.3.9. 可视化构建步骤依赖项
有时,查看各种构建步骤之间的交互的可视化表示会很有用。对于这种情况,在构建应用程序时添加 -Dquarkus.builder.graph-output=build.dot
将导致在项目的根目录中创建 build.dot
文件。请参阅此以获取可以打开该文件并显示实际可视化表示的软件列表。
2.4. 配置
Quarkus 中的配置基于 SmallRye Config。 SmallRye Config 提供的所有功能在 Quarkus 中也可用。
扩展必须使用 SmallRye Config @ConfigMapping 来映射扩展所需的配置。这将允许 Quarkus 自动向每个配置阶段公开映射的实例,并生成配置文档。
2.4.1. 配置阶段
配置映射严格受配置阶段的约束,尝试从其对应的阶段之外访问配置映射将导致错误。它们决定了何时从配置中读取包含的键,以及何时可供应用程序使用。 io.quarkus.runtime.annotations.ConfigPhase
定义的阶段如下
阶段名称 | 在构建时读取和可用 | 在运行时可用 | 在静态初始化期间读取 | 在启动时重新读取(本机可执行文件) | 备注 |
---|---|---|---|---|---|
|
✓ |
✗ |
✗ |
✗ |
适用于影响构建的事物。 |
|
✓ |
✓ |
✗ |
✗ |
适用于影响构建并且必须对运行时代码可见的事物。在运行时不从配置中读取。 |
|
✗ |
✓ |
✓ |
✓ |
在构建时不可用,在所有模式下都在启动时读取。 |
对于除 BUILD_TIME
情况之外的所有情况,配置映射接口和其中包含的所有配置组和类型必须位于或可从扩展的运行时工件访问。阶段 BUILD_TIME
的配置映射可能位于或可从扩展的运行时或部署工件访问。
2.4.2. 配置示例
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import java.io.File;
import java.util.logging.Level;
/**
* Logging configuration.
*/
@ConfigMapping(prefix = "quarkus.log") (1)
@ConfigRoot(phase = ConfigPhase.RUN_TIME) (2)
public interface LogConfiguration {
// ...
/**
* Configuration properties for the logging file handler.
*/
FileConfig file();
interface FileConfig {
/**
* Enable logging to a file.
*/
@WithDefault("true")
boolean enable();
/**
* The log format.
*/
@WithDefault("%d{yyyy-MM-dd HH:mm:ss,SSS} %h %N[%i] %-5p [%c{1.}] (%t) %s%e%n")
String format();
/**
* The level of logs to be written into the file.
*/
@WithDefault("ALL")
Level level();
/**
* The name of the file in which logs will be written.
*/
@WithDefault("application.log")
File path();
}
}
public class LoggingProcessor {
// ...
/*
* Logging configuration.
*/
LogConfiguration config; (3)
}
配置属性名称可以分为多个段。例如,像 quarkus.log.file.enable
这样的属性名称可以分为以下几段
-
quarkus
- 由 Quarkus 声明的命名空间,它是@ConfigMapping
接口的前缀, -
log
- 名称段,对应于用@ConfigMapping
注解的接口中设置的前缀, -
file
- 名称段,对应于此类中的file
字段, -
enable
- 名称段,对应于FileConfig
中的enable
字段。
1 | @ConfigMapping 注解表明该接口是配置映射,在这种情况下,它对应于 quarkus.log 段。 |
2 | @ConfigRoot 注解指示配置应用于哪个配置阶段。 |
3 | 在这里,LoggingProcessor 通过检测 @ConfigRoot 注解来自动注入 LogConfiguration 实例。 |
上面示例的相应 application.properties
可能是
quarkus.log.file.enable=true
quarkus.log.file.level=DEBUG
quarkus.log.file.path=/tmp/debug.log
由于未在这些属性中定义 format
,因此将使用 @WithDefault
中的默认值。
配置映射名称可以包含一个额外的后缀段,以用于存在多个配置阶段的配置映射的情况。对应于 BUILD_TIME
和 BUILD_AND_RUN_TIME_FIXED
的类可能以 BuildTimeConfig
或 BuildTimeConfiguration
结尾,对应于 RUN_TIME
阶段的类可能以 RuntimeConfig
、RunTimeConfig
、RuntimeConfiguration
或 RunTimeConfiguration
结尾。
2.4.3. 配置参考文档
配置是每个扩展的重要组成部分,因此需要正确记录。每个配置属性都应具有适当的 Javadoc 注释。
虽然在编码时可以方便地获取文档,但配置文档也必须在扩展指南中提供。Quarkus 构建会根据 Javadoc 注释自动生成配置文档,但需要将其显式包含在每个指南中。
2.4.3.1. 编写文档
每个配置属性都需要 Javadoc 来解释其用途。
第一句话应有意义且独立,因为它包含在摘要表中。 |
虽然标准 Javadoc 注释对于简单的文档来说非常合适(甚至推荐),但 AsciiDoc 更适合提示、源代码提取、列表等
/**
* Class name of the Hibernate ORM dialect. The complete list of bundled dialects is available in the
* https://docs.jboss.com.cn/hibernate/stable/orm/javadocs/org/hibernate/dialect/package-summary.html[Hibernate ORM JavaDoc].
*
* [NOTE]
* ====
* Not all the dialects are supported in GraalVM native executables: we currently provide driver extensions for
* PostgreSQL, MariaDB, Microsoft SQL Server and H2.
* ====
*
* @asciidoclet
*/
Optional<String> dialect();
要使用 AsciiDoc,必须使用 @asciidoclet
标记注解 Javadoc 注释。此标记有两个用途:它被用作 Quarkus 生成工具的标记,但也被 javadoc
进程用于 Javadoc 生成。
一个更详细的示例
// @formatter:off
/**
* Name of the file containing the SQL statements to execute when Hibernate ORM starts.
* Its default value differs depending on the Quarkus launch mode:
*
* * In dev and test modes, it defaults to `import.sql`.
* Simply add an `import.sql` file in the root of your resources directory
* and it will be picked up without having to set this property.
* Pass `no-file` to force Hibernate ORM to ignore the SQL import file.
* * In production mode, it defaults to `no-file`.
* It means Hibernate ORM won't try to execute any SQL import file by default.
* Pass an explicit value to force Hibernate ORM to execute the SQL import file.
*
* If you need different SQL statements between dev mode, test (`@QuarkusTest`) and in production, use Quarkus
* https://quarkus.net.cn/guides/config#configuration-profiles[configuration profiles facility].
*
* [source,property]
* .application.properties
* ----
* %dev.quarkus.hibernate-orm.sql-load-script = import-dev.sql
* %test.quarkus.hibernate-orm.sql-load-script = import-test.sql
* %prod.quarkus.hibernate-orm.sql-load-script = no-file
* ----
*
* [NOTE]
* ====
* Quarkus supports `.sql` file with SQL statements or comments spread over multiple lines.
* Each SQL statement must be terminated by a semicolon.
* ====
*
* @asciidoclet
*/
// @formatter:on
Optional<String> sqlLoadScript();
为了在 Javadoc 注释中保留缩进(列表项分布在多行或缩进的源代码中),必须禁用自动 Eclipse 格式化程序(格式化程序自动包含在构建中),带有标记 // @formatter:off
/// @formatter:on
。这些需要在 //
标记后添加单独的注释和一个强制空格。
AsciiDoc 文档不支持打开块 ( |
默认情况下,文档生成器将使用带连字符的字段名称作为
可以为文档默认值编写文本解释,这在生成文档时很有用:
|
2.4.3.2. 编写章节文档
要生成给定组的配置章节,请使用 @ConfigDocSection
注解
/**
* Config group related configuration.
* Amazing introduction here
*/
@ConfigDocSection (1)
ConfigGroupConfig configGroup();
1 | 这将在生成的文档中为 configGroup 配置项添加章节文档。章节标题和介绍将从配置项的 javadoc 中派生。来自 javadoc 的第一句话被认为是章节标题,其余句子用作章节介绍。 |
2.4.3.3. 生成文档
要生成文档
-
执行
./mvnw -DquicklyDocs
-
可以在全局或特定扩展目录(例如
extensions/mailer
)中执行。
文档在项目的根目录下的全局 target/asciidoc/generated/config/
中生成。
2.4.3.4. 在扩展指南中包含文档
要在指南中包含生成的配置参考文档,请使用
include::{generated-dir}/config/quarkus-your-extension.adoc[opts=optional, leveloffset=+1]
要仅包含特定的配置组
include::{generated-dir}/hyphenated-config-group-class-name-with-runtime-or-deployment-namespace-replaced-by-config-group-namespace.adoc[opts=optional, leveloffset=+1]
例如,io.quarkus.vertx.http.runtime.FormAuthConfig
配置组将在名为 quarkus-vertx-http-config-group-form-auth-config.adoc
的文件中生成。
一些建议
-
opts=optional
是强制性的,以防止仅生成部分配置文档时构建失败。 -
生成的文档的标题级别为 2(即
==
)。可能需要使用leveloffset=+N
进行调整。 -
整个配置文档不应包含在指南的中间。
如果指南包含 application.properties
示例,则必须在代码片段下方添加提示
[TIP]
For more information about the extension configuration please refer to the <<configuration-reference,Configuration Reference>>.
并在指南的末尾添加广泛的配置文档
[[configuration-reference]]
== Configuration Reference
include::{generated-dir}/config/quarkus-your-extension.adoc[opts=optional, leveloffset=+1]
所有文档都应在提交之前生成和验证。 |
2.5. 条件步骤包含
可以仅在某些条件下包含给定的 @BuildStep
。 @BuildStep
注解有两个可选参数:onlyIf
和 onlyIfNot
。这些参数可以设置为一个或多个实现 BooleanSupplier
的类。仅当方法返回 true
(对于 onlyIf
)或 false
(对于 onlyIfNot
)时,才会包含构建步骤。
条件类可以注入配置映射,只要它们属于构建时阶段。运行时配置不适用于条件类。
条件类还可以注入类型为 io.quarkus.runtime.LaunchMode
的值。支持构造函数参数和字段注入。
@BuildStep(onlyIf = IsDevMode.class)
LogCategoryBuildItem enableDebugLogging() {
return new LogCategoryBuildItem("org.your.quarkus.extension", Level.DEBUG);
}
static class IsDevMode implements BooleanSupplier {
LaunchMode launchMode;
public boolean getAsBoolean() {
return launchMode == LaunchMode.DEVELOPMENT;
}
}
如果您需要使构建步骤依赖于另一个扩展的存在与否,可以使用Capabilities。 |
您还可以使用 @BuildSteps
将一组条件应用于给定类中的所有构建步骤
@BuildSteps(onlyIf = MyDevModeProcessor.IsDevMode.class) (1)
class MyDevModeProcessor {
@BuildStep
SomeOutputBuildItem mainBuildStep(SomeOtherBuildItem input) { (2)
return new SomeOutputBuildItem(input.getValue());
}
@BuildStep
SomeOtherOutputBuildItem otherBuildStep(SomeOtherInputBuildItem input) { (3)
return new SomeOtherOutputBuildItem(input.getValue());
}
static class IsDevMode implements BooleanSupplier {
LaunchMode launchMode;
public boolean getAsBoolean() {
return launchMode == LaunchMode.DEVELOPMENT;
}
}
}
1 | 此条件将应用于 MyDevModeProcessor 中定义的所有方法 |
2 | 主构建步骤仅在开发模式下执行。 |
3 | 其他构建步骤仅在开发模式下执行。 |
2.6. 生成字节码
2.6.1. 字节码记录
构建过程的主要输出之一是记录的字节码。此字节码实际上设置了运行时环境。例如,为了启动 Undertow,生成的应用程序将具有一些字节码,该字节码直接注册所有 Servlet 实例,然后启动 Undertow。
由于直接编写字节码很复杂,因此改为通过字节码记录器完成。在部署时,会调用包含实际运行时逻辑的记录器对象,但是这些调用不会像正常情况下那样进行,而是会被拦截和记录(因此得名)。然后,此记录用于生成字节码,该字节码在运行时执行相同的调用序列。这本质上是一种延迟执行的形式,其中在部署时进行的调用会被延迟到运行时。
让我们看一下经典的“Hello World”类型示例。要以 Quarkus 的方式执行此操作,我们将创建一个记录器,如下所示
@Recorder
class HelloRecorder {
public void sayHello(String name) {
System.out.println("Hello" + name);
}
}
然后创建一个使用此记录器的构建步骤
@Record(RUNTIME_INIT)
@BuildStep
public void helloBuildStep(HelloRecorder recorder) {
recorder.sayHello("World");
}
当运行此构建步骤时,不会在控制台中打印任何内容。这是因为注入的 HelloRecorder
实际上是一个代理,它记录所有调用。相反,如果我们运行生成的 Quarkus 程序,我们将看到“Hello World”打印到控制台。
记录器上的方法可以返回值,该值必须是可代理的(如果您想返回一个不可代理的项目,请将其包装在 io.quarkus.runtime.RuntimeValue
中)。但是,这些代理不能被直接调用,但可以传递到其他记录器方法中。这可以是任何记录器方法,包括来自其他 @BuildStep
方法的方法,因此一种常见的模式是生成包装这些记录器调用结果的 BuildItem
实例。
例如,为了对 Servlet 部署进行任意更改,Undertow 有一个 ServletExtensionBuildItem
,它是一个包装 ServletExtension
实例的 MultiBuildItem
。我可以从另一个模块的记录器返回一个 ServletExtension
,Undertow 将使用它并将其传递到启动 Undertow 的记录器方法中。
在运行时,字节码将按照生成的顺序被调用。这意味着构建步骤依赖项隐式地控制了生成的字节码的运行顺序。在上面的示例中,我们知道生成 ServletExtensionBuildItem
的字节码将在消费它的字节码之前运行。
以下对象可以传递给记录器
-
原始类型
-
String
-
Class<?> 对象
-
从先前的记录器调用返回的对象
-
具有无参构造函数和所有属性的 getter/setter(或公共字段)的对象
-
具有用
@RecordableConstructor
注解的构造函数且参数名称与字段名称匹配的对象 -
通过
io.quarkus.deployment.recording.RecorderContext#registerSubstitution(Class, Class, Class)
机制的任何任意对象 -
上述对象的数组、列表和映射
在某些要记录的对象的字段应被忽略的情况下(即,构建时存在的值不应反映在运行时),可以将 如果该类不能依赖于 Quarkus,则 Quarkus 可以使用任何自定义注解,只要该扩展实现了 此 SPI 也可以用于提供自定义注解,该注解将替代 |
2.6.2. 将配置注入记录器
具有阶段 RUNTIME
或 BUILD_AND_RUNTIME_FIXED
的配置对象可以通过构造函数注入注入到记录器中。只需创建一个构造函数,该构造函数采用记录器所需的配置对象。如果记录器有多个构造函数,则可以用 @Inject
注解您希望 Quarkus 使用的构造函数。如果记录器想要注入运行时配置,但也用于静态初始化时,那么它需要注入一个 RuntimeValue<ConfigObject>
,此值仅在调用运行时方法时设置。
2.6.3. RecorderContext
io.quarkus.deployment.recording.RecorderContext
提供了一些便捷方法来增强字节码记录,这包括注册没有无参构造函数的类的创建函数的能力,注册对象替换(基本上是从不可序列化对象到可序列化对象的转换器,反之亦然),以及创建类代理。此接口可以直接作为方法参数注入到任何 @Record
方法中。
使用给定的完全限定类名调用 classProxy
将创建一个 Class
实例,该实例可以传递到记录器方法中,并且在运行时将被替换为传递给 classProxy()
的类名。但是,在大多数用例中不需要此方法,因为在构建步骤中直接加载部署/应用程序类是安全的。因此,不建议使用此方法。但是,在某些用例中,此方法会派上用场,例如引用使用 GeneratedClassBuildItem
在先前构建步骤中生成的类。
2.6.3.1. 打印步骤执行时间
有时,知道应用程序运行时每个启动任务(这是每个字节码记录的结果)的确切时间很有用。确定此信息的最简单方法是使用 -Dquarkus.debug.print-startup-times=true
系统属性启动 Quarkus 应用程序。输出将如下所示
Build step LoggingResourceProcessor.setupLoggingRuntimeInit completed in: 42ms
Build step ConfigGenerationBuildStep.checkForBuildTimeConfigChange completed in: 4ms
Build step SyntheticBeansProcessor.initRuntime completed in: 0ms
Build step ConfigBuildStep.validateConfigProperties completed in: 1ms
Build step ResteasyStandaloneBuildStep.boot completed in: 95ms
Build step VertxHttpProcessor.initializeRouter completed in: 1ms
Build step VertxHttpProcessor.finalizeRouter completed in: 4ms
Build step LifecycleEventsBuildStep.startupEvent completed in: 1ms
Build step VertxHttpProcessor.openSocket completed in: 93ms
Build step ShutdownListenerBuildStep.setupShutdown completed in: 1ms
2.6.4. 使用 Gizmo
在某些情况下,可能需要对字节码进行更重要的操作。如果字节码记录不足,Gizmo 是 ASM 的便捷替代方案,具有更高级别的 API。
2.7. 生成资源
可以使用扩展生成资源,在某些情况下,您需要将资源生成到 META-INF
目录中,该资源可以是 SPI 服务或简单的 HTML、CSS、Javascript 文件。
/**
* This build step aggregates all the produced service providers
* and outputs them as resources.
*/
@BuildStep
public void produceServiceFiles(
BuildProducer<GeneratedStaticResourceBuildItem> generatedStaticResourceProducer,
BuildProducer<GeneratedResourceBuildItem> generatedResourceProducer
) throws IOException {
generatedResourceProducer.produce( (1)
new GeneratedResourceBuildItem(
"META-INF/services/io.quarkus.services.GreetingService",
"""
public class HelloService implements GreetingService {
@Override
public void do() {
System.out.println("Hello!");
}
}
""".getBytes(StandardCharsets.UTF_8)));
generatedStaticResourceProducer.produce( (2)
new GeneratedStaticResourceBuildItem(
"/index.js",
"console.log('Hello World!')".getBytes(StandardCharsets.UTF_8))
);
}
-
以资源形式在 META-INF/services 中生成 SPI 服务实现
-
生成 Vertx 提供的静态资源(例如,JavaScript 文件)
2.7.1. 关键点
-
GeneratedResourceBuildItem
-
生成持久保存在生产模式下的资源。
-
在开发和其他非生产模式下,资源保留在内存中,并使用
QuarkusClassLoader
加载。
-
-
GeneratedStaticResourceBuildItem
-
生成 Vertx 提供的静态资源(例如,JavaScript、HTML 或 CSS 等文件)。
-
在开发模式下,Quarkus 使用由基于类加载器的文件系统支持的 Vertx 处理程序来提供这些资源。
-
2.7.2. GeneratedResourceBuildItem
和 GeneratedStaticResourceBuildItem
之间的区别
虽然两者都用于生成资源,但它们的用途和行为有所不同
GeneratedResourceBuildItem
:
-
用于运行时需要的资源(例如,SPI 服务定义)。
-
仅在生产模式下持久保存;否则,存储在内存中。
GeneratedStaticResourceBuildItem
:
-
设计用于通过 HTTP 提供静态资源(例如,JavaScript 或 CSS 文件)。
-
在开发模式下,这些资源使用 Vertx 动态提供。
-
生成
GeneratedResourceBuildItem
。 -
仅在正常模式下生成
AdditionalStaticResourceBuildItem
。
通过适当地使用这些构建项目,您可以有效地在 Quarkus 扩展中生成和管理资源。
2.8. 上下文和依赖注入
CDI 集成指南提供了有关常见 CDI 相关用例的更多详细信息,以及解决方案的示例代码。
2.8.1. 扩展点
作为基于 CDI 的运行时,Quarkus 扩展通常使 CDI Bean 作为扩展行为的一部分可用。但是,Quarkus DI 解决方案不支持 CDI 可移植扩展。相反,Quarkus 扩展可以使用各种构建时扩展点。
2.9. Quarkus Dev UI
您可以使您的扩展支持 Quarkus Dev UI,以获得更好的开发者体验。
2.10. 扩展定义的端点
您的扩展可以添加额外的非应用程序端点,以便与 Health、Metrics、OpenAPI、Swagger UI 等的端点一起提供。
使用 NonApplicationRootPathBuildItem
定义端点
@BuildStep
RouteBuildItem myExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
return nonApplicationRootPathBuildItem.routeBuilder()
.route("custom-endpoint")
.handler(new MyCustomHandler())
.displayOnNotFoundPage()
.build();
}
请注意,上面的路径不以 '/' 开头,表示它是一个相对路径。上面的端点将相对于配置的非应用程序端点根目录提供。默认情况下,非应用程序端点根目录为 /q
,这意味着生成的端点将在 /q/custom-endpoint
找到。
绝对路径的处理方式不同。如果上面调用了 route("/custom-endpoint")
,则生成的端点将在 /custom-endpoint
找到。
如果扩展需要嵌套的非应用程序端点
@BuildStep
RouteBuildItem myNestedExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
return nonApplicationRootPathBuildItem.routeBuilder()
.nestedRoute("custom-endpoint", "deep")
.handler(new MyCustomHandler())
.displayOnNotFoundPage()
.build();
}
给定默认的非应用程序端点根目录 /q
,这将创建一个位于 /q/custom-endpoint/deep
的端点。
绝对路径也对嵌套端点产生影响。如果上面调用了 nestedRoute("custom-endpoint", "/deep")
,则生成的端点将在 /deep
找到。
有关如何配置非应用程序根路径的详细信息,请参阅 Quarkus Vertx HTTP 配置参考。
2.11. 扩展健康检查
健康检查通过 quarkus-smallrye-health
扩展提供。它提供活性和就绪性检查功能。
编写扩展时,最好为扩展提供健康检查,这些健康检查可以自动包含,而无需开发人员编写自己的健康检查。
为了提供健康检查,您应该执行以下操作
-
将
quarkus-smallrye-health
扩展作为可选依赖项导入到运行时模块中,这样如果未包含健康检查,则不会影响应用程序的大小。 -
按照 SmallRye Health 指南创建您的健康检查。我们建议仅为扩展提供就绪性检查(活性检查旨在表达应用程序已启动并且需要轻量级)。
-
在您的部署模块中导入
quarkus-smallrye-health-spi
库。 -
在您的部署模块中添加一个生成
HealthBuildItem
的构建步骤。 -
添加一种通过配置项
quarkus.<extension>.health.enabled
禁用扩展健康检查的方法,该配置项默认应启用。
以下是 Agroal 扩展的示例,该扩展提供了 DataSourceHealthCheck
来验证数据源的就绪性。
@BuildStep
HealthBuildItem addHealthCheck(AgroalBuildTimeConfig agroalBuildTimeConfig) {
return new HealthBuildItem("io.quarkus.agroal.runtime.health.DataSourceHealthCheck",
agroalBuildTimeConfig.healthEnabled);
}
2.12. 扩展指标
quarkus-micrometer
扩展和 quarkus-smallrye-metrics
扩展提供了对收集指标的支持。作为兼容性说明,quarkus-micrometer
扩展将 MP Metrics API 适配到 Micrometer 库原始类型,因此可以在不破坏依赖于 MP Metrics API 的代码的情况下启用 quarkus-micrometer
扩展。请注意,Micrometer 发出的指标是不同的,有关更多信息,请参阅 quarkus-micrometer
扩展文档。
MP Metrics API 的兼容性层将在未来迁移到不同的扩展中。 |
扩展可以使用两种广泛的模式与可选的指标扩展交互,以添加他们自己的指标
-
消费者模式:扩展声明一个
MetricsFactoryConsumerBuildItem
,并使用它来向指标扩展提供字节码记录器。当指标扩展初始化后,它将迭代注册的消费者以使用MetricsFactory
初始化它们。此工厂可用于声明与 API 无关的指标,这非常适合于为收集统计信息提供可检测对象的扩展(例如,Hibernate 的Statistics
类)。 -
绑定器模式:扩展可以选择根据指标系统使用完全不同的收集实现。
Optional<MetricsCapabilityBuildItem> metricsCapability
构建步骤参数可用于声明或以其他方式初始化基于活动指标扩展的 API 特定指标(例如,“smallrye-metrics”或“micrometer”)。可以通过使用MetricsFactory::metricsSystemSupported()
来测试记录器中的活动指标扩展来组合此模式和消费者模式。
请记住,对指标的支持是可选的。扩展可以在其构建步骤中使用 Optional<MetricsCapabilityBuildItem> metricsCapability
参数来测试已启用的指标扩展的存在。考虑使用其他配置来控制指标的行为。例如,数据源指标可能很昂贵,因此使用其他配置标志来在单个数据源上启用指标收集。
为您的扩展添加指标时,您可能会发现自己处于以下情况之一
-
扩展使用的底层库直接使用特定的 Metrics API(MP Metrics、Micrometer 或其他)。
-
底层库使用其自己的机制来收集指标,并使用其自己的 API 在运行时使它们可用,例如 Hibernate 的
Statistics
类或 Vert.xMetricsOptions
。 -
底层库不提供指标(或者根本没有库),并且您想要添加检测。
2.12.1. 情况 1:库直接使用指标库
如果库直接使用指标 API,则有两个选项
-
使用
Optional<MetricsCapabilityBuildItem> metricsCapability
参数来测试在您的构建步骤中支持哪个指标 API(例如,“smallrye-metrics”或“micrometer”),并使用它来选择性地声明或初始化 API 特定的 Bean 或构建项目。 -
创建一个单独的构建步骤,该步骤使用
MetricsFactory
,并在字节码记录器中使用MetricsFactory::metricsSystemSupported()
方法来初始化所需资源(如果支持所需的指标 API,例如 "smallrye-metrics" 或 "micrometer")。
如果不存在活动的指标扩展,或者扩展不支持库所需的 API,则扩展可能需要提供回退方案。
2.12.2. 案例 2:库提供自己的指标 API
有两个库提供自己的指标 API 的示例
-
扩展将可检测对象定义为 Agroal 对
io.agroal.api.AgroalDataSourceMetrics
所做的那样,或者 -
扩展提供自己的指标抽象,如 Jaeger 对
io.jaegertracing.spi.MetricsFactory
所做的那样。
2.12.2.1. 观察可检测对象
让我们首先来看可检测对象 (io.agroal.api.AgroalDataSourceMetrics
) 的情况。在这种情况下,您可以执行以下操作
-
定义一个
BuildStep
,它生成一个MetricsFactoryConsumerBuildItem
,该项目使用RUNTIME_INIT
或STATIC_INIT
记录器来定义MetricsFactory
使用者。例如,以下代码创建MetricsFactoryConsumerBuildItem
,当且仅当 Agroal 通常以及特定于数据源启用了指标时@BuildStep @Record(ExecutionTime.RUNTIME_INIT) void registerMetrics(AgroalMetricsRecorder recorder, DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, BuildProducer<MetricsFactoryConsumerBuildItem> datasourceMetrics, List<AggregatedDataSourceBuildTimeConfigBuildItem> aggregatedDataSourceBuildTimeConfigs) { for (AggregatedDataSourceBuildTimeConfigBuildItem aggregatedDataSourceBuildTimeConfig : aggregatedDataSourceBuildTimeConfigs) { // Create a MetricsFactory consumer to register metrics for a data source // IFF metrics are enabled globally and for the data source // (they are enabled for each data source by default if they are also enabled globally) if (dataSourcesBuildTimeConfig.metricsEnabled && aggregatedDataSourceBuildTimeConfig.getJdbcConfig().enableMetrics.orElse(true)) { datasourceMetrics.produce(new MetricsFactoryConsumerBuildItem( recorder.registerDataSourceMetrics(aggregatedDataSourceBuildTimeConfig.getName()))); } } }
-
关联的记录器应使用提供的
MetricsFactory
来注册指标。对于 Agroal,这意味着使用MetricFactory
API 来观察io.agroal.api.AgroalDataSourceMetrics
方法。例如/* RUNTIME_INIT */ public Consumer<MetricsFactory> registerDataSourceMetrics(String dataSourceName) { return new Consumer<MetricsFactory>() { @Override public void accept(MetricsFactory metricsFactory) { String tagValue = DataSourceUtil.isDefault(dataSourceName) ? "default" : dataSourceName; AgroalDataSourceMetrics metrics = getDataSource(dataSourceName).getMetrics(); // When using MP Metrics, the builder uses the VENDOR registry by default. metricsFactory.builder("agroal.active.count") .description( "Number of active connections. These connections are in use and not available to be acquired.") .tag("datasource", tagValue) .buildGauge(metrics::activeCount); ....
MetricsFactory
提供了一个流畅的构建器来注册指标,最后一步是基于 Supplier
或 ToDoubleFunction
构建量具或计数器。计时器可以包装 Callable
、Runnable
或 Supplier
实现,也可以使用 TimeRecorder
来累积时间块。底层指标扩展将创建适当的工件来观察或测量定义的函数。
2.12.2.2. 使用特定于 Metrics API 的实现
在某些情况下,可能首选使用特定于 metrics-API 的实现。例如,Jaeger 定义了自己的指标接口 io.jaegertracing.spi.MetricsFactory
,它使用该接口来定义计数器和量具。从该接口到指标系统的直接映射将是最有效的。在这种情况下,重要的是隔离这些专门的实现,并避免急切的类加载,以确保指标 API 仍然是一个可选的编译时依赖项。
Optional<MetricsCapabilityBuildItem> metricsCapability
可以在构建步骤中使用,以有选择地控制 Bean 的初始化或其他构建项目的生成。例如,Jaeger 扩展可以使用以下方法来控制专用 Metrics API 适配器的初始化
+
/* RUNTIME_INIT */
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void setupTracer(JaegerDeploymentRecorder jdr, JaegerBuildTimeConfig buildTimeConfig, JaegerConfig jaeger,
ApplicationConfig appConfig, Optional<MetricsCapabilityBuildItem> metricsCapability) {
// Indicates that this extension would like the SSL support to be enabled
extensionSslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.JAEGER.getName()));
if (buildTimeConfig.enabled) {
// To avoid dependency creep, use two separate recorder methods for the two metrics systems
if (buildTimeConfig.metricsEnabled && metricsCapability.isPresent()) {
if (metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)) {
jdr.registerTracerWithMicrometerMetrics(jaeger, appConfig);
} else {
jdr.registerTracerWithMpMetrics(jaeger, appConfig);
}
} else {
jdr.registerTracerWithoutMetrics(jaeger, appConfig);
}
}
}
使用 MetricsFactory
的记录器可以使用 MetricsFactory::metricsSystemSupported()
以类似的方式控制字节码记录期间指标对象的初始化。
2.12.3. 案例 3:需要在扩展代码中收集指标
要从头开始定义自己的指标,您有两种基本选择:使用通用的 MetricFactory
构建器,或者遵循绑定器模式,并创建特定于启用的指标扩展的检测。
要使用与扩展无关的 MetricFactory
API,您的处理器可以定义一个 BuildStep
,该步骤生成一个 MetricsFactoryConsumerBuildItem
,该项目使用 RUNTIME_INIT
或 STATIC_INIT
记录器来定义 MetricsFactory
使用者。
+
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
MetricsFactoryConsumerBuildItem registerMetrics(MyExtensionRecorder recorder) {
return new MetricsFactoryConsumerBuildItem(recorder.registerMetrics());
}
+ - 关联的记录器应使用提供的 MetricsFactory
来注册指标,例如
+
final LongAdder extensionCounter = new LongAdder();
/* RUNTIME_INIT */
public Consumer<MetricsFactory> registerMetrics() {
return new Consumer<MetricsFactory>() {
@Override
public void accept(MetricsFactory metricsFactory) {
metricsFactory.builder("my.extension.counter")
.buildGauge(extensionCounter::longValue);
....
请记住,指标扩展是可选的。保持与指标相关的初始化与扩展的其他设置隔离,并构造您的代码以避免急切导入指标 API。收集指标也可能很昂贵。如果指标支持的存在/缺失不足,请考虑使用其他特定于扩展的配置来控制指标的行为。
2.13. 自定义扩展中的 JSON 处理
扩展通常需要为扩展提供的类型注册序列化器和/或反序列化器。
为此,Jackson 和 JSON-B 扩展都提供了一种从扩展部署模块中注册序列化器/反序列化器的方法。
请记住,并非每个人都需要 JSON,因此您需要使其成为可选的。
如果扩展打算提供 JSON 相关的自定义,强烈建议为 Jackson 和 JSON-B 提供自定义。
2.13.1. 自定义 Jackson
首先,在扩展的运行时模块上添加对 quarkus-jackson
的可选依赖。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
<optional>true</optional>
</dependency>
然后为 Jackson 创建一个序列化器或反序列化器(或两者),可以在 mongodb-panache
扩展中看到一个示例。
public class ObjectIdSerializer extends StdSerializer<ObjectId> {
public ObjectIdSerializer() {
super(ObjectId.class);
}
@Override
public void serialize(ObjectId objectId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (objectId != null) {
jsonGenerator.writeString(objectId.toString());
}
}
}
在扩展的部署模块上添加对 quarkus-jackson-spi
的依赖。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson-spi</artifactId>
</dependency>
向您的处理器添加一个构建步骤,以通过 JacksonModuleBuildItem
注册 Jackson 模块。您需要以一种跨所有 Jackson 模块的唯一方式命名您的模块。
@BuildStep
JacksonModuleBuildItem registerJacksonSerDeser() {
return new JacksonModuleBuildItem.Builder("ObjectIdModule")
.add(io.quarkus.mongodb.panache.jackson.ObjectIdSerializer.class.getName(),
io.quarkus.mongodb.panache.jackson.ObjectIdDeserializer.class.getName(),
ObjectId.class.getName())
.build();
}
然后,Jackson 扩展将使用生成的构建项自动注册 Jackson 中的模块。
如果您需要比注册模块更多的自定义功能,您可以通过 AdditionalBeanBuildItem
生成一个实现 io.quarkus.jackson.ObjectMapperCustomizer
的 CDI Bean。有关自定义 Jackson 的更多信息,请参见 JSON 指南 配置 JSON 支持
2.13.2. 自定义 JSON-B
首先,在扩展的运行时模块上添加对 quarkus-jsonb
的可选依赖。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb</artifactId>
<optional>true</optional>
</dependency>
然后为 JSON-B 创建一个序列化器和/或反序列化器,可以在 mongodb-panache
扩展中看到一个示例。
public class ObjectIdSerializer implements JsonbSerializer<ObjectId> {
@Override
public void serialize(ObjectId obj, JsonGenerator generator, SerializationContext ctx) {
if (obj != null) {
generator.write(obj.toString());
}
}
}
在扩展的部署模块上添加对 quarkus-jsonb-spi
的依赖。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb-spi</artifactId>
</dependency>
向您的处理器添加一个构建步骤,以通过 JsonbSerializerBuildItem
注册序列化器。
@BuildStep
JsonbSerializerBuildItem registerJsonbSerializer() {
return new JsonbSerializerBuildItem(io.quarkus.mongodb.panache.jsonb.ObjectIdSerializer.class.getName()));
}
然后,JSON-B 扩展将使用生成的构建项自动注册您的序列化器/反序列化器。
如果您需要比注册序列化器或反序列化器更多的自定义功能,您可以通过 AdditionalBeanBuildItem
生成一个实现 io.quarkus.jsonb.JsonbConfigCustomizer
的 CDI Bean。有关自定义 JSON-B 的更多信息,请参见 JSON 指南 配置 JSON 支持
2.14. 与开发模式集成
有多种 API 可用于与开发模式集成,并获取有关当前状态的信息。
2.14.1. 处理重启
当 Quarkus 启动时,保证存在 io.quarkus.deployment.builditem.LiveReloadBuildItem
,它提供有关此启动的信息,特别是
-
这是一个干净的启动还是实时重新加载
-
如果是实时重新加载,哪些更改的文件/类触发了重新加载
它还提供了一个全局上下文映射,您可以使用它来存储重启之间的信息,而无需使用静态字段。
以下是一个跨实时重新加载持久化上下文的构建步骤的示例
@BuildStep(onlyIf = {IsDevelopment.class})
public void keyPairDevService(LiveReloadBuildItem liveReloadBuildItem, BuildProducer<KeyPairBuildItem> keyPairs) {
KeyPairContext ctx = liveReloadBuildItem.getContextObject(KeyPairContext.class); (1)
if (ctx == null && !liveReloadBuildItem.isLiveReload()) { (2)
KeyPair keyPair = generateKeyPair(2048);
Map<String, String> properties = generateDevServiceProperties(keyPair);
liveReloadBuildItem.setContextObject( (3)
KeyPairContext.class, new KeyPairContext(properties));
keyPairs.produce(new KeyPairBuildItem(properties));
}
if (ctx != null) {
Map<String, String> properties = ctx.getProperties();
keyPairs.produce(new KeyPairBuildItem(properties));
}
}
static record KeyPairContext(Map<String, String> properties) {}
1 | 您可以从 LiveReloadBuildItem 检索上下文。如果指定类型没有上下文,则此调用返回 null ;否则,它返回来自先前实时重新加载执行的存储实例。 |
2 | 您可以检查这是否是第一次执行(不是实时重新加载)。 |
3 | LiveReloadBuildItem#setContextObject 方法允许您在实时重新加载之间设置上下文。 |
2.14.2. 触发实时重新加载
实时重新加载通常由 HTTP 请求触发,但是并非所有应用程序都是 HTTP 应用程序,某些扩展可能希望基于其他事件触发实时重新加载。为此,您需要在运行时模块中实现 io.quarkus.dev.spi.HotReplacementSetup
,并添加一个列出您的实现的 META-INF/services/io.quarkus.dev.spi.HotReplacementSetup
。
在启动时,将调用 setupHotDeployment
方法,您可以使用提供的 io.quarkus.dev.spi.HotReplacementContext
来启动对更改文件的扫描。
2.14.3. Dev Services
当扩展使用外部服务时,添加 Dev Service 可以改善开发和测试模式下的用户体验。有关更多详细信息,请参见 如何编写 Dev Service。
2.15. 测试扩展
Quarkus 扩展的测试应使用 io.quarkus.test.QuarkusUnitTest
JUnit 5 扩展完成。此扩展允许 Arquillian 风格的测试,这些测试会测试特定功能。它不适用于测试用户应用程序,因为这应通过 io.quarkus.test.junit.QuarkusTest
完成。主要区别在于 QuarkusTest
只是在运行开始时启动应用程序一次,而 QuarkusUnitTest
为每个测试类部署自定义 Quarkus 应用程序。
这些测试应放置在部署模块中,如果测试需要其他 Quarkus 模块,则还应将它们的部署模块添加为测试范围的依赖项。
请注意,QuarkusUnitTest
位于 quarkus-junit5-internal
模块中。
一个示例测试类可能如下所示
package io.quarkus.health.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import org.eclipse.microprofile.health.Liveness;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import io.quarkus.test.QuarkusUnitTest;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import io.restassured.RestAssured;
public class FailingUnitTest {
@RegisterExtension (1)
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() ->
ShrinkWrap.create(JavaArchive.class) (2)
.addClasses(FailingHealthCheck.class)
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")
);
@Inject (3)
@Liveness
Instance<HealthCheck> checks;
@Test
public void testHealthServlet() {
RestAssured.when().get("/q/health").then().statusCode(503); (4)
}
@Test
public void testHealthBeans() {
List<HealthCheck> check = new ArrayList<>(); (5)
for (HealthCheck i : checks) {
check.add(i);
}
assertEquals(1, check.size());
assertEquals(HealthCheckResponse.State.DOWN, check.get(0).call().getState());
}
}
1 | QuarkusUnitTest 扩展必须与静态字段一起使用。如果与非静态字段一起使用,则不会启动测试应用程序。 |
2 | 此生产者用于构建要测试的应用程序。它使用 Shrinkwrap 创建一个 JavaArchive 来测试 |
3 | 可以将来自我们的测试部署的 Bean 直接注入到测试用例中 |
4 | 此方法直接调用运行状况检查 Servlet 并验证响应 |
5 | 此方法使用注入的运行状况检查 Bean 来验证它是否返回了预期的结果 |
如果要测试扩展在构建时是否正确失败,请使用 setExpectedException
方法
package io.quarkus.hibernate.orm;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.test.QuarkusUnitTest;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
public class PersistenceAndQuarkusConfigTest {
@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.setExpectedException(ConfigurationException.class) (1)
.withApplicationRoot((jar) -> jar
.addAsManifestResource("META-INF/some-persistence.xml", "persistence.xml")
.addAsResource("application.properties"));
@Test
public void testPersistenceAndConfigTest() {
// should not be called, deployment exception should happen first:
// it's illegal to have Hibernate configuration properties in both the
// application.properties and in the persistence.xml
Assertions.fail();
}
}
1 | 这告诉 JUnit Quarkus 部署应该失败并出现特定异常 |
2.16. 测试热重载
还可以编写测试来验证扩展在开发模式下是否正常工作,并且可以正确处理更新。
对于大多数扩展,这都可以“开箱即用”,但是,进行冒烟测试以验证此功能是否按预期工作仍然是一个好主意。要测试这一点,我们使用 QuarkusDevModeTest
public class ServletChangeTestCase {
@RegisterExtension
final static QuarkusDevModeTest test = new QuarkusDevModeTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class) (1)
.addClass(DevServlet.class)
.addAsManifestResource(new StringAsset("Hello Resource"), "resources/file.txt");
}
});
@Test
public void testServletChange() throws InterruptedException {
RestAssured.when().get("/dev").then()
.statusCode(200)
.body(is("Hello World"));
test.modifySourceFile("DevServlet.java", new Function<String, String>() { (2)
@Override
public String apply(String s) {
return s.replace("Hello World", "Hello Quarkus");
}
});
RestAssured.when().get("/dev").then()
.statusCode(200)
.body(is("Hello Quarkus"));
}
@Test
public void testAddServlet() throws InterruptedException {
RestAssured.when().get("/new").then()
.statusCode(404);
test.addSourceFile(NewServlet.class); (3)
RestAssured.when().get("/new").then()
.statusCode(200)
.body(is("A new Servlet"));
}
@Test
public void testResourceChange() throws InterruptedException {
RestAssured.when().get("/file.txt").then()
.statusCode(200)
.body(is("Hello Resource"));
test.modifyResourceFile("META-INF/resources/file.txt", new Function<String, String>() { (4)
@Override
public String apply(String s) {
return "A new resource";
}
});
RestAssured.when().get("file.txt").then()
.statusCode(200)
.body(is("A new resource"));
}
@Test
public void testAddResource() throws InterruptedException {
RestAssured.when().get("/new.txt").then()
.statusCode(404);
test.addResourceFile("META-INF/resources/new.txt", "New File"); (5)
RestAssured.when().get("/new.txt").then()
.statusCode(200)
.body(is("New File"));
}
}
1 | 这将启动部署,您的测试可以将其修改为测试套件的一部分。Quarkus 将在每个测试方法之间重新启动,因此每个方法都以干净的部署开始。 |
2 | 此方法允许您修改类文件的源。旧源被传递到函数中,并返回更新的源。 |
3 | 此方法将新的类文件添加到部署中。使用的源将是当前项目中使用的原始源。 |
4 | 此方法修改静态资源 |
5 | 此方法添加新的静态资源 |
2.17. Native Executable 支持
Quarkus 提供了许多构建项,用于控制本机可执行文件构建的各个方面。这允许扩展以编程方式执行任务,例如注册类以进行反射或将静态资源添加到本机可执行文件中。下面列出了其中一些构建项
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem
-
将静态资源包含到本机可执行文件中。
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem
-
将目录的静态资源包含到本机可执行文件中。
io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem
-
一个类,它将在运行时由 Substrate 重新初始化。这将导致静态初始化程序运行两次。
io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem
-
将在本机可执行文件构建时设置的系统属性。
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem
-
在本机可执行文件中包含资源束。
io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem
-
在 Substrate 中注册一个类以进行反射。构造函数始终注册,而方法和字段是可选的。
io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem
-
一个类,它将在运行时而不是构建时初始化。如果该类在本机可执行文件构建过程中初始化,这将导致构建失败,因此必须小心。
io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem
-
一个方便的功能,允许您从单个构建项控制上述大多数功能。
io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem
-
指示应在本机映像中启用所有字符集。
io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem
-
一种告诉 Quarkus 扩展需要 SSL 的便捷方法,并且应在本机映像构建期间启用它。使用此功能时,请记住将您的扩展添加到 本机和 SSL 指南上自动提供 SSL 支持的扩展列表中。
2.18. IDE 支持提示
2.18.1. 在 Eclipse 中编写 Quarkus 扩展
在 Eclipse 中编写 Quarkus 扩展的唯一特殊方面是,APT(注释处理工具)是扩展构建的一部分所必需的,这意味着您需要
-
从 https://marketplace.eclipse.org/content/m2e-apt 安装
m2e-apt
-
在您的
pom.xml
中定义此属性:<m2e.apt.activation>jdt_apt</m2e.apt.activation>
,尽管如果您依赖io.quarkus:quarkus-build-parent
,您将免费获得它。 -
如果您在 IDE 中同时打开了
io.quarkus:quarkus-extension-processor
项目(例如,如果您检出了 Quarkus 源代码并在 IDE 中打开),则需要关闭该项目。否则,Eclipse 将不会调用它包含的 APT 插件。 -
如果您刚刚关闭了扩展处理器项目,请务必对其他项目执行
Maven > Update Project
,以便 Eclipse 从 Maven 存储库中获取扩展处理器。
2.19. 故障排除/调试提示
2.19.1. 检查生成的/转换的类
Quarkus 在构建阶段生成大量类,并且在许多情况下还会转换现有类。在扩展开发过程中,查看生成的字节码和转换后的类通常非常有用。
如果将 quarkus.package.jar.decompiler.enabled
属性设置为 true
,则 Quarkus 将下载并调用 Vineflower 反编译器并将结果转储到构建工具输出的 decompiled
目录中(例如,Maven 的 target/decompiled
)。可以使用 quarkus.package.jar.decompiler.output-dir
更改输出目录。
此属性仅在正常的生产构建期间(即,不适用于开发模式/测试)以及使用 fast-jar 打包类型(默认行为)时有效。 |
还有三个系统属性允许您将生成的/转换的类转储到文件系统并稍后检查它们,例如通过 IDE 中的反编译器。
-
quarkus.debug.generated-classes-dir
- 用于转储生成的类,例如 Bean 元数据 -
quarkus.debug.transformed-classes-dir
- 用于转储转换后的类,例如 Panache 实体 -
quarkus.debug.generated-sources-dir
- 用于转储 ZIG 文件;ZIG 文件是在堆栈跟踪中引用的生成代码的文本表示形式
这些属性在开发模式或运行测试时特别有用,因为生成的/转换的类仅保存在类加载器中的内存中。
例如,您可以指定 quarkus.debug.generated-classes-dir
系统属性,以便将这些类写入磁盘以供在开发模式下进行检查
./mvnw quarkus:dev -Dquarkus.debug.generated-classes-dir=dump-classes
属性值可以是绝对路径,例如 Linux 机器上的 /home/foo/dump ,也可以是相对于用户工作目录的路径,即 dump 对应于开发模式下的 {user.dir}/target/dump 和运行测试时的 {user.dir}/dump 。 |
您应该在日志中看到一行,对应于写入目录的每个类
INFO [io.qua.run.boo.StartupActionImpl] (main) Wrote /path/to/my/app/target/dump-classes/io/quarkus/arc/impl/ActivateRequestContextInterceptor_Bean.class
运行测试时也会遵守该属性
./mvnw clean test -Dquarkus.debug.generated-classes-dir=target/dump-generated-classes
类似地,您可以使用 quarkus.debug.transformed-classes-dir
和 quarkus.debug.generated-sources-dir
属性来转储相关的输出。
2.19.2. 检查 QuarkusUnitTest
中生成的/转换的类
当 使用 QuarkusUnitTest
时,作为 手动设置 quarkus.debug.*-dir
的替代方法,您可以简单地调用 QuarkusUnitTest#debugBytecode
public class MyTest {
@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClass(MyEntity.class))
.debugBytecode(true);
// ... test methods go here ...
}
这将自动设置这些配置属性,以便将类/源转储到 target/debug
,仅适用于该测试类,并且位于每个测试执行唯一的子目录中。有关详细信息,请参见 QuarkusUnitTest#debugBytecode
的 javadoc。
这对于调试仅在 CI 环境中发生的不稳定的测试非常方便,特别是;例如,https://github.com/quarkusio/quarkus/ 上的 GitHub Actions CI 已设置,以便在每次 CI 运行后,这些 target/debug
目录被收集到可供下载的构建工件中。
2.19.3. 仅为特定测试启用跟踪日志
当 使用 QuarkusUnitTest
时,如果需要为特定的测试类启用跟踪日志,您可以简单地调用 QuarkusUnitTest#traceCategories
并在参数中传递日志记录类别
public class MyTest {
@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClass(MyEntity.class))
.traceCategories("org.hibernate", "io.quarkus.hibernate", "io.quarkus.panache");
// ... test methods go here ...
}
有关详细信息,请参见 QuarkusUnitTest#traceCategories
的 javadoc。
这对于调试仅在 CI 环境中发生的不稳定的测试非常方便,特别是,因为这只会增加启用该选项的特定测试中的日志详细程度。
2.20. 示例测试扩展
我们有一个扩展,用于测试扩展处理中的回归。它位于 https://github.com/quarkusio/quarkus/tree/main/integration-tests/test-extension/extension 目录中。在本节中,我们将介绍扩展作者通常需要执行的一些任务,使用 test-extension 代码来说明如何完成该任务。
2.20.1. 功能和能力
2.20.1.1. 功能
功能 表示扩展提供的功能。该功能的名称在应用程序引导期间显示在日志中。
2019-03-22 14:02:37,884 INFO [io.quarkus] (main) Quarkus 999-SNAPSHOT started in 0.061s.
2019-03-22 14:02:37,884 INFO [io.quarkus] (main) Installed features: [cdi, test-extension] (1)
1 | 运行时映像中安装的功能列表 |
可以在 构建步骤处理器方法中注册功能,该方法生成 FeatureBuildItem
@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem("test-extension");
}
功能的名称应仅包含小写字符,单词之间用破折号分隔;例如 security-jpa
。一个扩展最多应提供一个功能,并且名称必须是唯一的。如果多个扩展注册了具有相同名称的功能,则构建将失败。
功能名称还应映射到扩展的 devtools/common/src/main/filtered/extensions.json
条目中的标签,以便启动行显示的功能名称与在创建项目时可用于选择扩展的标签相匹配,该项目使用 Quarkus Maven 插件,如从 编写 JSON REST 服务 指南中获取的此示例所示,其中引用了 rest-jackson
功能
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=rest-json \
-DclassName="org.acme.rest.json.FruitResource" \
-Dpath="/fruits" \
-Dextensions="rest,rest-jackson"
cd rest-json
2.20.1.2. 能力
能力 表示可由其他扩展查询的技术能力。一个扩展可以提供多个能力,并且多个扩展可以提供相同的能力。默认情况下,能力不会显示给用户。在检查扩展是否存在时,应使用能力,而不是基于类路径的检查。
可以在 构建步骤处理器方法中注册能力,该方法生成 CapabilityBuildItem
@BuildStep
void capabilities(BuildProducer<CapabilityBuildItem> capabilityProducer) {
capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-transactions"));
capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-metrics"));
}
扩展可以使用 Capabilities
构建项来使用注册的能力
@BuildStep
void doSomeCoolStuff(Capabilities capabilities) {
if (capabilities.isPresent(Capability.TRANSACTIONS)) {
// do something only if JTA transactions are in...
}
}
能力应遵循 Java 包的命名约定;例如 io.quarkus.security.jpa
。核心扩展提供的能力应列在 io.quarkus.deployment.Capability
枚举中,并且它们的名称应始终以 io.quarkus
前缀开头。
2.20.2. Bean 定义注释
CDI 层处理显式注册的 CDI Bean,或者它基于 2.5.1. Bean 定义注释中定义的 Bean 定义注释发现的 CDI Bean。您可以扩展此注释集,以包括您的扩展使用 BeanDefiningAnnotationBuildItem
处理的注释,如 TestProcessor#registerBeanDefinningAnnotations
示例所示
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.jandex.DotName;
import io.quarkus.extest.runtime.TestAnnotation;
public final class TestProcessor {
static DotName TEST_ANNOTATION = DotName.createSimple(TestAnnotation.class.getName());
static DotName TEST_ANNOTATION_SCOPE = DotName.createSimple(ApplicationScoped.class.getName());
...
@BuildStep
BeanDefiningAnnotationBuildItem registerX() {
(1)
return new BeanDefiningAnnotationBuildItem(TEST_ANNOTATION, TEST_ANNOTATION_SCOPE);
}
...
}
/**
* Marker annotation for test configuration target beans
*/
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
@Inherited
public @interface TestAnnotation {
}
/**
* A sample bean
*/
@TestAnnotation (2)
public class ConfiguredBean implements IConfigConsumer {
...
1 | 使用 Jandex DotName 类注册注释类和 CDI 默认范围。 |
2 | CDI 层将处理 ConfiguredBean ,就像使用 CDI 标准 @ApplicationScoped 注释的 Bean 一样。 |
2.20.3. 将配置解析为对象
扩展可能执行的主要任务之一是将行为的配置阶段与运行时阶段完全分离。框架通常在启动时执行配置的解析/加载,这可以在构建时完成,既可以减少对 XML 解析器等框架的运行时依赖性,又可以减少解析产生的启动时间。
TestProcessor#parseServiceXmlConfig
方法中显示了一个使用 JAXB 解析 XML 配置文件的示例
@BuildStep
@Record(STATIC_INIT)
RuntimeServiceBuildItem parseServiceXmlConfig(TestRecorder recorder) throws JAXBException {
RuntimeServiceBuildItem serviceBuildItem = null;
JAXBContext context = JAXBContext.newInstance(XmlConfig.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
InputStream is = getClass().getResourceAsStream("/config.xml"); (1)
if (is != null) {
log.info("Have XmlConfig, loading");
XmlConfig config = (XmlConfig) unmarshaller.unmarshal(is); (2)
...
}
return serviceBuildItem;
}
1 | 查找 config.xml 类路径资源 |
2 | 如果找到,则使用 XmlConfig.class 的 JAXB 上下文进行解析 |
如果在构建环境中没有可用的 /config.xml 资源,则将返回一个 null |
通常,人们正在加载配置以创建一些运行时组件/服务,正如 parseServiceXmlConfig
正在做的那样。我们将在以下 管理非 CDI 服务 部分中回到 parseServiceXmlConfig
中的其余行为。
如果由于某种原因,您需要在扩展处理器中的其他构建步骤中解析配置并使用它,则需要创建一个 XmlConfigBuildItem
以传递已解析的 XmlConfig 实例。
如果您查看 XmlConfig 代码,您将看到它确实随身携带 JAXB 注释。如果您不希望这些出现在运行时映像中,您可以将 XmlConfig 实例克隆到一些 POJO 对象图中,然后用 POJO 类替换 XmlConfig。我们将在 替换 Native Image 中的类中这样做。 |
2.20.4. 使用 Jandex 扫描部署
如果您的扩展定义了标记需要处理的 Bean 的注释或接口,则可以使用 Jandex API(Java 注释索引器和离线反射库)来定位这些 Bean。以下 TestProcessor#scanForBeans
方法显示了如何查找使用我们的 @TestAnnotation
注释并实现 IConfigConsumer
接口的 Bean
static DotName TEST_ANNOTATION = DotName.createSimple(TestAnnotation.class.getName());
...
@BuildStep
@Record(STATIC_INIT)
void scanForBeans(TestRecorder recorder, BeanArchiveIndexBuildItem beanArchiveIndex, (1)
BuildProducer<TestBeanBuildItem> testBeanProducer) {
IndexView indexView = beanArchiveIndex.getIndex(); (2)
Collection<AnnotationInstance> testBeans = indexView.getAnnotations(TEST_ANNOTATION); (3)
for (AnnotationInstance ann : testBeans) {
ClassInfo beanClassInfo = ann.target().asClass();
try {
boolean isConfigConsumer = beanClassInfo.interfaceNames()
.stream()
.anyMatch(dotName -> dotName.equals(DotName.createSimple(IConfigConsumer.class.getName()))); (4)
if (isConfigConsumer) {
Class<IConfigConsumer> beanClass = (Class<IConfigConsumer>) Class.forName(beanClassInfo.name().toString(), false, Thread.currentThread().getContextClassLoader());
testBeanProducer.produce(new TestBeanBuildItem(beanClass)); (5)
log.infof("Configured bean: %s", beanClass);
}
} catch (ClassNotFoundException e) {
log.warn("Failed to load bean class", e);
}
}
}
1 | 依赖于 BeanArchiveIndexBuildItem ,以便在部署被索引后运行构建步骤。 |
2 | 检索索引。 |
3 | 查找使用 @TestAnnotation 注释的所有 Bean。 |
4 | 确定哪些 Bean 也具有 IConfigConsumer 接口。 |
5 | 将 Bean 类保存在 TestBeanBuildItem 中,以便在稍后的 RUNTIME_INIT 构建步骤中使用,该步骤将与 Bean 实例交互。 |
2.20.5. 与扩展 Bean 交互
您可以使用 io.quarkus.arc.runtime.BeanContainer
接口与您的扩展 Bean 交互。以下 configureBeans
方法说明了如何与在上一个部分中扫描的 Bean 交互
// TestProcessor#configureBeans
@BuildStep
@Record(RUNTIME_INIT)
void configureBeans(TestRecorder recorder, List<TestBeanBuildItem> testBeans, (1)
BeanContainerBuildItem beanContainer, (2)
TestRunTimeConfig runTimeConfig) {
for (TestBeanBuildItem testBeanBuildItem : testBeans) {
Class<IConfigConsumer> beanClass = testBeanBuildItem.getConfigConsumer();
recorder.configureBeans(beanContainer.getValue(), beanClass, buildAndRunTimeConfig, runTimeConfig); (3)
}
}
// TestRecorder#configureBeans
public void configureBeans(BeanContainer beanContainer, Class<IConfigConsumer> beanClass,
TestBuildAndRunTimeConfig buildTimeConfig,
TestRunTimeConfig runTimeConfig) {
log.info("Begin BeanContainerListener callback\n");
IConfigConsumer instance = beanContainer.beanInstance(beanClass); (4)
instance.loadConfig(buildTimeConfig, runTimeConfig); (5)
log.infof("configureBeans, instance=%s\n", instance);
}
1 | 使用从扫描构建步骤生成的 `TestBeanBuildItem`。 |
2 | 使用 BeanContainerBuildItem 以命令此构建步骤在创建 CDI Bean 容器后运行。 |
3 | 调用运行时记录器以记录 Bean 交互。 |
4 | 运行时记录器使用其类型检索 Bean。 |
5 | 运行时记录器调用 IConfigConsumer#loadConfig(…) 方法,传入带有运行时信息的配置对象。 |
2.20.6. 管理非 CDI 服务
扩展的一个常见目的是将非 CDI 感知服务集成到基于 CDI 的 Quarkus 运行时中。此任务的第一步是在 STATIC_INIT 构建步骤中加载所需的任何配置,正如我们在 将配置解析为对象中所做的那样。现在我们需要使用配置创建服务的实例。让我们回到 TestProcessor#parseServiceXmlConfig
方法,看看如何完成此操作。
// TestProcessor#parseServiceXmlConfig
@BuildStep
@Record(STATIC_INIT)
RuntimeServiceBuildItem parseServiceXmlConfig(TestRecorder recorder) throws JAXBException {
RuntimeServiceBuildItem serviceBuildItem = null;
JAXBContext context = JAXBContext.newInstance(XmlConfig.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
InputStream is = getClass().getResourceAsStream("/config.xml");
if (is != null) {
log.info("Have XmlConfig, loading");
XmlConfig config = (XmlConfig) unmarshaller.unmarshal(is);
log.info("Loaded XmlConfig, creating service");
RuntimeValue<RuntimeXmlConfigService> service = recorder.initRuntimeService(config); (1)
serviceBuildItem = new RuntimeServiceBuildItem(service); (3)
}
return serviceBuildItem;
}
// TestRecorder#initRuntimeService
public RuntimeValue<RuntimeXmlConfigService> initRuntimeService(XmlConfig config) {
RuntimeXmlConfigService service = new RuntimeXmlConfigService(config); (2)
return new RuntimeValue<>(service);
}
// RuntimeServiceBuildItem
final public class RuntimeServiceBuildItem extends SimpleBuildItem {
private RuntimeValue<RuntimeXmlConfigService> service;
public RuntimeServiceBuildItem(RuntimeValue<RuntimeXmlConfigService> service) {
this.service = service;
}
public RuntimeValue<RuntimeXmlConfigService> getService() {
return service;
}
}
1 | 调用运行时记录器以记录服务的创建。 |
2 | 使用解析的 XmlConfig 实例,创建 RuntimeXmlConfigService 的实例并将其包装在 RuntimeValue 中。对于非接口对象(不可代理)使用 RuntimeValue 包装器。 |
3 | 将返回的服务值包装在 RuntimeServiceBuildItem 中,以便在将启动服务的 RUNTIME_INIT 构建步骤中使用。 |
2.20.6.1. 启动服务
现在您已经在构建阶段记录了服务的创建,您需要在启动期间记录如何在运行时启动服务。您可以使用 RUNTIME_INIT 构建步骤来完成此操作,如 TestProcessor#startRuntimeService
方法所示。
// TestProcessor#startRuntimeService
@BuildStep
@Record(RUNTIME_INIT)
ServiceStartBuildItem startRuntimeService(TestRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem , (1)
RuntimeServiceBuildItem serviceBuildItem) throws IOException { (2)
if (serviceBuildItem != null) {
log.info("Registering service start");
recorder.startRuntimeService(shutdownContextBuildItem, serviceBuildItem.getService()); (3)
} else {
log.info("No RuntimeServiceBuildItem seen, check config.xml");
}
return new ServiceStartBuildItem("RuntimeXmlConfigService"); (4)
}
// TestRecorder#startRuntimeService
public void startRuntimeService(ShutdownContext shutdownContext, RuntimeValue<RuntimeXmlConfigService> runtimeValue)
throws IOException {
RuntimeXmlConfigService service = runtimeValue.getValue();
service.startService(); (5)
shutdownContext.addShutdownTask(service::stopService); (6)
}
1 | 我们使用 ShutdownContextBuildItem 来注册服务关闭。 |
2 | 我们使用先前在 RuntimeServiceBuildItem 中捕获的已初始化服务。 |
3 | 调用运行时记录器以记录服务启动调用。 |
4 | 生成 ServiceStartBuildItem 以指示服务的启动。有关详细信息,请参见 启动和关闭事件。 |
5 | 运行时记录器检索服务实例引用并调用其 startService 方法。 |
6 | 运行时记录器向 Quarkus ShutdownContext 注册服务实例 stopService 方法的调用。 |
可以在此处查看 RuntimeXmlConfigService
的代码:RuntimeXmlConfigService.java
可以在 ConfiguredBeanTest
和 NativeImageIT
的 testRuntimeXmlConfigService
测试中找到用于验证 RuntimeXmlConfigService
是否已启动的测试用例。
2.20.7. 启动和关闭事件
Quarkus 容器支持启动和关闭生命周期事件,以通知组件容器的启动和关闭。触发了 CDI 事件,组件可以观察到这些事件,如本示例所示
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
public class SomeBean {
/**
* Called when the runtime has started
* @param event
*/
void onStart(@Observes StartupEvent event) { (1)
System.out.printf("onStart, event=%s%n", event);
}
/**
* Called when the runtime is shutting down
* @param event
*/
void onStop(@Observes ShutdownEvent event) { (2)
System.out.printf("onStop, event=%s%n", event);
}
}
1 | 观察 StartupEvent 以在运行时启动时收到通知。 |
2 | 观察 ShutdownEvent 以在运行时即将关闭时收到通知。 |
启动和关闭事件对于扩展作者有什么意义?我们已经看到了在启动服务部分中使用 ShutdownContext
注册回调以执行关闭任务。这些关闭任务将在发送 ShutdownEvent
之后调用。
在所有 io.quarkus.deployment.builditem.ServiceStartBuildItem
生产者被消费后,会触发 StartupEvent
。这意味着,如果扩展具有应用程序组件期望在观察到 StartupEvent
时已经启动的服务,则调用运行时代码以启动这些服务的构建步骤需要生成 ServiceStartBuildItem
,以确保运行时代码在发送 StartupEvent
之前运行。回想一下,我们在上一节中看到了 ServiceStartBuildItem
的生成,为了清楚起见,这里重复一下
// TestProcessor#startRuntimeService
@BuildStep
@Record(RUNTIME_INIT)
ServiceStartBuildItem startRuntimeService(TestRecorder recorder, ShutdownContextBuildItem shutdownContextBuildItem,
RuntimeServiceBuildItem serviceBuildItem) throws IOException {
...
return new ServiceStartBuildItem("RuntimeXmlConfigService"); (1)
}
1 | 生成一个 ServiceStartBuildItem ,以表明这是一个需要在发送 StartupEvent 之前运行的服务启动步骤。 |
2.20.8. 注册本地镜像中使用的资源
并非所有配置或资源都可以在构建时使用。如果您有运行时需要访问的类路径资源,您需要通知构建阶段这些资源需要复制到本地镜像中。这可以通过生成一个或多个 NativeImageResourceBuildItem
或 NativeImageResourceBundleBuildItem
(如果是资源包) 来完成。此示例 registerNativeImageResources
构建步骤中显示了示例
public final class MyExtProcessor {
@BuildStep
void registerNativeImageResources(BuildProducer<NativeImageResourceBuildItem> resource, BuildProducer<NativeImageResourceBundleBuildItem> resourceBundle) {
resource.produce(new NativeImageResourceBuildItem("/security/runtime.keys")); (1)
resource.produce(new NativeImageResourceBuildItem(
"META-INF/my-descriptor.xml")); (2)
resourceBundle.produce(new NativeImageResourceBuildItem("jakarta.xml.bind.Messages")); (3)
}
}
1 | 指示应将 /security/runtime.keys 类路径资源复制到本地镜像中。 |
2 | 指示应将 META-INF/my-descriptor.xml 资源复制到本地镜像中 |
3 | 指示应将 "jakarta.xml.bind.Messages" 资源包复制到本地镜像中。 |
2.20.9. 服务文件
如果您正在使用 META-INF/services
文件,您需要将这些文件注册为资源,以便您的本地镜像可以找到它们,但您还需要为每个列出的类注册反射,以便它们可以在运行时实例化或检查
public final class MyExtProcessor {
@BuildStep
void registerNativeImageResources(BuildProducer<ServiceProviderBuildItem> services) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();
// find out all the implementation classes listed in the service files
Set<String> implementations =
ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
// register every listed implementation class so they can be instantiated
// in native-image at run-time
services.produce(
new ServiceProviderBuildItem(io.quarkus.SomeService.class.getName(),
implementations.toArray(new String[0])));
}
}
ServiceProviderBuildItem 接受服务实现类列表作为参数:如果您不是从服务文件中读取它们,请确保它们与服务文件内容相对应,因为服务文件仍将在运行时读取和使用。这不能代替编写服务文件。 |
这只会注册通过反射进行实例化的实现类(您将无法检查其字段和方法)。如果您需要这样做,您可以这样做 |
public final class MyExtProcessor {
@BuildStep
void registerNativeImageResources(BuildProducer<NativeImageResourceBuildItem> resource,
BuildProducer<ReflectiveClassBuildItem> reflectionClasses) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();
// register the service file so it is visible in native-image
resource.produce(new NativeImageResourceBuildItem(service));
// register every listed implementation class so they can be inspected/instantiated
// in native-image at run-time
Set<String> implementations =
ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
reflectionClasses.produce(
new ReflectiveClassBuildItem(true, true, implementations.toArray(new String[0])));
}
}
虽然这是使您的服务以本机方式运行的最简单方法,但它不如在构建时扫描实现类并生成代码在静态初始化时注册它们而不是依赖反射效率高。
您可以通过调整先前的构建步骤以使用静态初始化记录器而不是注册类以进行反射来实现此目的
public final class MyExtProcessor {
@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void registerNativeImageResources(RecorderContext recorderContext,
SomeServiceRecorder recorder) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();
// read the implementation classes
Collection<Class<? extends io.quarkus.SomeService>> implementationClasses = new LinkedHashSet<>();
Set<String> implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
for(String implementation : implementations) {
implementationClasses.add((Class<? extends io.quarkus.SomeService>)
recorderContext.classProxy(implementation));
}
// produce a static-initializer with those classes
recorder.configure(implementationClasses);
}
}
@Recorder
public class SomeServiceRecorder {
public void configure(List<Class<? extends io.quarkus.SomeService>> implementations) {
// configure our service statically
SomeServiceProvider serviceProvider = SomeServiceProvider.instance();
SomeServiceBuilder builder = serviceProvider.getSomeServiceBuilder();
List<io.quarkus.SomeService> services = new ArrayList<>(implementations.size());
// instantiate the service implementations
for (Class<? extends io.quarkus.SomeService> implementationClass : implementations) {
try {
services.add(implementationClass.getConstructor().newInstance());
} catch (Exception e) {
throw new IllegalArgumentException("Unable to instantiate service " + implementationClass, e);
}
}
// build our service
builder.withSomeServices(implementations.toArray(new io.quarkus.SomeService[0]));
ServiceManager serviceManager = builder.build();
// register it
serviceProvider.registerServiceManager(serviceManager, Thread.currentThread().getContextClassLoader());
}
}
2.20.10. 对象替换
在构建阶段创建并传递到运行时的对象需要有一个默认构造函数,以便可以在运行时从构建时状态启动时创建和配置它们。如果对象没有默认构造函数,您将在生成增强工件期间看到类似于以下内容的错误
[error]: Build step io.quarkus.deployment.steps.MainClassBuildStep#build threw an exception: java.lang.RuntimeException: Unable to serialize objects of type class sun.security.provider.DSAPublicKeyImpl to bytecode as it has no default constructor
at io.quarkus.builder.Execution.run(Execution.java:123)
at io.quarkus.builder.BuildExecutionBuilder.execute(BuildExecutionBuilder.java:136)
at io.quarkus.deployment.QuarkusAugmentor.run(QuarkusAugmentor.java:110)
at io.quarkus.runner.RuntimeRunner.run(RuntimeRunner.java:99)
... 36 more
有一个 io.quarkus.runtime.ObjectSubstitution
接口可以实现,以告诉 Quarkus 如何处理此类。此处显示了 DSAPublicKey
的示例实现
package io.quarkus.extest.runtime.subst;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.DSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.logging.Logger;
import io.quarkus.runtime.ObjectSubstitution;
public class DSAPublicKeyObjectSubstitution implements ObjectSubstitution<DSAPublicKey, KeyProxy> {
private static final Logger log = Logger.getLogger("DSAPublicKeyObjectSubstitution");
@Override
public KeyProxy serialize(DSAPublicKey obj) { (1)
log.info("DSAPublicKeyObjectSubstitution.serialize");
byte[] encoded = obj.getEncoded();
KeyProxy proxy = new KeyProxy();
proxy.setContent(encoded);
return proxy;
}
@Override
public DSAPublicKey deserialize(KeyProxy obj) { (2)
log.info("DSAPublicKeyObjectSubstitution.deserialize");
byte[] encoded = obj.getContent();
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encoded);
DSAPublicKey dsaPublicKey = null;
try {
KeyFactory kf = KeyFactory.getInstance("DSA");
dsaPublicKey = (DSAPublicKey) kf.generatePublic(publicKeySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
e.printStackTrace();
}
return dsaPublicKey;
}
}
1 | serialize 方法采用没有默认构造函数的对象,并创建一个 KeyProxy ,其中包含重建 DSAPublicKey 所需的信息。 |
2 | deserialize 方法使用 KeyProxy 通过密钥工厂从其编码形式重新创建 DSAPublicKey 。 |
扩展通过生成 ObjectSubstitutionBuildItem
来注册此替换,如 TestProcessor#loadDSAPublicKey
片段中所示
@BuildStep
@Record(STATIC_INIT)
PublicKeyBuildItem loadDSAPublicKey(TestRecorder recorder,
BuildProducer<ObjectSubstitutionBuildItem> substitutions) throws IOException, GeneralSecurityException {
...
// Register how to serialize DSAPublicKey
ObjectSubstitutionBuildItem.Holder<DSAPublicKey, KeyProxy> holder = new ObjectSubstitutionBuildItem.Holder(
DSAPublicKey.class, KeyProxy.class, DSAPublicKeyObjectSubstitution.class);
ObjectSubstitutionBuildItem keysub = new ObjectSubstitutionBuildItem(holder);
substitutions.produce(keysub);
log.info("loadDSAPublicKey run");
return new PublicKeyBuildItem(publicKey);
}
2.20.11. 在本地镜像中替换类
Graal SDK 支持本地镜像中类的替换。这些示例类显示了一个如何用没有 JAXB 注解依赖项的版本替换 XmlConfig/XmlData
类的示例
package io.quarkus.extest.runtime.graal;
import java.util.Date;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import io.quarkus.extest.runtime.config.XmlData;
@TargetClass(XmlConfig.class)
@Substitute
public final class Target_XmlConfig {
@Substitute
private String address;
@Substitute
private int port;
@Substitute
private ArrayList<XData> dataList;
@Substitute
public String getAddress() {
return address;
}
@Substitute
public int getPort() {
return port;
}
@Substitute
public ArrayList<XData> getDataList() {
return dataList;
}
@Substitute
@Override
public String toString() {
return "Target_XmlConfig{" +
"address='" + address + '\'' +
", port=" + port +
", dataList=" + dataList +
'}';
}
}
@TargetClass(XmlData.class)
@Substitute
public final class Target_XmlData {
@Substitute
private String name;
@Substitute
private String model;
@Substitute
private Date date;
@Substitute
public String getName() {
return name;
}
@Substitute
public String getModel() {
return model;
}
@Substitute
public Date getDate() {
return date;
}
@Substitute
@Override
public String toString() {
return "Target_XmlData{" +
"name='" + name + '\'' +
", model='" + model + '\'' +
", date='" + date + '\'' +
'}';
}
}
3. 生态系统集成
一些扩展可能是私有的,而另一些可能希望成为更广泛的 Quarkus 生态系统的一部分,并且可供社区重复使用。包含在 Quarkiverse Hub 中是处理持续测试和发布的便捷机制。Quarkiverse Hub Wiki 包含有关如何启动扩展的说明。
或者,可以手动处理持续测试和发布。
3.2. 在 registry.quarkus.io 中发布您的扩展
在将您的扩展发布到 Quarkus 工具之前,请确保满足以下要求
-
quarkus-extension.yaml 文件(在扩展的
runtime/
模块中)设置了最小元数据-
name
-
description
(除非您已在runtime/pom.xml
的<description>
元素中设置它,这是推荐的方法)
-
-
您的扩展已发布在 Maven Central 中
-
您的扩展存储库配置为使用 Ecosystem CI。
然后,您必须创建一个拉取请求,在Quarkus Extension Catalog中的extensions/
目录中添加一个your-extension.yaml
文件。YAML 必须具有以下结构
group-id: <YOUR_EXTENSION_RUNTIME_GROUP_ID>
artifact-id: <YOUR_EXTENSION_RUNTIME_ARTIFACT_ID>
当您的存储库包含多个扩展时,您需要为每个单独的扩展创建一个单独的文件,而不是为整个存储库创建一个文件。 |
就这样。合并拉取请求后,计划的作业将检查 Maven Central 的新版本并更新Quarkus Extension Registry。