编辑此页面

类加载参考

本文档解释了 Quarkus 类加载架构。 它适用于扩展作者和想要准确了解 Quarkus 如何工作的 高级用户。

Quarkus 类加载架构略有不同,具体取决于应用程序的运行模式。

当使用 fast-jar 包类型(这是默认设置)运行生产应用程序时,几乎所有依赖项都通过 io.quarkus.bootstrap.runner.RunnerClassLoader 加载,该类在构建时对类进行索引,而一小部分依赖项是从系统类加载器加载的。

当使用 legacy-jar 包类型运行生产应用程序时,所有内容都加载到系统类加载器中,因此它是一个完全扁平的类路径。

扁平类路径策略也用于 GraalVM 本机镜像,因为 GraalVM 实际上不支持多个类加载器。

对于所有其他用例(例如,测试、开发模式和构建应用程序),Quarkus 使用以下部分中概述的类加载架构。

引导 Quarkus

所有 Quarkus 应用程序都由 independent-projects/bootstrap 模块中的 QuarkusBootstrap 类创建。 此类用于解析 Quarkus 应用程序所需的所有相关依赖项(部署和运行时)。 此过程的最终结果是一个 CuratedApplication,其中包含应用程序的所有类加载信息。

然后,可以使用 CuratedApplication 创建 AugmentAction 实例,该实例可以创建生产应用程序并启动/重新启动运行时应用程序。 此应用程序实例存在于一个隔离的类加载器中,无需在类路径上放置任何 Quarkus 部署类,因为 curate 过程会为您解析它们。

无论 Quarkus 如何启动,此引导过程都应相同,只是传入的参数不同。

当前运行模式

目前,我们有以下引导 Quarkus 的用例

  • Maven 创建生产应用程序

  • Maven 开发模式

  • Gradle 创建生产应用程序

  • Gradle 开发模式

  • QuarkusTest(Maven、Gradle 和 IDE)

  • QuarkusUnitTest(Maven、Gradle 和 IDE)

  • QuarkusDevModeTest(Maven、Gradle 和 IDE)

  • Arquillian 适配器

此重构的目标之一是使所有这些不同的运行模式以基本相同的方式引导 Quarkus。

关于转换器安全性的说明

如果可以在转换器准备好之前在类加载器中加载类,则该类加载器被认为是“转换器安全”的。 一旦加载了一个类,就无法更改它,因此如果在准备转换器之前加载了一个类,这将阻止转换工作。 在转换器安全的类加载器中加载类不会阻止转换,因为加载的类在运行时不使用。

类加载器实现

Quarkus 有许多类加载器。 这显示了它们之间的关系

Quarkus ClassLoader hierarchy

以下是每个类加载器的角色

基本类加载器

这通常是普通的 JVM 系统类加载器。 在某些环境中,例如 Maven,它可能有所不同。 此类加载器用于加载引导类,其他类加载器实例会将 JDK 类的加载委托给它。

增强类加载器

这将加载所有 -deployment 工件及其依赖项,以及其他用户依赖项。 它不加载应用程序根目录或任何热部署代码。 此类加载器是持久的,即使应用程序重新启动,它也会保留(这就是为什么它无法加载可能被热部署的应用程序类)。 它的父类是基本类加载器,并且是转换器安全的。

目前,可以将其配置为委托给基本类加载器,但计划是取消此选项,并始终将其作为隔离的类加载器。 使其成为隔离的类加载器很复杂,因为它意味着所有构建器类都是隔离的,这意味着想要自定义构建链的用例会稍微复杂一些。

部署类加载器

这可以加载所有应用程序类。 它的父类是增强类加载器,因此它也可以加载所有部署类。

此类加载器是非持久的,它将在应用程序启动时重新创建,并且是隔离的。 此类加载器是在运行构建步骤时使用的上下文类加载器。 它也是转换器安全的。

基本运行时类加载器

这将加载所有运行时扩展依赖项,以及其他用户依赖项(请注意,这可能包括增强类加载器也加载的类的重复副本)。 它不加载应用程序根目录或任何热部署代码。 此类加载器是持久的,即使应用程序重新启动,它也会保留(这就是为什么它无法加载可能被热部署的应用程序类)。 它的父类是基本类加载器。

这将加载不可热重载的代码,但它确实支持转换(尽管一旦加载了类,这种转换就不再可能)。 这意味着只有在第一次应用程序启动时注册的转换器才会生效,但是由于这些转换器应该是幂等的,因此不会引起问题。 此处可能需要的转换的一个示例是打包在外部 jar 中的 Panache 实体。 此类需要进行转换才能实现其静态方法,但是此转换仅发生一次,因此重新启动会使用在第一次启动时创建的类的副本。

此类加载器与增强和部署类加载器隔离。 这意味着不可能在部署端的静态字段中设置值,并期望在运行时读取它。 这允许开发和测试应用程序的行为更像生产应用程序(生产应用程序是隔离的,因为它们在一个全新的 JVM 中运行)。

这也意味着运行时版本可以链接到一组不同的依赖项,例如,在部署时使用的 hibernate 版本可能希望包含 ByteBuddy,而在运行时使用的版本则不需要。

运行时类加载器

此类加载器用于加载应用程序类和其他可热部署资源。 它的父类是基本运行时类加载器,并且在应用程序重新启动时重新创建。

隔离的类加载器

运行时类加载器始终是隔离的。 这意味着它将拥有来自已解析依赖项列表的几乎每个类自己的副本。 此例外是

  • JDK 类

  • 扩展标记为父类优先的工件中的类(稍后会详细介绍)。

父类优先依赖项

有些类不应以隔离的方式加载,而应始终由系统类加载器(或负责引导 Quarkus 的任何类加载器)加载。 大多数扩展不需要担心这一点,但是有些情况下这是必需的

  • 一些与日志相关的类,因为日志必须由系统类加载器加载

  • Quarkus 引导本身

如果需要,可以在 quarkus-extension-maven-plugin 中配置。 请注意,如果您将依赖项标记为父类优先,那么它的所有依赖项也必须是父类优先,否则可能会发生 LinkageError

<plugin>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-extension-maven-plugin</artifactId>
    <configuration>
        <parentFirstArtifacts>
            <parentFirstArtifact>io.quarkus:quarkus-bootstrap-core</parentFirstArtifact>
            <parentFirstArtifact>io.quarkus:quarkus-development-mode-spi</parentFirstArtifact>
            <parentFirstArtifact>org.jboss.logmanager:jboss-logmanager</parentFirstArtifact>
            <parentFirstArtifact>org.jboss.logging:jboss-logging</parentFirstArtifact>
            <parentFirstArtifact>org.ow2.asm:asm</parentFirstArtifact>
        </parentFirstArtifacts>
    </configuration>
</plugin>

禁止的依赖项

有些依赖项我们可以确定我们不想要。 当依赖项的名称发生更改时,通常会发生这种情况(例如,smallrye-config 将组从 org.smallrye 更改为 org.smallrye.configjavaxjakarta 重命名)。 这可能会导致问题,因为如果这些工件最终出现在依赖项树中,则可能会加载与 Quarkus 不兼容的过时类。 为了解决这个问题,扩展可以指定永远不应该加载的工件。 这是通过修改 pom 中的 quarkus-extension-maven-plugin 配置(这会生成 quarkus-extension.properties 文件)来完成的。 只需添加一个 excludedArtifacts 部分,如下所示

<plugin>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-extension-maven-plugin</artifactId>
    <configuration>
        <excludedArtifacts>
            <excludedArtifact>io.smallrye:smallrye-config</excludedArtifact>
            <excludedArtifact>javax.enterprise:cdi-api</excludedArtifact>
        </excludedArtifacts>
    </configuration>
</plugin>

只有当扩展依赖于这些工件的较新版本时才应这样做。 如果扩展没有将替换工件作为依赖项引入,那么应用程序需要的类最终可能会丢失。

配置类加载

可以在开发和测试模式下配置类加载的某些方面。 这可以使用 application.properties 完成。 请注意,类加载配置与普通配置不同,因为它不使用标准的 Quarkus 配置机制(因为它需要太早),因此仅支持 application.properties。 支持以下选项。

构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖

配置属性

类型

默认

以父类优先方式加载的工件。 这可用于解决给定的类需要由系统类加载器加载的问题。 请注意,如果您使库成为父类优先,那么它的所有依赖项通常也应该是父类优先的。

工件应配置为以逗号分隔的工件 ID 列表,其中组、工件 ID 和可选分类符用冒号分隔。

此配置属性只能在 application.properties 中设置

环境变量: QUARKUS_CLASS_LOADING_PARENT_FIRST_ARTIFACTS

显示更多

字符串列表

在开发模式下的运行时类加载器中加载的工件,因此它们将在更改时被删除和重新创建。

这是一个高级选项,只有在库在重新加载之间保持过时状态时才应使用它。 请注意,如果您使用此选项,则任何依赖于所列库的库也需要是可重新加载的。

此设置对生产构建没有影响。

工件应配置为以逗号分隔的工件 ID 列表,其中组、工件 ID 和可选分类符用冒号分隔。

此配置属性只能在 application.properties 中设置

环境变量: QUARKUS_CLASS_LOADING_RELOADABLE_ARTIFACTS

显示更多

字符串

类加载器永远不会加载的工件,并且不会打包到最终应用程序中。 这允许您显式地从应用程序中删除工件,即使它们可能存在于类路径上。

环境变量: QUARKUS_CLASS_LOADING_REMOVED_ARTIFACTS

显示更多

字符串列表

应从依赖项中删除/隐藏的资源。

这允许从依赖项中删除类和其他资源,因此应用程序无法访问它们。 这是一个从工件 ID(以 group:artifact 形式)到要删除的资源列表的映射。

在开发和测试模式下运行时,这些资源从类加载器中隐藏,在生产模式下运行时,这些文件将从包含它们的 jar 文件中删除。

请注意,如果要删除一个类,则需要指定类文件名。 例如,要删除 com.acme.Foo,您将指定 com/acme/Foo.class

请注意,由于技术原因,在使用 JBang 运行时不支持此功能。

环境变量: QUARKUS_CLASS_LOADING_REMOVED_RESOURCES__GROUP_ID_ARTIFACT_ID_

显示更多

Map<String,Set<String>>

从依赖项中隐藏/删除类和资源

可以从依赖项中隐藏/删除类和资源。 这是一个高级选项,但在某些时候可能很有用。 这是通过 quarkus.class-loading.removed-resources 配置键完成的,例如

quarkus.class-loading.removed-resources."io.quarkus\:quarkus-integration-test-shared-library"=io/quarkus/it/shared/RemovedResource.class

这将从 io.quarkus:quarkus-integration-test-shared-library 工件中删除 RemovedResource.class 文件。

即使此选项是一个类加载选项,它也会影响生成的应用程序,因此在创建应用程序时,无法访问删除的资源。

读取类字节码

使用正确的 ClassLoader 很重要。 推荐的方法是通过调用 Thread.currentThread().getContextClassLoader() 方法来获取它。

示例

@BuildStep
GeneratedClassBuildItem instrument(final CombinedIndexBuildItem index) {
    final String classname = "com.example.SomeClass";
    final ClassLoader cl = Thread.currentThread().getContextClassLoader();
    final byte[] originalBytecode = IoUtil.readClassAsBytes(cl, classname);
    final byte[] enhancedBytecode = ... // class instrumentation from originalBytecode
    return new GeneratedClassBuildItem(true, classname, enhancedBytecode));
}

相关内容