Writing Your Own Extension

Quarkus 扩展为核心产品增加了新的面向开发人员的行为,并由两个不同的部分组成,即构建时增强和运行时容器。增强部分负责所有元数据处理,例如读取注释、XML 描述符等。此增强阶段的输出是记录的字节码,该字节码负责直接实例化相关的运行时服务。 这意味着元数据仅在构建时处理一次,这既节省了启动时间,也节省了内存使用,因为用于处理的类等不会在运行时 JVM 中加载(甚至不存在)。

这是一份深入的文档,如果您需要简介,请参阅 building my first extension

Extension philosophy

此部分是一份进行中的工作,并收集了应该设计和编写扩展的理念。

Why an extension framework

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 类)

Favor build time work over runtime work

尽可能优先在构建时(扩展的部署部分)执行工作,而不是让框架在启动时(运行时)执行工作。在那里执行的越多,使用该扩展的 Quarkus 应用程序就越小,加载速度就越快。

How to expose configuration

Quarkus 简化了最常见的用法。这意味着它的默认值可能与它集成的库不同。

为了让简单体验更容易,通过 SmallRye Config 统一 application.properties 中的配置。避免使用特定于库的配置文件,或至少使其成为可选的:例如,persistence.xml 对于 Hibernate ORM 是可选的。

扩展应将配置整体视为 Quarkus 应用程序,而不是专注于库体验。例如 quarkus.database.url 及其相关项在扩展之间共享,因为定义数据库访问是一项共享任务(例如,而不是 hibernate. 属性)。最实用的配置选项应公开为 quarkus.[extension].,而不是库的自然命名空间。不常见的属性可以在库命名空间中使用。

为了充分启用 Quarkus 可以进行最佳优化的封闭世界假设,最好将配置选项视为构建时设置的,而不是可以在运行时覆盖的。当然,诸如主机、端口、密码之类的属性应该可以在运行时覆盖。但诸如启用缓存或设置 JDBC 驱动程序之类的许多属性可以安全地要求重新构建该应用程序。

Static Init Config

如果扩展提供额外的配置源,并且这些源在静态初始化期间是必需的,则必须使用 StaticInitConfigBuilderBuildItem 注册这些源。静态初始化中的配置不会扫描其他源,以避免在应用程序启动时进行重复初始化。

Expose your components via CDI

由于 CDI 是组件组合中的核心编程模型,因此框架和扩展应将其组件公开为 bean,以便用户应用程序可以轻松使用它们。例如,Hibernate ORM 公开 EntityManagerFactoryEntityManager bean,连接池公开 DataSource bean 等。扩展必须在构建时注册这些 bean 定义。

Beans backed by classes

扩展可产生一个“AdditionalBeanBuildItem”,用于指示容器读取某个类提供的 Bean 定义,如同它是原始应用的一部分:

Bean Class Registered by AdditionalBeanBuildItem
@Singleton 1
public class Echo {

   public String echo(String val) {
      return val;
   }
}
1 如果某个由一个“AdditionalBeanBuildItem”注册的 Bean 未指定作用域,则假设为“@Dependent”。

所有其他 Bean 可注入此类 Bean:

Bean Injecting a Bean Produced by an AdditionalBeanBuildItem
@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:

Extension Bean Injection Example
@Singleton
public class Echo {

    @Inject
    DataSource dataSource;  1

    @Inject
    Instance<List<String>> listsOfStrings; 2

    //...
}
1 注入其他扩展提供的 Bean。
2 注入与类型“List&lt;String&gt;”匹配的所有 Bean。

Bean initialization

根据在增强期间收集的信息,某些组件可能需要额外的初始化。最简单的解决方案是从构建步骤中直接提取一个 Bean 实例并调用一个方法。但是,在增强阶段提取一个 Bean 实例是“illegal”。原因在于 CDI 容器尚未启动。它在“Static init bootstrap phase”期间启动。

BUILD_AND_RUN_TIME_FIXED”和“RUN_TIME”config 根可注入任何 Bean 中。“RUN_TIME”config 根只应在启动后注入。

不过可以从“recorder method”中调用一个 Bean 方法。如果你需要在“@Record(STATIC_INIT)”构建步骤中访问一个 Bean,那么它必须依赖于“BeanContainerBuildItem”或在一个“BeanContainerListenerBuildItem”中包装逻辑。原因很简单——我们需要确保 CDI 容器已完全初始化并已启动。但是,你可以肯定 CDI 容器已在“@Record(RUNTIME_INIT)”构建步骤中完全初始化并正在运行。你可以通过“CDI.current()”或 Quarkus 特有的“Arc.container()”获取容器的引用。

不要忘记确保 Bean 状态保证可见性,例如,通过“volatile”关键字。

这种“延迟初始化”方法有一个重大的缺点。“uninitialized”Bean 可被其他扩展或在启动期间实例化的应用组件访问。我们将在“Synthetic beans”中介绍更健壮的解决方案。

Default beans

创建此类 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
    }
}

How to Override a Bean Defined by a Library/Quarkus Extension that doesn’t use @DefaultBean

虽然“@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 备用方案。例如,默认实现仍然会接收观察器通知。

Synthetic beans

有时,能够注册一个合成 Bean 非常有用。合成 Bean 的 Bean 属性不会从 Java 类、方法或字段派生。相反,属性由扩展指定。

既然 CDI 容器不控制合成 Bean 的实例化,则不支持依赖注入和其他服务(例如拦截器)。换句话说,为合成 Bean 实例提供所有所需服务取决于扩展。

有许多方法可以在 Quarkus 中注册 synthetic bean。在本章中,我们将介绍一个用例,该用例可用于以安全的方式初始化扩展 Bean(与 Bean initialization相比)。

`SyntheticBeanBuildItem`可用于注册一个合成 Bean:

  • 其实例可通过 recorder轻松生成,

  • 提供一个“上下文”Bean,其中包含增强期间收集的所有信息,以便真实组件无需任何“延迟初始化”,因为它们可以直接注入上下文 Bean。

Instance Produced Through Recorder
@BuildStep
@Record(STATIC_INIT)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
   return SyntheticBeanBuildItem.configure(Foo.class).scope(Singleton.class)
                .runtimeValue(recorder.createFoo("parameters are recorder in the bytecode")) 1
                .done();
}
1 字符串值记录在字节码中,并用于初始化 `Foo`的实例。
"Context" Holder
@BuildStep
@Record(STATIC_INIT)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
   return SyntheticBeanBuildItem.configure(TestContext.class).scope(Singleton.class)
                .runtimeValue(recorder.createContext("parameters are recorder in the bytecode")) 1
                .done();
}
1 “真实”组件可以直接注入 TestContext

Some types of extensions

扩展有多个原型,让我们列举几个。

Bare library running

This is the less sophisticated extension. It consists of a set of patches to make sure a library runs on GraalVM. If possible, contribute these patches upstream, not in extensions. Second best is to write Substrate VM substitutions, which are patches applied during native image compilation.

Get a framework running

A framework at runtime typically reads configuration, scan the classpath and classes for metadata (annotations, getters etc.), build a metamodel on top of which it runs, find options via the service loader pattern, prepare invocation calls (reflection), proxy interfaces, etc. These operations should be done at build time and the metamodel be passed to the recorder DSL that will generate classes that will be executed at runtime and boot the framework.

Get a CDI portable extension running

The CDI portable extension model is very flexible. Too flexible to benefit from the build time boot promoted by Quarkus. Most extension we have seen do not make use of these extreme flexibility capabilities. The way to port a CDI extension to Quarkus is to rewrite it as a Quarkus extension which will define the various beans at build time (deployment time in extension parlance).

Technical aspect

Three Phases of Bootstrap and Quarkus Philosophy

Quarkus APP 有三个不同的引导阶段:

Augmentation

This is the first phase, and is done by the Build Step Processors. These processors have access to Jandex annotation information and can parse any descriptors and read annotations, but should not attempt to load any application classes. The output of these build steps is some recorded bytecode, using an extension of the ObjectWeb ASM project called Gizmo(ext/gizmo), that is used to actually bootstrap the application at runtime. Depending on the io.quarkus.deployment.annotations.ExecutionTime value of the @io.quarkus.deployment.annotations.Record annotation associated with the build step, the step may be run in a different JVM based on the following two modes.

Static Init

If bytecode is recorded with @Record(STATIC_INIT) then it will be executed from a static init method on the main class. For a native executable build, this code is executed in a normal JVM as part of the native build process, and any retained objects that are produced in this stage will be directly serialized into the native executable via an image mapped file. This means that if a framework can boot in this phase then it will have its booted state directly written to the image, and so the boot code does not need to be executed when the image is started.

在这一阶段可以做什么有一些限制,因为 Substrate VM 不允许在本地可执行文件中使用某些对象。例如,你不应尝试在这一阶段监听一个端口或启动线程。此外,禁止在静态初始化期间读取运行时配置。 在非本机纯 JVM 模式中,静态初始化和运行时初始化之间没有实际区别,不同之处在于静态初始化始终首先执行。此模式受益于与本机模式相同的构建阶段增强,因为描述符解析和注释扫描在构建时完成,并且任何关联的类/框架依赖项都可以从构建输出 jar 中删除。在 WildFly 等服务器中,与部署相关的类(例如 XML 解析器)将伴随应用程序的生命周期,占用宝贵的内存。Quarkus 旨在消除这种情况,以便在运行时加载的唯一类实际上是在运行时使用的。 作为一个例子,Quarkus 应用程序加载 XML 解析器的唯一原因是用户在其应用程序中使用 XML。配置的任何 XML 解析都应在增强阶段完成。

Runtime Init

If bytecode is recorded with @Record(RUNTIME_INIT) then it is executed from the application’s main method. This code will be run on native executable boot. In general as little code as possible should be executed in this phase, and should be restricted to code that needs to open ports etc.

尽可能多地将内容推送到 `@Record(STATIC_INIT)`阶段允许两种不同的优化:

  1. 在本地可执行和纯 JVM 模式中,由于处理是在构建期间完成的,因此这允许应用程序以最快的速度启动。这也将应用程序中需要用于纯运行时相关行为的类/本机代码最小化。

  2. 在本地可执行模式下,另一个好处是 Substrate 可以更轻松地消除未使用的功能。如果直接通过字节码初始化特性,则 Substrate 可以检测到永远不会调用某个方法并消除该方法。如果在运行时读取配置,则 Substrate 无法推理配置的内容,因此需要保留所有特性,以防需要它们。

Project setup

扩展项目应设置为一个多模块项目,其中包含两个子模块:

  1. 处理构建时间处理和字节码记录的部署时间子模块。

  2. 包含将在原生可执行文件或运行时 JVM 中提供扩展行为的运行时行为的运行时子模块。

如果要使用它们提供的功能,您的运行时工件应依赖于 io.quarkus:quarkus-core,还可能依赖其他 Quarkus 模块的运行时工件。

您的部署时间模块应依赖于 io.quarkus:quarkus-core-deployment、运行时工件以及您的扩展依赖的任何其他 Quarkus 扩展的部署工件。这一点至关重要,否则任何暂态性引入的扩展都无法提供其全部功能。

Maven 和 Gradle 插件会为此进行验证,并会提醒您您可能忘记添加的任何部署工件。

在任何情况下,运行时模块都不能依赖于部署工件。这将导致将所有部署时间代码拉入运行时范围,这与拆分的目的是相反的。

Using Maven

如果您正在使用 Quarkus 父 pom,它将自动继承正确的配置,那么您需要包含 io.quarkus:quarkus-extension-maven-plugin 并配置 maven-compiler-plugin 以检测 quarkus-extension-processor 注解处理器,才能收集和生成扩展工件所需的 Quarkus extension metadata

您可能希望使用 io.quarkus.platform:quarkus-maven-plugincreate-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>
Create new Quarkus Core extension modules using Maven

Quarkus 提供 create-extension Maven Mojo 来初始化扩展项目。

它将尝试自动检测其选项:

  • 来自 quarkus (Quarkus 核心)或 quarkus/extensions 目录,它将使用“Quarkus 核心”扩展布局和默认值。

  • 使用 -DgroupId=io.quarkiverse.[extensionId],它将使用“Quarkiverse”扩展布局和默认值。

  • 在其他情况下,它将使用“独立”扩展布局和默认值。

  • 我们未来可能会引入其他布局类型。

如果不指定任何参数,则可以使用交互模式:mvn io.quarkus.platform:quarkus-maven-plugin:${project.version}:create-extension -N

作为示例,让我们向 Quarkus 源树添加一个名为 my-ext 的新扩展:

git clone https://github.com/quarkusio/quarkus.git
cd quarkus
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create-extension -N \
    -DextensionId=my-ext \
    -DextensionName="My Extension" \
    -DextensionDescription="Do something useful."

默认情况下,groupIdversionquarkusVersionnamespaceIdnamespaceName 将与其他 Quarkus 核心扩展保持一致。

扩展说明很重要,因为它显示在 [role="bare"][role="bare"]https://code.quarkus.io/ 上,例如使用 Quarkus CLI 列出扩展时。

上面的命令序列执行以下操作:

  • 创建了四个新的 Maven 模块:

    • quarkus-my-ext-parentextensions/my-ext 目录中

    • quarkus-my-extextensions/my-ext/runtime 目录中

    • quarkus-my-ext-deploymentextensions/my-ext/deployment 目录中;在此模块中生成了一个基本的 MyExtProcessor 类。

    • quarkus-my-ext-integration-testintegration-tests/my-ext/deployment 目录;此模块中生成了一个空的 Jakarta REST Resource 类和两个测试类(用于 JVM 模式和本机模式)。

  • 在必要时链接这三个模块:

    • quarkus-my-ext-parent 被添加到 quarkus-extensions-parent&lt;modules&gt;

    • quarkus-my-ext 被添加到 Quarkus BOM(材料清单)bom/application/pom.xml&lt;dependencyManagement&gt;

    • quarkus-my-ext-deployment 被添加到 Quarkus BOM(材料清单)bom/application/pom.xml&lt;dependencyManagement&gt;

    • quarkus-my-ext-integration-test 被添加到 quarkus-integration-tests-parent&lt;modules&gt;

您还必须填写 runtime 模块 src/main/resources/META-INF 文件夹中描述您的扩展的 quarkus-extension.yaml 模板文件。

以下是 quarkus-agroal 扩展的 quarkus-extension.yaml 模板,您可以将其用作示例:

name: "Agroal - Database connection pool" 1
metadata:
  keywords: 2
  - "agroal"
  - "database-connection-pool"
  - "datasource"
  - "jdbc"
  guide: "https://quarkus.io/guides/datasource" 3
  categories: 4
  - "data"
  status: "stable" 5
1 将显示给用户的扩展名称
2 可用于在扩展目录中查找扩展的关键字
3 指向扩展指南或文档的链接
4 扩展应在 code.quarkus.io 中显示的类别,可省略,在这种情况下,扩展仍将被列出,但不在任何特定类别下
5 成熟度状态,可能是 stablepreviewexperimental,由扩展维护者评估

mojo 的 name 参数是可选的。如果您没有在命令行中指定它,插件会通过用空格替换破折号并使每个标记大写从 extensionId 中派生出它。因此,在某些情况下,您可以考虑省略显式的 name

请参阅 CreateExtensionMojo JavaDoc 以了解 mojo 的所有可用选项。

Using Gradle

您需要在扩展项目的 runtime 模块中应用 io.quarkus.extension 插件。该插件包括将生成 META-INF/quarkus-extension.propertiesMETA-INF/quarkus-extension.yml 文件的 extensionDescriptor 任务。该插件还在 deploymentruntime 模块中启用 io.quarkus:quarkus-extension-processor 注释处理器以收集和生成其余的 Quarkus extension metadata。部署模块的名称可以通过设置 deploymentModule 属性在插件中配置。该属性默认设置为 deployment

plugins {
    id 'java'
    id 'io.quarkus.extension'
}

quarkusExtension {
    deploymentModule = 'deployment'
}

dependencies {
    implementation platform('io.quarkus:quarkus-bom:{quarkus-version}')
}

Build Step Processors

工作在生成和使用 build itemsbuild steps 的增加时间完成。在与项目构建中扩展相对应的部署模块中发现的构建步骤会自动连接在一起并执行以生成最终构建工件。

Build steps

build step 是用 @io.quarkus.deployment.annotations.BuildStep 注释标记的非静态方法。每个构建步骤都可以 consume 由早期阶段生成、并且可以 produce 由后期阶段使用的项目。构建步骤通常只有在其生成最终由另一个步骤使用的构建项目时才运行。

构建步骤通常放在扩展的部署模块中的简单类上。在增加过程中会自动实例化类,并利用 injection

Build items

构建项目是抽象 io.quarkus.builder.item.BuildItem 类的具体、最终子类。每个构建项目表示必须从一个阶段传递到另一个阶段的信息单元。基本 BuildItem 类本身可能无法直接进行子类化;更确切地说,对于 may 能创建的每种构建项目子类,都有抽象子类: simplemultiempty

将构建项目视为不同扩展相互通信的一种方式。例如,构建项目可以:

  • 公开数据库配置存在的事实

  • 使用该数据库配置(例如连接池扩展或 ORM 扩展)

  • 要求某个扩展为另一个扩展执行工作:例如想要定义新的 CDI bean 并要求 ArC 扩展执行此操作的某个扩展

这是一个非常灵活的机制。

BuildItem 实例应该是不可变的,因为生产者/使用者模型不允许以正确的顺序执行变更。这并没有得到强制,但未能遵守此规则可能会导致竞争条件。

仅当构建步骤生成其他构建步骤(及传递依赖项)所需要的构建项目时,才会执行该构建步骤。确保构建步骤生成构建项目,否则您可能需要为构建验证生成 ValidationErrorBuildItem,或为生成的手工制品生成 ArtifactResultBuildItem

Simple build items

简单的构建项目是扩展 io.quarkus.builder.item.SimpleBuildItem 的 final 类。给定构建中只有一个步骤可以生成简单的构建项目;如果构建中的多个步骤声明它们生成相同的简单构建项目,则会引发错误。任意数量的构建步骤都可以使用简单的构建项目。使用简单构建项目的构建步骤始终会在生成该项目的构建步骤 after

Example of a single build item
/**
 * 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;
    }
}
Multi build items

多构建项目或“多”构建项目是扩展 io.quarkus.builder.item.MultiBuildItem 的 final 类。任何数量的步骤都可以生成给定类中的任意数量的多构建项目,但使用多构建项目的任何步骤只会在能生成它们的每个步骤 after 运行。

Example of a multiple build item
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;
    }
}
Example of multiple build item usage
/**
 * 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()
                )
            );
        }
    }
}
Empty build items

空构建项目是扩展 io.quarkus.builder.item.EmptyBuildItem 的 final(通常为空)类。它们表示实际上不携带任何数据的构建项目,并允许生成和使用此类项目,而无需实例化空类。它们本身无法实例化。

由于它们无法实例化,因此它们不能通过任何方式注入,也不能通过构建步骤(或通过 BuildProducer)返回。要生成空构建项目, 您必须使用 @Produce(MyEmptyBuildItem.class) 对构建步骤进行注释并且通过 @Consume(MyEmptyBuildItem.class) 来使用它们。

Example of an empty build item
public final class NativeImageBuildItem extends EmptyBuildItem {
    // empty
}

空构建项目可以表示可以强制步骤之间的顺序的“障碍”。它们还可以按照流行构建系统使用“伪目标”的方式使用,也就是说构建项目可以表示没有具体表示的概念目标。

Example of usage of an empty build item in a "pseudo-target" style
/**
 * 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)
    // ...
}
Example of usage of an empty build item in a "barrier" style
/**
 * This would always run after {@link #produceNativeImage()} completes, producing
 * an instance of {@code SomeOtherBuildItem}.
 */
@BuildStep
@Consume(NativeImageBuildItem.class)
SomeOtherBuildItem secondBuildStep() {
    return new SomeOtherBuildItem("foobar");
}
Validation Error build items

它们表示包含使构建失败的验证错误的构建项目。这些构建项目在 CDI 容器初始化期间使用。

Example of usage of an validation error build item in a "pseudo-target" style
@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")));
    }
}
Artifact Result build items

它们表示构建生成的运行时手工制品,例如 uberjar 或 thin jar。这些构建项目还可用于始终执行构建步骤,而无需生成任何内容。

Example of build step that is always executed in a "pseudo-target" style
@BuildStep
@Produce(ArtifactResultBuildItem.class)
void runBuildStepThatProducesNothing() {
    // ...
}

Injection

包含构建步骤的类支持以下注入类型:

  • Constructor parameter injection

  • Field injection

  • 方法参数注入(仅适用于构建步骤方法)

构建步骤类在每次构建步骤调用时实例化并注入,然后丢弃。即使步骤在同一类上,也只应通过构建项目在构建步骤之间进行通信。

最终字段不考虑注入,但可以根据需要通过构造函数参数注入来填充。静态字段绝不考虑进行注入。

可以注入的值的类型包括:

注入到构建步骤方法或其类_must not_中的对象在该方法执行结束前不能使用

注入在编译时通过注释处理器解决,生成代码没有权限注入私有字段或调用私有方法

Producing values

构建步骤可以通过几种可能的方式为后续步骤产生值:

  • 通过返回simple build itemmulti build item实例

  • 通过返回一个多构建项目类的`List`

  • 通过注入一个简单的或多构建项目类的`BuildProducer`

  • 通过注释使用 `@io.quarkus.deployment.annotations.Produce`的方法,提供empty build item的类名称

如果在构建步骤中声明了一个简单的构建项目,则 must 会在该构建步骤期间产生,否则将产生错误。注入到步骤中的构建生成器不能 must not 在该步骤外使用

请注意,只有当 @BuildStep 方法生成消费者或最终输出所需的东西时,它才会被调用。如果没有特定项目的消费者,那么它将不会生成。生成的内容依赖于正在生成的最终目标。例如,当在开发者模式下运行时,最终输出将不会调用诸如 ReflectiveClassBuildItem 的 GraalVM 特定的构建项目,所以只生成这些项目的那些方法将不会被调用。

Consuming values

构建步骤可以通过以下方式使用先前步骤的值:

  • 通过注入 simple build item

  • 通过注入一个简单的构建项目类的 Optional

  • 通过注入一个 Listmulti build item

  • 通过注释使用 @io.quarkus.deployment.annotations.Consume 的方法,提供 empty build item 的类名称

通常,如果一个包含的步骤使用了未由任何其他步骤生成的一个简单的构建项目,则会出现错误。通过这种方式,可以保证在步骤运行时,所有声明的值都存在并且是非-null 的。

有时,值对于构建完成而言并非必要,但如果存在,它可能会告知构建步骤的一些行为。在这种情况下,可以有选择地插入该值。

多构建值始终被视为 optional。如果不存在,则将插入一个空列表。

Weak value production

通常,无论它产生任何构建项,只要其他构建步骤进而使用任何构建项,就都会包括构建步骤。通过这种方式,只会包括生成最终制品所需的步骤,而与未安装的扩展有关的步骤或仅生成与给定制品类型无关的构建项的步骤将被排除在外。

如果这不是所需的行为,则可以使用 `@io.quarkus.deployment.annotations.Weak`注释。此注释表示不应仅根据生成带注释的值而自动包含构建步骤。

Example of producing a build item weakly
/**
 * 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

请注意,在使用 bytecode recording时,可以通过使用 `@io.quarkus.deployment.annotations.Record`注释的 `optional`属性将隐式生成的类声明为弱。

Example of using a bytecode recorder where the generated class is weakly produced
/**
 * 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 Note the optional attribute.
2 此示例正在使用记录器代理;有关更多信息,请参阅有关 bytecode recording的部分。

Application Archives

@BuildStep`注释还可以注册确定类路径上的哪些档案被视为“应用程序档案”的标记文件,因此将被编入索引。这是通过 `applicationArchiveMarkers`完成的。例如,ArC 扩展注册 `META-INF/beans.xml,这意味着具有 `beans.xml`文件的类路径上的所有档案都将被编入索引。

Using Thread’s Context Class Loader

构建步骤将使用 TCCL 运行,该 TCCL 可以以转换器安全的方式从部署中加载用户类。此类加载器仅在增强期间存在,之后将被丢弃。该类将在运行时在不同的类加载器中再次加载。这意味着在增强期间加载类不会阻止它在开发/测试模式下运行时被转换。

Adding external JARs to the indexer with IndexDependencyBuildItem

扫描的类索引自动不包括外部类依赖项。若要添加依赖项,请创建一个 @BuildStep,该 `@BuildStep`为 `groupId`和 `artifactId`生成 `IndexDependencyBuildItem`对象。

指定所有需要添加到索引器中的制品非常重要。没有制品会被隐式地以传递方式添加。

`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));
        }

    }

Visualizing build step dependencies

偶尔查看各种构建步骤之间的交互的视觉表现可能很有用。对于此类情况,在构建应用程序时添加 `-Dquarkus.builder.graph-output=build.dot`将导致在项目的根目录中创建 `build.dot`文件。请参阅 this以获取可以打开该文件并显示实际视觉表示的软件列表。

Configuration

Quarkus 中的配置基于 SmallRye ConfigSmallRye Config提供的所有功能也在 Quarkus 中可用。

扩展必须使用 SmallRye Config @ConfigMapping来映射扩展所需的配置。这将允许 Quarkus 自动将映射的实例公开到每个配置阶段并生成配置文档。

Config Phases

配置映射严格受限于配置阶段,尝试从其对应阶段之外访问配置映射将会导致错误。它们规定了何时从配置读取其包含的密钥,以及何时它们可供应用程序使用。io.quarkus.runtime.annotations.ConfigPhase 定义的阶段如下所示:

Phase name 在构建时读入和利用 Avail. at run time Read during static init 在启动过程中重新读取(本机可执行文件) Notes

BUILD_TIME

适用于会影响构建的内容。

BUILD_AND_RUN_TIME_FIXED

适用于会影响构建且必须对运行时代码可见的内容。在运行时不会从配置读取。

BOOTSTRAP

在需要从外部系统(如 Consul)获取运行时配置但该系统的详细信息需要可配置时使用(例如 Consul 的 URL)。这种方式的高级工作原理是使用标准 Quarkus 配置源(例如属性文件、系统属性等)并生成 ConfigSourceProvider 对象,这些对象随后在 Quarkus 创建最终运行时 Config 对象时被考虑在内。

RUN_TIME

在构建时不可用,在所有模式下启动时读取。

对于 BUILD_TIME 之外的所有情况,配置映射接口及其包含的所有配置组和类型都必须位于扩展的运行时工件中或可以从扩展的运行时工件中访问。阶段 BUILD_TIME 的配置映射可以位于扩展的运行时工件或部署工件中或可以从其中访问。

Bootstrap 配置步骤在运行时初始化 before 的任何其他运行时步骤期间执行。这意味着作为此步骤一部分执行的代码无法访问在运行时初始化步骤(运行时合成 CDI bean 是一个这样的示例)中初始化的任何内容。

Configuration Example

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 注释指示配置适用的 Config 阶段。
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 中的默认值代替。

配置映射名称可以包含一个额外的后缀片段,以用于有多个 Config Phases 的情况。与 BUILD_TIMEBUILD_AND_RUN_TIME_FIXED 对应的类可能以 BuildTimeConfigBuildTimeConfiguration 结尾,与 RUN_TIME 阶段对应的类可能以 RuntimeConfigRunTimeConfigRuntimeConfigurationRunTimeConfiguration 结尾,而与 BOOTSTRAP 配置对应的类可能以 BootstrapConfigBootstrapConfiguration 结尾。

Configuration Reference Documentation

配置是每个扩展的重要组成部分,因此需要进行正确的文档记录。每个配置属性都应有正确的 Javadoc 注释。

虽然在编码时拥有可用文档很方便,但配置文档也必须在扩展指南中可用。Quarkus 构建会根据 Javadoc 注释自动生成配置文档,但需要将其明确包含在每个指南中。

Writing the documentation

每个配置属性都需要一个解释其用途的 Javadoc。

第一句话应该是切实有意义且独立的,因为它包含在摘要表中。

虽然对于简单的文档,标准的 Javadoc 注释完全够用(甚至推荐),但 AsciiDoc 更适合提示、源代码摘录、列表等等:

/**
 * Class name of the Hibernate ORM dialect. The complete list of bundled dialects is available in the
 * https://docs.jboss.org/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。

更详细的示例:

/**
 * 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.io/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
 */
Optional<String> sqlLoadScript();

为了使缩进在 Javadoc 注释(跨多行或缩进行源代码的列表项)中得到遵守,必须禁用自动 Eclipse 格式化程序(格式化程序会自动包含在构建中),且使用 // @formatter:off/// @formatter:on 标记。这些要求单独的注释,并在 // 标记后强制留一个空格。

AsciiDoc 文档中不支持开放块 (--)。其他所有类型的块(源代码、警告…​)均受支持。

默认情况下,文档生成器将使用带连字符的字段名作为 java.util.Map 的键。使用 io.quarkus.runtime.annotations.ConfigDocMapKey 注释来覆盖行为。

@ConfigMapping(prefix = "quarkus.some")
@ConfigRoot
public interface SomeConfig {
    /**
     * Namespace configuration.
     */
    @WithParentName
    @ConfigDocMapKey("cache-name") (1)
    Map<String, Name> namespace();
}
1 这将生成一个名为 quarkus.some."cache-name" 而不是 quarkus.some."namespace" 的配置映射键。

可以编写一个文本解释以用于文档默认值,当生成默认值时,这个解释很有用:@ConfigDocDefault("explain how this is generated"). @ConfigDocEnumValue 提供了一种明确自定义在为枚举列出允许的值时文档中显示的字符串的方法。

Writing section documentation

要生成给定组的配置部分,请使用 @ConfigDocSection 注释:

/**
* Config group related configuration.
* Amazing introduction here
*/
@ConfigDocSection (1)
ConfigGroupConfig configGroup();
1 这将在生成的文档中为 configGroup 配置项添加一个部分文档。部分标题和引言将根据配置项的 javadoc 导出。javadoc 中的第一句话被视为部分标题,而剩余的句子则用作部分引言。
Generating the documentation

要生成文档:

  • Execute ./mvnw -DquicklyDocs

  • 可以在全局范围内或在特定扩展目录(如 extensions/mailer)中执行。

文档在位于项目根目录的全局 target/asciidoc/generated/config/ 中生成。

Including the documentation in the extension guide

要将生成的配置参考文档包含在指南中,请使用:

要仅包含一个特定的配置组:

例如,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

在提交之前,所有文档都应生成并验证。

Conditional Step Inclusion

只能在特定条件下包含给定的 @BuildStep@BuildStep 注释有两个可选参数: onlyIfonlyIfNot。可以将这些参数设置为一个或多个实现 BooleanSupplier 的类。只有当方法返回 true(对于 onlyIf)或 false(对于 onlyIfNot) 时,才包含该构建步骤。

条件类可以注入 configuration mappings,只要它们属于构建时阶段。条件类不提供运行时配置。

条件类还可以注入类型为 io.quarkus.runtime.LaunchMode 的值。支持构造函数参数和字段注入。

An example of a conditional build step
@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 向给定类中的所有构建步骤应用一组条件:

Class-wide condition for build step with @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 其他构建步骤只会在开发模式下执行。

Bytecode Recording

构建过程的主要输出之一是记录的字节码。该字节码实际上设置了运行时环境。例如,为了启动 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");
}

运行此构建步骤时,控制台不会打印任何内容。这是因为注入的 @Recorder 实际上是一个记录所有调用的代理。而如果我们运行生成的 Quarkus 程序,我们将看到“Hello World”打印到控制台。

记录器上的方法可以返回值,该返回值必须是可代理的(如果您想返回一个不可代理的项,请用 @Supplier 包裹它)。但是,这些代理不能直接调用,可以将其传递给其他记录器方法。这可以是任何记录器方法,包括来自其他 @Record 方法的方法,因此一种常见模式是生成 @Supplier 实例,这些实例包含这些记录器调用结果的封装。

例如,为了对 Servlet 部署进行任意的修改,Undertow 具有一个 @ServletExtension,这是一个 @Recorder,它包装了一个 @Servlet instance。我可以从一个记录器返回一个 @Supplier,而在另一个模块中,Undertow 会使用它并将其传递给启动 Undertow 的记录器方法。

在运行时,将按生成顺序调用字节码。这意味着构建步骤依赖隐式控制了生成字节码的运行顺序。在上面的示例中,我们知道生成 @Supplier 的字节码将在使用它的字节码之前运行。

可以将以下对象传递给记录器:

  • Primitives

  • String

  • Class<?> objects

  • 从上一个记录器调用返回的对象

  • 具有无参数构造函数以及所有属性(或公共字段)的 getter/setter 的对象

  • 构造函数带 @Param 注解且参数名称与字段名称匹配的对象

  • 通过 @Supplier 机制任意对象

  • 上述对象的数组、列表和映射

在应该忽略要记录的对象的一些字段的情况下(即构建时所处的值不应在运行时反映出来),可将 @Ignore 在该字段上。 如果类不能依赖于 Quarkus,那么只要扩展实现 @Record SPI 就可以使用任何自定义注解。 也可以使用同一个 SPI 提供一个自定义注解,以替代 @Record。

Injecting Configuration into Recorders

阶段为 @Build 或 @BuildProducer 的配置对象可以通过构造函数注入注入到记录器中。只需创建一个包含记录器需要的配置对象的构造函数。如果记录器有多个构造函数,您可以使用 @Inject 注解希望 Quarkus 使用的构造函数。如果记录器要注入运行时配置,但也用于静态初始化时间,那么它需要注入 @InitializedBean,该值仅当调用运行时方法时才被设置。

RecorderContext

io.quarkus.deployment.recording.RecorderContext 提供了一些增强字节码记录的便利方法,其中包括为没有无参数构造函数的类型登记创建功能、登记一个对象置换(基本上从一个不可序列化的对象变换成一个可序列化对象,反之亦然)以及创建一个类代理。这个接口能够直接作为方法参数注入到任何一个 @Record 方法中。

使用给定的完全限定类名调用 classProxy 会创建一个 Class 实例,该实例可以传递到一个记录器方法中,并在运行时使用传递到 classProxy() 中的类名进行置换。然而,在大多数情况下不需要使用这个方法,因为直接在生成步骤的处理时间加载部署/应用类是安全的。因此,这个方法被废弃了。尽管如此,在一些情况下这个方法非常有用,比如引用利用`GeneratedClassBuildItem`在之前的生成步骤中生成的类。

Runtime Classpath check

扩展经常需要一种方法来确定一个给定的类是否属于应用的运行类路径的一部分。扩展执行这个检查的正确方法是使用 io.quarkus.bootstrap.classloading.QuarkusClassLoader.isClassPresentAtRuntime

Printing step execution time

有时,了解每个启动任务(它是每次字节码记录的结果)在应用程序运行时花费的精确时间可能很有用。确定这个信息的简单方法是启动带有 -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

Contexts and Dependency Injection

Extension Points

作为基于 CDI 的运行时,Quarkus 扩展经常将 CDI bean 作为扩展行为的一部分。然而,Quarkus DI 解决方式不支持 CDI 可移植扩展。相反,Quarkus 扩展可以使用各种各样的 Build Time Extension Points

Quarkus Dev UI

你可以让你的扩展支持 Quarkus Dev UI 来提升开发人员体验。

Extension-defined endpoints

你的扩展可以添加额外的,非应用端点与用于健康、指标、OpenAPI、Swagger UI 等的端点一起提供服务。

使用 NonApplicationRootPathBuildItem 定义一个端点:

@BuildStep
RouteBuildItem myExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
    return nonApplicationRootPathBuildItem.routeBuilder()
                .route("custom-endpoint")
                .handler(new MyCustomHandler())
                .displayOnNotFoundPage()
                .build();
}

请注意,上述路径不以 '/' 开头,表明它是一个相对路径。上述 end-point 将相对于配置的非应用程序端点根来提供服务。默认情况下,非应用程序端点根是 /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 configuration reference

Extension Health Check

健康检查通过 quarkus-smallrye-health 扩展提供。它既提供了活动检查又提供了准备检查的能力。

在编写一个扩展时,最好为扩展提供健康检查功能,它可以自动包含,而不需要开发人员自己编写。

为了提供健康检查,你应该:

  • 在你的运行时模块中将 quarkus-smallrye-health 扩展作为 optional 依赖项导入,这样如果未包含健康检查,它将不会影响应用程序的大小。

  • 按照 SmallRye Health 指南创建你的健康检查。我们建议只为扩展提供准备检查(活动检查用于表示一个正在启动并且需要轻量的应用)。

  • 在您的部署模块中导入 quarkus-smallrye-health-spi 库。

  • 在您的部署模块中添加生成 HealthBuildItem 的构建步骤。

  • 添加通过默认情况下应启用的配置项 quarkus.&lt;extension&gt;.health.enabled 禁用扩展运行状况检查的方法。

以下是来自 Agroal 扩展的示例,它提供了 DataSourceHealthCheck 来验证数据源的准备就绪。

@BuildStep
HealthBuildItem addHealthCheck(AgroalBuildTimeConfig agroalBuildTimeConfig) {
    return new HealthBuildItem("io.quarkus.agroal.runtime.health.DataSourceHealthCheck",
            agroalBuildTimeConfig.healthEnabled);
}

Extension Metrics

quarkus-micrometer 扩展和 quarkus-smallrye-metrics 扩展提供收集指标的支持。作为兼容性备注,quarkus-micrometer 扩展将 MP 指标 API 调整为 Micrometer 库的基元,因此可以在不破坏依赖于 MP 指标 API 的代码的情况下启用 quarkus-micrometer 扩展。请注意,Micrometer 发出的指标是不同的,更多信息请参见 quarkus-micrometer 扩展文档。

MP 指标 API 的兼容性层将来将移至不同的扩展。

扩展可以使用两个广泛的模式与可选指标扩展进行交互,以添加自己的指标:

  • 使用者模式:一个扩展声明 MetricsFactoryConsumerBuildItem 并使用它向指标扩展提供字节码记录器。当指标扩展初始化后,它将遍历已注册使用者以使用 MetricsFactory 对它们进行初始化。该工厂可用于声明与 API 无关的指标,这很适合于为收集统计信息提供可插装对象的扩展(例如,Hibernate 的 Statistics 类)。

  • Binder 模式:一个扩展可以选择完全不同的收集实现,具体取决于指标系统。一个 Optional&lt;MetricsCapabilityBuildItem&gt; metricsCapability 构建步骤参数可用于声明或根据活动指标扩展(例如“smallrye-metrics”或“micrometer”)初始化特定于 API 的指标。该模式可以通过使用 MetricsFactory::metricsSystemSupported() 在记录器中测试活动指标扩展,与使用者模式组合在一起。

请记住,指标支持是可选的。扩展可以在其构建步骤中使用 Optional<MetricsCapabilityBuildItem> metricsCapability 参数来测试是否启用了指标扩展。考虑使用附加配置来控制指标的行为。例如,数据源指标可能开销很高,因此使用额外的配置标志对各个数据源启用指标收集。

添加扩展的指标时,您可能会发现自己处于以下情况之一:

  1. - 扩展使用的底层库直接使用特定的指标 API(MP 指标、Micrometer 或其他一些 API)。

  2. - 底层库使用自己的机制收集指标,并使用其自己的 API 在运行时使其可用,例如 Hibernate 的 Statistics 类或 Vert.x MetricsOptions

  3. - 底层库不提供指标(或根本没有库),而您想要添加检测。

Case 1: The library uses a metrics library directly

如果库直接使用指标 API,则有两个选项:

  • - 在您的构建步骤中使用 Optional&lt;MetricsCapabilityBuildItem&gt; metricsCapability 参数来测试在您的构建步骤中受支持的指标 API(例如,“smallrye-metrics”或“micrometer”),并使用它来选择性地声明或初始化特定于 API 的 Bean 或构建项。

  • - 创建一个单独的构建步骤,它使用 MetricsFactory,并在支持所需的指标 API(例如,“smallrye-metrics”或“micrometer”)的情况下,使用字节码记录器中的 MetricsFactory::metricsSystemSupported() 方法来初始化所需资源。

如果不存在活动指标扩展或扩展不支持库所需的 API,则扩展可能需要提供后备。

Case 2: The library provides its own metric API

图书馆提供其自己的指标 API 的例子有两个:

  • 扩展程序将可检测对象定义为 @1 的 Agroal 所做的那样或

  • 扩展程序提供自己的指标抽象,就像 @2 的 Jaeger 所做的那样。

Observing instrumentable objects

让我们首先介绍可检测对象(@3)的情况。在这种情况下,你可以执行以下操作:

  • 定义一个 @4,用于生成一个 @5,它使用 @6 或 @7 记录器定义一个 @8 消费者。例如,仅当同时为 Agroal 和特定数据源启用了指标时,才会创建 @9:[source, java]

@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())));
        }
    }
}
  • 关联的记录器应使用提供的 @10 注册指标。对于 Agroal,这意味着使用 @11 API 来观察 @12 方法。例如:[source, java]

/* 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);
            ....

@12 为指标注册提供了一个流畅的构建器,最后一步基于 @13 或 @14 构建量表或计数器。计时器可以包装 @15、@16 或 @17 实现,也可以使用 @18 累积时间块。基础指标扩展程序将创建相应的工件来观察或测量已定义的函数。

Using a Metrics API-specific implementation

在某些情况下,可能更喜欢使用特定的指标 API 实现。例如,Jaeger 定义了自己的指标接口 @20,它用于定义计数器和量表。从该接口到指标系统的直接映射将是最有效的。在这种情况下,重要的是隔离这些专门实现并避免急切类加载,以确保指标 API 保持可选的编译时依赖项。

可以在构建步骤中使用 @21 来选择性地控制 bean 的初始化或其他构建项的生成。例如,Jaeger 扩展程序可以使用以下命令来控制特殊指标 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);
        }
    }
}

使用 @22 的记录器可以类似地使用 @23 在字节码记录期间控制指标对象的初始化。

Case 3: It is necessary to collect metrics within the extension code

要从头定义自己的指标,你有两个基本选择:使用通用的 @24 构建器,或遵循绑定器模式,并创建特定于已启用指标扩展程序的检测。

要使用与扩展程序无关的 @25 API,你的处理器可以定义一个 @26,用于生成一个 @27,它使用 @28 或 @29 记录器来定义一个 @30 消费者。

+

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
MetricsFactoryConsumerBuildItem registerMetrics(MyExtensionRecorder recorder) {
    return new MetricsFactoryConsumerBuildItem(recorder.registerMetrics());
}

+- 关联的记录器应使用提供的 @31 注册指标,例如

+

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。收集指标也可能是昂贵的。请考虑使用其他扩展特定的配置来控制指标行为,如果指标支持的存在/不存在不足以满足需求。

Customizing JSON handling from an extension

扩展程序通常需要为扩展程序提供的类型注册序列化器和/或反序列化器。

为此,Jackson 和 JSON-B 扩展程序都提供了一种从扩展程序部署模块内注册序列化器/反序列化器的方法。

请记住,并非每个人都需要 JSON,因此你需要使它成为可选的。

如果扩展程序打算提供与 JSON 相关的自定义,则强烈建议同时为 Jackson 和 JSON-B 提供自定义。

Customizing Jackson

首先,向扩展程序的运行时模块的 @32 中添加一个 @33 依赖项。

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jackson</artifactId>
  <optional>true</optional>
</dependency>

然后为 Jackson 创建一个序列化器或反序列化器(或两者),可以在 @34 扩展程序中看到其中一个示例。

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 指南 Configuring JSON support 中找到

Customizing JSON-B

首先,在您的扩展的运行时模块上将 optional 依赖项添加到 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 指南 Configuring JSON support 中找到

Integrating with Development Mode

您可以使用各种 API 来集成开发模式并获取有关当前状态的信息。

Handling restarts

当 Quarkus 启动 io.quarkus.deployment.builditem.LiveReloadBuildItem 时,它会保证存在,提供有关此启动的信息,尤其是以下信息:

  • 这是一次干净启动,还是一次热加载

  • 如果是热加载,哪些更改的文件/类触发了热加载

它还提供了一个全局上下文映射,您可以使用此映射在重启期间存储信息,而不需要使用静态字段。

Triggering Live Reload

热加载通常由一个 HTTP 请求触发,但是并非所有应用程序都是 HTTP 应用程序,有些扩展可能希望根据其他事件触发热加载。要执行此操作,您需要在运行时模块中实现 io.quarkus.dev.spi.HotReplacementSetup,并添加列出您的实现的 META-INF/services/io.quarkus.dev.spi.HotReplacementSetup

在启动时,将调用 setupHotDeployment 方法,您可以使用提供的 io.quarkus.dev.spi.HotReplacementContext 启动扫描更改的文件。

Testing Extensions

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 部署应失败,并显示特定的异常

Testing hot reload

还可以编写测试来验证扩展在开发模式下是否可以正常工作,并且可以正确处理更新。

对于大多数扩展,这都可以直接“开箱即用”,但最好进行集成测试以验证此功能是否按预期工作。我们可以使用 `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 此方法添加新的静态资源

Native Executable Support

Quarkus 提供了许多生成项来控制本机可执行程序构建的各个方面。这允许扩展以编程方式执行任务,例如注册反射类或向本机可执行程序添加静态资源。下面列出了其中一些生成项:

io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem

Includes static resources into the native executable.

io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem

Includes directory’s static resources into the native executable.

io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem

A class that will be reinitialized at runtime by Substrate. This will result in the static initializer running twice.

io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem

A system property that will be set at native executable build time.

io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem

Includes a resource bundle in the native executable.

io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem

Registers a class for reflection in Substrate. Constructors are always registered, while methods and fields are optional.

io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem

A class that will be initialized at runtime rather than build time. This will cause the build to fail if the class is initialized as part of the native executable build process, so care must be taken.

io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem

A convenience feature that allows you to control most of the above features from a single build item.

io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem

Indicates that all charsets should be enabled in native image.

io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem

A convenient way to tell Quarkus that the extension requires SSL, and it should be enabled during native image build. When using this feature, remember to add your extension to the list of extensions that offer SSL support automatically on the native and ssl guide.

IDE support tips

Writing Quarkus extensions in Eclipse

在 Eclipse 中编写 Quarkus 扩展的唯一特定方面是 APT(注解处理工具)是扩展构建的一部分所需的,这意味着你需要:

  • Install m2e-apt from [role="bare"]https://marketplace.eclipse.org/content/m2e-apt

  • pom.xml`中定义此属性: `&lt;m2e.apt.activation&gt;jdt_apt&lt;/m2e.apt.activation&gt;,不过,如果你依赖 io.quarkus:quarkus-build-parent,你将免费获得此属性。

  • 如果您在 IDE 中同时打开了 io.quarkus:quarkus-extension-processor 项目(例如,如果您已签出并打开了 Quarkus 源代码),您需要关闭该项目。否则,Eclipse 不会调用其中包含的 APT 插件。

  • 如果您刚刚关闭了扩展处理器项目,务必对其他项目执行 Maven &gt; Update Project 操作,以便 Eclipse 从 Maven 存储库中提取扩展处理器。

Troubleshooting / Debugging Tips

Inspecting the Generated/Transformed Classes

Quarkus 在构建阶段会生成大量类,在许多情况下还会转换现有类。在扩展开发过程中,经常非常有必要看到生成的字节码和转换过的类。

如果您将 quarkus.package.jar.decompiler.enabled 属性设置为 true,Quarkus 将下载并调用 Vineflower decompiler,并将结果转储到构建工具输出的 decompiled 目录中(例如,对于 Maven 为 target/decompiled)。

此属性仅在正常的生产构建期间有效(即,不适用于开发模式/测试),且在使用 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-dirquarkus.debug.generated-sources-dir 属性转储相关输出。

Multi-module Maven Projects and the Development Mode

在包含“示例”模块的多模块 Maven 项目中开发扩展是常见做法。但是,如果您要在开发模式下运行示例,则必须使用 -DnoDeps 系统属性来排除本地项目依赖项。否则,Quarkus 会尝试监控扩展类,这可能会导致奇怪的类加载问题。

./mvnw compile quarkus:dev -DnoDeps

Indexer does not include your external dependency

请务必将 IndexDependencyBuildItem 工件添加到您的 @BuildStep 中。

Sample Test Extension

我们有一个扩展程序,用于测试扩展处理中的回归。它位于 $${quarkus-base-url}/tree/main/integration-tests/test-extension/extension 目录中。在本节中,我们将介绍一个扩展作者通常需要使用 test-extension 代码来执行的一些任务,以说明如何完成该任务。

Features and Capabilities

Features

feature 表示扩展程序提供的功能。特性名称在应用程序引导期间显示在日志中。

Example Startup Lines
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 运行时镜像中安装的功能列表

功能可以在生成 FeatureBuildItemBuild Step Processors 方法中注册:

TestProcessor#feature()
    @BuildStep
    FeatureBuildItem feature() {
        return new FeatureBuildItem("test-extension");
    }

该功能的名称应仅包含小写字符,单词用破折号分隔;例如 security-jpa。一个扩展应最多提供一个功能,该名称必须是唯一的。如果多个扩展注册了同名功能,构建将失败。

该功能名称还应映射到扩展的 devtools/common/src/main/filtered/extensions.json 条目中的标签,以便启动行显示的功能名称与在创建项目时使用 Quarkus maven 插件来选择扩展时可以使用的一个标签相匹配,如从 Writing JSON REST Services 指南中摘取的此示例所示,其中引用了 rest-jackson 功能:

mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-json \
    -DclassName="org.acme.rest.json.FruitResource" \
    -Dpath="/fruits" \
    -Dextensions="rest,rest-jackson"
cd rest-json
Capabilities

capability 表示其他扩展可以查询的技术功能。一个扩展可以提供多个功能,多个扩展可以提供相同的功能。默认情况下,功能不会显示给用户。在检查扩展是否存在时应使用功能,而不是基于类路径的检查。

功能可以在生成 CapabilityBuildItemBuild Step Processors 方法中注册:

TestProcessor#capability()
    @BuildStep
    void capabilities(BuildProducer<CapabilityBuildItem> capabilityProducer) {
        capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-transactions"));
        capabilityProducer.produce(new CapabilityBuildItem("org.acme.test-metrics"));
    }

扩展可以使用 Capabilities 构建项使用已注册的功能:

TestProcessor#doSomeCoolStuff()
    @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 前缀开头。

Bean Defining Annotations

CDI 层处理明确注册或基于 2.5.1. Bean defining annotations 中定义的 bean 定义注释发现的 CDI bean。您可以使用 BeanDefiningAnnotationBuildItem 扩展此注释集以包括扩展进程注释,如此 TestProcessor#registerBeanDefinningAnnotations 示例所示:

Register a Bean Defining Annotation
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 标准 @ApplicationScoped 注解的 bean 一样,CDI 层将处理 ConfiguredBean

Parsing Config to Objects

扩展很可能要做的主要事情之一是将行为的配置阶段与运行时阶段完全分开。框架通常在启动时执行配置的解析/加载,这可以在构建期间完成,以同时减少对类似 xml 解析器之类的框架的运行时依赖以及缩短解析所花费的启动时间。

TestProcessor#parseServiceXmlConfig 方法中展示了使用 JAXB 解析 XML 配置文件的一个示例:

Parsing an XML Configuration into Runtime XmlConfig Instance
    @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 RuntimeServiceBuildItem,且不执行基于生成 RuntimeServiceBuildItem 的后续逻辑。

通常,加载配置是为了创建某个运行时组件/服务,就像 parseServiceXmlConfig 所做的那样。在以下 Manage Non-CDI Service 部分中的 parseServiceXmlConfig 中,我们将在后面回到行为的余下部分。

如果因为某个原因,您需要在扩展进程的其他构建步骤中解析配置并使用它,则需要创建一个 XmlConfigBuildItem 来传递解析的 XmlConfig 实例。

如果您查看 XmlConfig 代码,您将看到它确实承载 JAXB 注释。如果您希望运行时镜像中不包含这些注释,您可以将 XmlConfig 实例克隆到某些 POJO 对象图中,然后使用 POJO 类替换 XmlConfig。我们将在 Replacing Classes in the Native Image 中执行此操作。

Scanning Deployments Using Jandex

如果您的扩展定义了用于标记需要处理的 bean 的注释或接口,那么您可以使用 Jandex API(Java 注释索引器和离线反射库)来定位这些 bean。下面的 TestProcessor#scanForBeans 方法展示了如何查找也实现了 IConfigConsumer 接口的用 @TestAnnotation 进行注释的 bean:

Example Jandex Usage
    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 Retrieve the index.
3 查找用 @TestAnnotation 进行注释的所有 bean。
4 确定其中哪些 bean 也有 IConfigConsumer 接口。
5 将 bean 类保存在 TestBeanBuildItem 中,以便在后面的 RUNTIME_INIT 构建步骤中使用它,该步骤将与 bean 实例交互。

Interacting With Extension Beans

您可以使用 io.quarkus.arc.runtime.BeanContainer 接口与扩展 bean 交互。以下 configureBeans 方法说明了如何与前面部分中扫描的 bean 进行交互:

Using CDI BeanContainer Interface
// 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(&#8230;&#8203;) 方法,传递包含运行时信息的配置对象。

Manage Non-CDI Service

扩展的一个常见目的是将非 CDI 感知服务集成到基于 CDI 的 Quarkus 运行时中。此任务的第一步是在 STATIC_INIT 构建步骤中加载任何需要的配置,就像我们在 Parsing Config to Objects 中所做的那样。现在我们需要使用配置创建服务实例。让我们回到 TestProcessor#parseServiceXmlConfig 方法来了解如何做到这一点。

Creating a Non-CDI Service
// 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 构建步骤中使用它。
Starting a Service

现在您已经记录了在构建阶段创建服务,您需要在引导期间记录如何在运行时启动服务。您可以使用 RUNTIME_INIT 构建步骤来完成此操作,如 TestProcessor#startRuntimeService 方法所示。

Starting/Stopping a Non-CDI Service
// 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 来表示服务启动。详情请见 Startup and Shutdown Events
5 运行时记录器检索服务实例引用并调用其 startService 方法。
6 运行时记录器使用 Quarkus ShutdownContext 注册了对 stopService 服务实例方法的调用。

可以在此处查看 RuntimeXmlConfigService 的代码:$${quarkus-base-url}/blob/main/integration-tests/test-extension/extension/runtime/src/main/java/io/quarkus/extest/runtime/RuntimeXmlConfigService.java[RuntimeXmlConfigService.java]

可在 testRuntimeXmlConfigServiceConfiguredBeanTestNativeImageIT 测试中找到用于验证 RuntimeXmlConfigService 已启动的测试用例。

Startup and Shutdown Events

Quarkus 容器支持启动和关闭生命周期事件,以通知组件容器启动和关闭。已激发组件可以观察的 CDI 事件,此示例对此进行了说明:

Observing Container Startup
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 的使用,用于在 Starting a Service 部分中注册回调以执行关闭任务。这些关闭任务将在发送 ShutdownEvent 后调用。

在已使用所有 io.quarkus.deployment.builditem.ServiceStartBuildItem 产生器后激发 StartupEvent。这意味着如果扩展中包含服务,则应用程序组件会希望在观察到 StartupEvent 时启动这些服务,则调用运行时代码以启动这些服务的构建步骤需要产生 ServiceStartBuildItem 以确保在发送 StartupEvent 之前运行该运行时代码。回想一下,我们在上一部分中看到了 ServiceStartBuildItem 的生成,此处为清楚起见而重复:

Example of Producing a 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 之前需要运行的服务启动步骤。

Register Resources for Use in Native Image

并非所有配置或资源都可以在构建时使用。如果您有运行时需要访问的类路径资源,则需要通知构建阶段需要将这些资源复制到本机映像中。这可以通过在资源包的情况下产生一个或多个 NativeImageResourceBuildItemNativeImageResourceBundleBuildItem 来完成。此 registerNativeImageResources 构建步骤中显示了此示例:

Registering Resources and ResourceBundles
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” 资源包复制到本机映像中。

Service files

如果正在使用 @28 文件,则需要将文件注册为资源,以便本机映像可以找到它们,但还需要为每个列出的类注册反射,以便它们可以在运行时实例化或检查:

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());
    }
}

Object Substitution

在构建阶段创建并传递到运行时的对象需要具有一个默认构造函数,以便它们在从构建时状态启动运行时时创建和配置。如果一个对象没有默认构造函数,那么在生成扩展制品期间,您将看到类似于以下内容的错误:

DSAPublicKey Serialization Error
	[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

有一个“@1”接口可以实现,以便告诉 Quarkus 如何处理此类类。这里显示了“@2”的示例实现:

DSAPublicKeyObjectSubstitution Example
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 序列化方法获取不带默认构造函数的对象,并创建包含必要信息以重新创建“@4”的“@3”。
2 反序列化方法使用“@5”来使用密钥工厂从其编码形式中重新创建“@6”。

一个扩展通过产生“@7”来注册此替代表现,如这个“@8”片段中所示:

Registering an Object Substitution
    @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);
    }

Replacing Classes in the Native Image

Graal SDK 支持本机映像中的类替代表现。这些示例类显示了如何用没有 JAXB 注释依赖项的版本来替换“@9”类:

Substitution of XmlConfig/XmlData Classes Example
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 + '\'' +
                '}';
    }
}

Ecosystem integration

一些扩展可能是私有的,而一些可能希望成为更广泛的 Quarkus 生态系统的一部分,即“@10”。纳入 Quarkiverse Hub 是一种方便的机制,用于处理持续测试和发布。“@11”包含用于接入您的扩展的指令。

或者,可以手动处理持续测试和发布。

Continuous testing of your extension

为了让扩展作者每天轻松地针对 Quarkus 的最新快照测试他们的扩展,Quarkus 引入了生态系统 CI 的概念。生态系统 CI “@12”包含有关如何设置 GitHub Actions 作业以利用此功能的所有详细信息, जबकि此“@13”提供了该流程的概述。

Publish your extension in registry.quarkus.io

在将您的扩展发布到“@14”之前,请确保满足以下要求:

  • 扩展的“@15”模块中的“@16”文件具有最小元数据集:

    • name

    • “@17”(除非您已经为其在“@18”元素中进行设置,这是推荐方法)

  • 您的扩展已在 Maven Central 中发布

  • 您的扩展仓库已配置为使用“@19”。

然后,您必须创建一个拉取请求,在“@22”中的“@21”目录中添加一个“@20”文件。YAML 必须具有以下结构:

group-id: <YOUR_EXTENSION_RUNTIME_GROUP_ID>
artifact-id: <YOUR_EXTENSION_RUNTIME_ARTIFACT_ID>

当您的仓库包含多个扩展时,您需要为每个单独的扩展创建单独的文件,而不是为整个仓库创建一个文件。

仅此而已。拉取请求合并后,一个计划的任务将检查 Maven Central 是否有新版本,并更新“@23”。