通往 CDI 兼容性的道路

从 Quarkus 最早的日子起,那些现在已被遗忘的幸福迷雾所笼罩,幸存者们只在喝了几杯啤酒后才会谈论它们,依赖注入容器就是这个设想框架不可或缺的一部分。而且不是任何一种依赖注入容器——而是一个CDI实现

最初,使用的 CDI 实现是 Weld。很快,CDI 工作背后(最终演变成 Quarkus)的 Masterminds 和 Deep Thoughts,Martin Kouba 和 Stuart Douglas,意识到 Weld 无法充分释放面向构建时框架所隐藏的全部潜力。因此,ArC 应运而生。

当我第一次听说 ArC 时,我想,这肯定意味着缩减版 CDI,尤其是有这样的首字母大写!唉,我大错特错了。它指的是一项我出于健康和安全的考虑而永远不可能参与的活动:电弧焊接。(这确实是对 Weld 的一种回溯。这里有些人真聪明!)

电弧焊接

ArC 的起点是一个重要的架构约束,与当时所有其他现有的 CDI 实现都有显著不同:它应该在应用程序构建期间执行繁重的工作。其中,包括整个 Bean 的发现过程。如果您熟悉 AtInject 及其各种实现,这个概念并不令人惊讶。例如,Guice 是一个流行的依赖注入容器,它在应用程序运行时完成所有工作,而 Dagger 是一个流行的替代方案,它在构建时预先计算依赖关系连接。

对于 CDI 来说,情况并非如此简单。CDI 2.0,当时的最新版本,包含了一些最终阻止在构建时运行 Bean 发现的功能;最值得注意的是Portable Extensions API。为了能够执行 Portable Extensions,您需要一个正在运行的 CDI 容器(以传递事件或使用 BeanManager API),您需要能够反射应用程序类(Annotated* 类型直接暴露 java.lang.reflect 类型),并且您需要支持持有各种状态的 Portable Extension 实例(包括启动的线程或打开的套接字)。

还有一些难以实现的特性,比如高效的 Bean 元数据存储和运行时访问,或者动态查找,但那些都是仅工作。Portable Extensions 根本上是不可能的。

ArC 做出了一个显而易见的选择:它将不支持 Portable Extensions,它不会成为一个完全兼容的 CDI 实现,它也不会通过 CDI TCK 进行验证。这个决定为修剪掉一些古老但未被广泛使用或被认为对当代软件世界不够重要的 CDI 功能打开了大门:对话、特化、钝化、使用 @Interceptors 注解绑定的拦截器、使用 beans.xml 进行启用等等。其他一些功能也不适合构建时方法:显式 Bean 归档的概念、InterceptionFactory,或者 BeanManager API 的某些部分。这可能听起来像一个长长的列表,但实际上,结果是一个完全“恰到好处”的 CDI 实现,它允许运行大量现有的基于 CDI 的库和框架,只需编写一个 Quarkus 特定的集成。

一切都很美好,粉红色的独角兽在鲜花盛开的草地上快乐地漫步,双彩虹在晴朗的蓝天下闪耀,开发者们在Kuberspace 的各个角落开发着强大的微服务。随着时间的推移,一些最初被省略的功能,如装饰器,也得到了实现。

当然,有些人试图抱怨 Quarkus 声称实现了 CDI,但实际上并没有,因为它没有通过 TCK,但我们不必为此烦恼。这些功能的缺失已被清楚地记录在案,而且自从远古以来,宏伟的排除列表一直是 CDI 世界的崇高传统。

CDI Lite

ArC 相当成功地证明了 CDI 中确实隐藏着一颗小种子,等待着被浇水施肥,等待着生长、绽放,并向世界展示 CDI 不必只是“guiced”(由 Guice 驱动);它也可以是“daggered”(由 Dagger 驱动)。

Red Hat 的一小群工程师聚集在一起,试图策划一个阴谋:这能否成为 CDI 本身的一部分?这个想法已经在外部和内部进行了相当广泛的讨论。幸运的是,CDI 是在 Red Hat 设计的,所以我们拥有所有专家,并且第一个具体的想法相对较快地公布了。

这些讨论中的很大一部分围绕着 Portable Extensions。如前所述,它们无法在构建时支持,因此我们很早就知道必须设计一个新的扩展 API。(这时,您本人登场了,计划在落幕前都不离开。)我们为 API 的各个方面制作了几个原型,包括一个新的语言模型,并最终发布了一个提案(当心,这篇文章现在已经严重过时了!)。我们称之为Build Compatible Extensions,以突出与 Portable Extensions 的鲜明对比:这个 API 可以在构建时和运行时实现。

发布该提案产生了两个影响。首先,它表明沟通是困难的,在线沟通更困难,而且非母语的在线沟通非常痛苦。其次,它表明我们有认真的意愿去做必要的工作。而且不只是我们——一些 Oracle 的人也出现了,最著名的是 Micronaut 功臣 Graeme Rocher。在随后的几年里,核心 CDI 规范被重构为 CDI Lite 和 CDI Full,Build Compatible Extensions API 被合并(为此,我实际上实现了两个原型,一个在 ArC 中,另一个作为 Weld 的 Portable Extension),CDI TCK 被拆分以支持仅测试 CDI Lite 实现,等等。

最终,作为 Jakarta EE 10 的一部分,CDI 4.0 发布了,其特点是 Lite 规范,它成为了 Jakarta EE Core Profile 的基石,而 Jakarta EE Core Profile 又成为了 MicroProfile 的基石。

故事结束,回家?没那么快。

兼容实现

现在我们有了 CDI Lite 规范,我们有实现吗?当然,所有现有的 CDI 实现都可以相对轻松地成为 CDI Full 实现;最困难的部分是实现新的扩展 API,这可以通过 Portable Extension 来实现。但是有新的实现吗?ArC 现在是 CDI Lite 的实现吗?它终于通过 TCK 验证了吗?

我们自然希望 ArC 实现 CDI Lite,但不仅仅是 ArC。Eclipse Open DI 项目也致力于成为 CDI Lite 的实现,它构建在 Micronaut 框架之上。我无法代表那个项目说话,但我可以说,与 ODI 背后的优秀人才一起致力于 CDI Lite 规范是一次很棒的经历!

现在,就 ArC 而言,显然还需要更多工作。幸运的是,由于之前的原型工作(Arquillian 是 CDI TCK 所依赖的测试框架),我已有了一个 ArC 的 Arquillian 适配器,并且其他相关的 TCK 非常容易嵌入。开始运行它们并不难:AtInject TCK、CDI Lang Model TCK 和 CDI Lite TCK。我们开始使用独立的 ArC 运行 TCK,以使工作更轻松、更快。CDI Lang Model TCK 始终通过,因为我与规范一起开发了实现,而让 AtInject TCK 通过也花费了不多的时间(主要是实现对被覆盖方法的精确解析)。CDI Lite TCK 显然是最复杂的;起初,我们大约有 2/3 的测试通过,其余 1/3 因各种原因失败。

在 Quarkus 2.16 开发周期中,我创建了一个初始排除列表,并开始缩小差距。有段时间,我们不得不使用一个额外的仓库,直到 Quarkus 从 javax 依赖迁移到 jakarta,但这设置起来相当简单。Arquillian 适配器需要改进才能正确实现 CDI 类型发现规则(因为 ArC 将大部分类型发现留给集成器)。ArC 缺少许多验证,我们添加了它们。我们甚至实现了一些其他功能。当独立的 ArC 通过 TCK 时,用完整的 Quarkus 运行它们也花费不了多少时间。总的来说,这需要 26 个 pull request 和 109 个 commit,历时四到五个月。

不幸的是,我们还必须创建一个严格模式。ArC 在 CDI 规范之上还有一些可用性改进,其中一些违反了规范规则。我们建议用户使用包含这些改进的默认模式,但我们也希望有一个选项可以关闭这些改进,以供那些更看重规范兼容性的用户使用。

而且,由于我们和所有体面的程序员一样懒惰,运行 TCK 已自动化为 Quarkus Maven 构建的一部分(这意味着它们也会在所有触及 ArC 的 Quarkus pull request 上运行)。如果您想自己尝试,只需要很少的手动工作。

  1. 克隆 Quarkus 仓库,如果您还没有的话

    git clone https://github.com/quarkusio/quarkus.git
  2. 构建 Quarkus

    cd quarkus
    mvn -Dquickly
  3. 运行 AtInject TCK

    cd tcks/jakarta-atinject
    mvn clean verify
  4. 运行 CDI Lang Model TCK

    cd ../jakarta-cdi-lang-model
    mvn clean verify
  5. 运行 CDI Lite TCK

    cd ../jakarta-cdi
    mvn clean verify

如果一切顺利,您应该会看到以下输出。

对于 AtInject

Running io.quarkus.tck.atinject.AtInjectTest
...
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

对于 CDI Lang Model

Running io.quarkus.tck.cdi.lang.model.LangModelTest
...
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

对于 CDI Lite

Running TestSuite
...
Tests run: 717, Failures: 0, Errors: 0, Skipped: 0

好了,各位!

我很高兴地宣布,Quarkus 3.2 成功通过了 AtInject TCK、CDI Lang Model TCK 和 CDI Lite TCK,因此成为 CDI Lite 的兼容实现。

我还要衷心感谢我们驻场的 CDI 专家 Martin Kouba 和 Matěj Novotný,感谢他们欢迎我并与我分享他们在这方面的深厚知识。我主要努力不破坏他们的代码。(我想,随着时间的推移,这也成了我的代码。糟糕!)