使用自定义 Quarkus 扩展解决问题

时不时地,我会在 Twitter 或文章中看到一些声称不理解 Quarkus 的好处的说法,例如“谁需要快速启动?”、“我的内存很多”或者“热重载有什么意义?”。

我可以写一篇文章来反驳这些观点,解释后者如何让你的开发工作流程更有效率,以及前者是如何实现后者的,即使快速启动对你来说不是最重要的。但为了这篇博文的宗旨,让我们承认这些人是完全正确的,并且这些并不是使用 Quarkus 的好理由。

那么现在怎么办?回到<在此插入你喜欢的框架>?没那么快……

Quarkus 并非通过使用暗黑魔法或懒加载技巧来实现快速启动和低内存占用,而是通过彻底重新思考 Java 应用程序的引导方式。Quarkus 的全部意义在于将尽可能多的工作移至构建时,而这个过程促使我们创建了一个框架,用于在构建时推送工作,这些工作可以在 Quarkus 扩展中加以利用。

Quarkus 扩展?听起来工作量很大?

不,真的不是。你可以非常轻松地开发自己的扩展,并且它们可以以一种非常简单的方式解决一些非同寻常的问题。

上周,我们的一位用户(嘿,Juan!)在 Zulip 上提出了这个问题

你好!我正在尝试理解如何根据某些标准查找类并将它们添加到依赖注入上下文中,例如:我想查找所有类名以“MessageTransformer”结尾的类,并将它们添加到上下文中。我想在外部库中查找这些类,所以无法为它们添加注解。

让我们看看如何通过开发自定义扩展来解决这个问题。

创建扩展

创建扩展非常简单

mvn io.quarkus:quarkus-maven-plugin:create-extension -DwithoutTests

它会询问 groupId - 让我们保留默认值 org.acme - 和一个扩展 id - 我选择了 message-transformers-as-beans

然后你可以将新扩展导入你喜欢的 IDE。

扩展结构

关于扩展有很多可以说的,但在这个博文的上下文中,我们将保持简短。该扩展由三个 Maven 模块组成

  • 父模块 - 这里没什么可看的

  • 部署模块 - 这是我们博文关注的模块

  • 运行时模块 - 在这篇博文中,我们不会修改它

让我们简单点说:部署模块是在构建时使用的,运行时模块是在运行时使用的。

在我们的例子中,我们想声明新的 bean,这是我们在构建时完成的事情,所以部署模块,我们来了!

处理器和构建步骤

如果你看一下你的 deployment 模块,你会看到一个 MessageTransformersAsBeansProcessor,你可以在其中看到一个带有 @BuildStep 注解的方法。

Quarkus 构建由这些构建步骤组成,它们遵循带有依赖注入的消费者/生产者模型。被消耗和产生的文件称为 BuildItem

自动生成的构建步骤很容易理解。它会生成一个 FeatureBuildItem,该文件将被 Quarkus 启动消耗,你将在 Quarkus 启动时显示的列表中看到扩展名称。

INFO  [io.quarkus] my-app 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.13.2.Final) started in 0.221s.
INFO  [io.quarkus] Profile prod activated.
INFO  [io.quarkus] Installed features: [cdi, message-transformers-as-beans]

Jandex 索引

现在我们已经完成了脚手架的搭建,让我们来思考一下我们想要实现的目标:我们需要在给定包中查找所有类名以 MessageTransformer 结尾的类。

Quarkus 的一个重要假设是应用程序生活在一个封闭的世界中。你不能在运行时动态地向 Quarkus 应用程序添加 jar 并期望它正常工作。

虽然这可能被视为一种限制,但它为各种可能性打开了大门,其中之一就是能够索引类及其注解以便轻松查找它们。

这个基于 Jandex 的索引是 Quarkus 引导过程的一个非常重要的部分。

Jandex 索引并不包含周围的所有类,但默认情况下,它仅限于应用程序类以及包含预构建索引或空的 META-INF/beans.xml 的依赖项。

在我们的例子中,我们想列出外部依赖项中的类,所以我们需要将它们添加到索引中。我们可以通过向 MessageTransformersAsBeansProcessor 添加一个构建步骤来轻松实现这一点

@BuildStep
IndexDependencyBuildItem indexExternalDependency() {
    return new IndexDependencyBuildItem("my.group.id", "my-artifact-id");
}

这将把 my.group.id:my-artifact-id jar 的内容添加到索引中。

声明附加 Bean

现在我们已经索引了我们的类,我们想让它们成为 CDI Bean。

这可以通过添加另一个构建步骤来实现

@BuildStep
void declareMessageTransformersAsBean(CombinedIndexBuildItem index, (1)
        BuildProducer<AdditionalBeanBuildItem> additionalBeans) { (2)
    List<String> messageTransformers = index.getIndex().getKnownClasses().stream() (3)
            .filter(ci -> !Modifier.isAbstract(ci.flags())) (4)
            .map(ci -> ci.name().toString()) (5)
            .filter(c -> c.startsWith("my.package.")) (6)
            .filter(c -> c.endsWith("MessageTransformer")) (7)
            .collect(Collectors.toList());

    additionalBeans.produce(new AdditionalBeanBuildItem.Builder() (8)
            .addBeanClasses(messageTransformers)
            .setUnremovable() (9)
            .setDefaultScope(DotNames.APPLICATION_SCOPED) (10)
            .build());
}
1 消耗 Jandex 索引
2 注入附加 Bean 生成器
3 从索引中获取所有已知类
4 过滤掉抽象类
5 获取类的 FQCN
6 只保留目标根包中的类
7 只保留 MessageTransformer
8 生成一个 AdditionalBeanBuildItem
9 使 Bean 不可移除,以防止 ArC 在仅通过编程方式使用 Bean 时将其移除
10 将默认范围设置为 @ApplicationScoped - 可以是任何你喜欢的 CDI 范围

通过这个构建步骤,我们根包 my.package 中所有以 MessageTransformer 结尾的非抽象类都将被设置为 @ApplicationScoped CDI Bean。

锦上添花的是,所有这些工作都在构建时完成,你无需在运行时扫描整个类路径。

通常,我们通过接口、超类或注解在索引中查找类。这比遍历整个索引并按名称过滤更不易出错且速度更快。

但这里的重点是满足用户的限制,并且无法调整外部依赖项。

就是这样了,伙计们!

显然,这是一个非常简单的例子,你可以使用 Quarkus 扩展做更多复杂的事情。

但这篇文章的重点是证明你可以轻松地利用我们的扩展框架来解决现实生活中的问题。而且在约 10 分钟的编码时间内,我们解决了问题

下一个是什么?

加入我们

我们非常重视您的反馈,所以请报告错误,提出改进建议…… 让我们一起构建伟大的东西!

如果您是 Quarkus 用户或只是好奇,请不要害羞,加入我们热情的社区