Quarkus 依赖注入

Quarkus ArC 是一个面向构建时的依赖注入,基于 CDI 2.0。在这篇博客中,我们将解释它与规范的关系,并描述构建时处理设计的优点和缺点。

兼容性

在依赖注入方面,没有必要重新发明轮子。有许多框架试图解决类似的问题。一年前,我们做出了一个设计决定,在 CDI 之上构建 Quarkus DI。我们有几个很好的理由选择 CDI:

  1. CDI 是一个成熟且经过验证的组件模型

  2. 我们在 Red Hat 拥有近十年的开发 Weld - CDI 参考实现 的经验。

  3. CDI API 构建在 javax.inject 之上,因此从任何与 @Inject 兼容的 DI 框架迁移应该很容易。

我们的主要目标是实现一个与 CDI 兼容的超音速构建时定向 DI 解决方案。这将允许用户继续在他们的应用程序中使用 CDI,同时也利用 Quarkus 的构建时优化。然而,ArC 并不是一个经过 TCK 验证的完整 CDI 实现 - 请参阅 支持的功能 列表和 限制 列表。

构建时处理的优点和缺点

快速失败

Bean 和依赖项在构建过程中得到验证,因此您的应用程序永远不会因生产中常见的 `AmbiguousResolutionException` 或 `UnsatisfiedResolutionException` 等问题而失败。

即时启动

当应用程序启动时,ArC 仅加载所有元数据并初始化一些内部结构。无需再次分析应用程序类。这意味着启动开销可以忽略不计。

这适用于 GraalVM 和 OpenJDK HotSpot 运行时。

最小化的运行时

在 Quarkus 0.19 中,ArC 加上集成运行时包含 72 个类,在 jar 文件中占用约 140 KB。 Weld 3.1.1 (CDI 参考实现) 核心大约有 1200 个类,jar 文件大小约为 2 MB。换句话说,ArC 运行时在类数量和 jar 体积方面大约占 Weld 运行时的 7%。

优化的代码路径和元数据结构

在生成元数据类时,ArC 有时可以产生更精简、更智能的逻辑,因为它已经分析了整个应用程序。这是我们希望开发和改进 ArC 的领域之一。

扩展点

不幸的是,CDI 可移植扩展本质上是运行时构造,因此无法在 Quarkus 中完全支持。实际上,目前所有 CDI 扩展都被忽略了。尽管如此,大多数功能都可以通过 Quarkus 扩展 来实现。鼓励 CDI 扩展来泛化代码,并在可能的情况下提供 Quarkus 扩展以充分利用构建时元数据处理。

非标准功能

ArC 不仅限于标准,我们还在不断寻找超越和扩展可能性。以下是 Quarkus DI 提供的一些非标准功能的示例。

限定注入字段

通常,如果您声明一个注入字段,您总是需要使用 @Inject 和可选的必需限定符。

  @Inject
  @ConfigProperty(name = "cool")
  String coolProperty;

在 Quarkus 中,如果注入字段至少声明了一个限定符,您可以完全省略 @Inject 注释。

  @ConfigProperty(name = "cool")
  String coolProperty;
构造函数和方法注入仍然需要 @Inject

简化的构造函数注入

在 CDI 中,一个正常作用域的 bean 必须始终声明一个无参构造函数(此构造函数通常由编译器生成,除非您声明任何其他构造函数)。然而,这个要求使得构造函数注入变得复杂——您需要提供一个虚拟的无参构造函数才能在 CDI 中工作。

@ApplicationScoped
public class MyCoolService {

  private SimpleProcessor processor;

  MyCoolService() { // dummy constructor needed
  }

  @Inject // constructor injection
  MyCoolService(SimpleProcessor processor) {
    this.processor = processor;
  }
}

在 Quarkus 中,无需为正常作用域的 bean 声明虚拟构造函数——它们会自动生成。此外,如果只有一个构造函数,则不需要 @Inject

@ApplicationScoped
public class MyCoolService {

  private SimpleProcessor processor;

  MyCoolService(SimpleProcessor processor) {
    this.processor = processor;
  }
}
如果 bean 类扩展了另一个未声明无参构造函数的类,我们不会自动生成无参构造函数。

默认 Bean

CDI 有一个称为 替代 的功能。简单来说,用 @Alternative@Priority 注释的 Bean 在类型安全的解析过程中优先于非替代 Bean。

class MyBean {
}

@Alternative
@Priority(1)
class MyAlternativeBean extends MyBean {
}

@Inject // MyAlternativeBean wins and is injected!
MyBean bean;

那么,如果用户想覆盖由库/扩展定义的 Bean 怎么办?该 Bean 必须被标记为 CDI @Alternative,并使用 @Priority 注释启用。有更简单的方法吗?是的,有。您可以使用一个称为“默认 Bean”的非标准功能。在这种情况下,可以覆盖的 Bean 应使用 @io.quarkus.arc.DefaultBean 进行注释。就是这样。

@DefaultBean
class MyBean {
}

class MyOwnBean extends MyBean {
}

@Inject // MyOwnBean wins and is injected!
MyBean bean;

移除未使用的 Bean

GraalVM 原生镜像在移除应用程序无法访问的所有类方面做得很好。然而,有时检查可达性是不够的。有时框架本身必须决定是否需要某个组件。在标准的 CDI 中,无论是否需要,所有 Bean 都会被容器保留。

假设我们有一个 Bean 类 org.acme.Foo。这个 Bean 类导入并使用了许多不同的类。它被注释为 @ApplicationScoped,因此 Quarkus 需要生成一个 Bean 元数据类和一个客户端代理,并在应用程序启动时注册此元数据。但是,如果没有人使用这个 Bean 呢?我们仍然会保留对生成的元数据的引用,以及 Bean 类本身及其依赖项。换句话说,所有这些类都是可达的。

Quarkus 默认尝试在构建过程中移除所有 **未使用的 Bean**。这有助于减少生成的类数量以及运行时所需的内存量。但是我们如何实际检测未使用的 Bean 呢?规则在 参考指南 中描述,但简单来说:如果一个 Bean 没有在任何地方注入,并且无法通过任何其他标准方式(例如观察者通知)到达,它就会被移除。此外,用户可以通过用 @io.quarkus.arc.Unremovable 注释 Bean 类来指示容器不要移除 Bean。最后,可以通过使用 quarkus.arc.remove-unused-beans 属性来禁用和微调此优化。

此功能也适用于 JVM 模式。