Contexts and Dependency Injection

基于 Quarkus 的 DI 解决方案(也称为 ArC)基于 Jakarta Contexts and Dependency Injection 4.1 规范。它实现了 CDI Lite 规范,在此基础上做了一些改进,并通过了 CDI Lite TCK。它没有实现完整的 CDI。另请参阅 the list of supported features and limitations

如果您不熟悉 CDI,我们建议您先阅读 Introduction to CDI

大多数现有 CDI 代码应该都能正常工作,但有一些小的差异源于 Quarkus 架构和目标。

Bean Discovery

CDI 中的 Bean 发现是一个复杂的过程,它涉及底层模块架构的传统部署结构和可访问性要求。但是,Quarkus 正在使用 simplified bean discovery。只有一个带有 bean discovery mode annotated 的 bean 存档,并且没有可见性边界。

bean 存档是由以下内容合成的:

  • the application classes,

  • 包含 beans.xml 描述符(忽略内容)的依赖项,

  • 包含 Jandex 索引的依赖项 - META-INF/jandex.idx

  • application.properties 中的 quarkus.index-dependency 引用的依赖项,

  • and Quarkus integration code.

没有 bean defining annotation 的 Bean 类不会被发现。这种行为是由 CDI 定义的。但是,即使声明的类未用 bean 定义注释作注释,也会发现生产者方法、字段和观察者方法(此行为与 CDI 中定义的行为不同)。事实上,声明 bean 类被视为用 @Dependent 作了注释。

Quarkus 扩展可能声明其他发现规则。例如,即使声明的类未用 bean 定义注释作注释,也会注册 @Scheduled 业务方法。

How to Generate a Jandex Index

为依赖项附上 Jandex 索引,系统会自动扫描是否存在 bean。要生成索引,只需将以下插件添加到构建文件中:

Maven
<build>
  <plugins>
    <plugin>
      <groupId>io.smallrye</groupId>
      <artifactId>jandex-maven-plugin</artifactId>
      <version>{jandex-version}</version>
      <executions>
        <execution>
          <id>make-index</id>
          <goals>
            <goal>jandex</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
Gradle (Groovy DSL)
plugins {
    id 'org.kordamp.gradle.jandex' version '{jandex-gradle-plugin-version}'
}

您可以在 Gradle Plugin Portal 中找到最新插件版本。

Gradle (Kotlin DSL)
plugins {
    id("org.kordamp.gradle.jandex") version "{jandex-gradle-plugin-version}"
}

您可以在 Gradle Plugin Portal 中找到最新插件版本。

如果您无法更改依赖项,您仍然可以通过向 application.properties 中添加 quarkus.index-dependency 条目来为其建立索引:

quarkus.index-dependency.<name>.group-id=
quarkus.index-dependency.<name>.artifact-id=(this one is optional)
quarkus.index-dependency.<name>.classifier=(this one is optional)

如果没有指定 artifact-id,则会为具有指定 group-id 的所有依赖项建立索引。

例如,以下条目可确保对 org.acme:acme-api 依赖项进行索引:

Example application.properties
quarkus.index-dependency.acme.group-id=org.acme 1
quarkus.index-dependency.acme.artifact-id=acme-api 2
1 Value 是由名称 acme 识别的依赖项的组 ID。
2 Value 是由名称 acme 识别的依赖项的构件 ID。

How To Exclude Types and Dependencies from Discovery

第三方库中的某些 bean 在 Quarkus 中可能无法正常工作。一个典型的示例是注入可移植扩展的 bean。在这种情况下,它有可能从 bean 发现中排除类型和依赖项。quarkus.arc.exclude-types 属性接受一个字符串值列表,用于匹配应该被排除的类。

Table 1. Value Examples

Value

Description

org.acme.Foo

匹配类的完全限定名称

org.acme.*

匹配具有包 org.acme 的类

org.acme.**

匹配以 org.acme 开头的包中的类

Bar

匹配类的简单名称

Example application.properties
quarkus.arc.exclude-types=org.acme.Foo,org.acme.*,Bar 123
1 Exclude the type org.acme.Foo.
2 org.acme 包中排除所有类型。
3 排除简单名称为 Bar 的所有类型

也可以排除本应扫描的 bean 依赖项构件。例如,因为它包含一个 beans.xml 描述符。

Example application.properties
quarkus.arc.exclude-dependency.acme.group-id=org.acme 1
quarkus.arc.exclude-dependency.acme.artifact-id=acme-services 2
1 Value 是由名称 acme 识别的依赖项的组 ID。
2 Value 是由名称 acme 识别的依赖项的构件 ID。

Native Executables and Private Members

Quarkus 使用 GraalVM 构建一个本地可执行文件。GraalVM 的一个限制是使用 Reflection。支持反射操作,但所有相关成员必须显式地用于反射注册。这些注册会导致更大的本地可执行文件。

如果 Quarkus DI 需要访问私有成员,则它 has to use reflection。这就是鼓励 Quarkus 用户在 bean 中 not to use private members 的原因。这涉及注入字段、构造函数和初始化程序、观察器方法、生产者方法和字段、处置程序和拦截器方法。

如何避免使用私有成员?您可以使用 package-private 修饰符:

@ApplicationScoped
public class CounterBean {

    @Inject
    CounterService counterService; 1

    void onMessage(@Observes Event msg) { 2
    }
}
1 A package-private injection field.
2 A package-private observer method.

或构造函数注入:

@ApplicationScoped
public class CounterBean {

    private CounterService service;

    CounterBean(CounterService service) { 1
      this.service = service;
    }
}
1 在这个特殊情况下,package-private 构造函数注入 @Inject 是可选的。

[id="supported_features"][id="limitations"] Supported Features and Limitations

CDI Lite 规范得到充分支持。CDI Full 中的以下功能也受支持:

  • Decorators

    • 不支持内置 bean 的装饰,例如 Event

  • BeanManager

    • 除了 BeanContainer 方法外,还支持以下方法: getInjectableReference()resolveDecorators()

  • @SessionScoped

    • 仅适用于 Undertow 扩展;有关详细信息,请参见 here

method invokers 实现支持异步方法。下列方法被认为是异步的,并且仅在异步操作完成时才销毁 @Dependent 实例:

  • 声明返回类型为 CompletionStage、`Uni`或 `Multi`的方法

这些附加特性未被 CDI Lite TCK 涵盖。

Non-standard Features

Eager Instantiation of Beans

Lazy By Default

默认情况下,CDI bean 在需要时延迟创建。具体“需要”的含义取决于 bean 的范围。

  • 当在注入实例(根据规范为上下文引用)上调用方法时,需要 normal scoped bean(@ApplicationScoped、`@RequestScoped`等)。换句话说,注入一个正常范围的 bean 是不够的,因为会注入一个 client proxy,而不是 bean 的上下文实例。

  • 注入时将创建一个 bean with a pseudo-scope(@Dependent`和 `@Singleton)。

Lazy Instantiation Example
@Singleton // => pseudo-scope
class AmazingService {
  String ping() {
    return "amazing";
  }
}

@ApplicationScoped // => normal scope
class CoolService {
  String ping() {
    return "cool";
  }
}

@Path("/ping")
public class PingResource {

  @Inject
  AmazingService s1; 1

  @Inject
  CoolService s2; 2

  @GET
  public String ping() {
    return s1.ping() + s2.ping(); 3
  }
}
1 注入将触发 `AmazingService`的实例化。
2 注入本身不会导致 `CoolService`的实例化。将注入一个客户端代理。
3 在注入代理上的第一次调用会触发 `CoolService`的实例化。

Startup Event

但是,如果你确实需要立即实例化 bean,你可以:

  • 声明 `StartupEvent`的观察者——在这种情况下,bean 的范围无关紧要:[source, java]

@ApplicationScoped
class CoolService {
  void startup(@Observes StartupEvent event) { 1
  }
}
1 将在启动期间创建一个 `CoolService`来为观察者方法调用提供服务。
  • 在 `StartupEvent`的观察者中使用 bean——必须使用正常范围的 bean,如 Lazy By Default中所述:[source, java]

@Dependent
class MyBeanStarter {

  void startup(@Observes StartupEvent event, AmazingService amazing, CoolService cool) { 1
    cool.toString(); 2
  }
}
1 将在注入期间创建 AmazingService
2 `CoolService`是一个正常范围的 bean,因此我们必须在注入代理上调用一个方法来强制实例化。
  • 使用 `@io.quarkus.runtime.Startup`对 bean 进行注释,如 Startup annotation中所述:[source, java]

@Startup (1)
@ApplicationScoped
public class EagerAppBean {

   private final String name;

   EagerAppBean(NameGenerator generator) { (2)
     this.name = generator.createName();
   }
}
1 对于用 @Startup 注释的每个 Bean,都会生成一个 StartupEvent 的综合观察者。会使用默认优先级。
2 当应用程序启动时,会调用 Bean 构造函数,并将生成的上下文实例存储在应用程序上下文中。

鼓励 Quarkus 用户始终优先使用 @Observes StartupEvent`而不是 `@Initialized(ApplicationScoped.class),如 Application Initialization and Termination指南中所述。

Request Context Lifecycle

请求上下文也处于活动状态:

  • 在同步观察器方法通知期间。

请求上下文已销毁:

  • 如果在通知开始时,事件的观察器通知没有完成,则执行此操作。

当为观察器通知初始化请求上下文时,将触发带有限定符 @Initialized(RequestScoped.class) 的事件。此外,当请求上下文被销毁时,将触发带有限定符 @BeforeDestroyed(RequestScoped.class)@Destroyed(RequestScoped.class) 的事件。

How to Enable Trace Logging for Request Context Activation

您可以为 io.quarkus.arc.requestContext 日志记录器设置 TRACE 级别,然后尝试分析日志输出。

application.properties Example
quarkus.log.category."io.quarkus.arc.requestContext".min-level=TRACE 1
quarkus.log.category."io.quarkus.arc.requestContext".level=TRACE
1 还需要调整相关类别的最低日志级别。

Qualified Injected Fields

在 CDI 中,如果您声明字段注入点,则需要使用 @Inject 以及一组限定符(可选)。

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

在 Quarkus 中,如果注入字段声明至少一个限定符,您可以完全跳过 @Inject 注解。

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

对于下面讨论的一个特殊情况来说,@Inject 仍然是构造函数和方法注入所必需的。

Simplified Constructor Injection

在 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 声明无参数构造函数 - 它们会自动生成。 außerdem, 如果只有一个构造函数,则无需 @Inject

@ApplicationScoped
public class MyCoolService {

  private SimpleProcessor processor;

  MyCoolService(SimpleProcessor processor) {
    this.processor = processor;
  }
}

如果 Bean 类扩展未声明无参数构造函数的类,我们将不会自动生成无参数构造函数。

Removing Unused Beans

默认情况下,容器尝试在构建期间删除所有未使用的 Bean、拦截器和装饰器。此优化有助于最大程度减少生成类的数量,从而节省内存。但是,Quarkus 无法通过 CDI.current() 静态方法检测执行的编程查找。因此,删除可能会导致误报错误,即删除了 Bean,而实际上该 Bean 仍被使用。在这些情况下,您会在日志中注意到一个大的警告。用户和扩展作者有几个 how to eliminate false positives 选项。

可以通过将 quarkus.arc.remove-unused-beans 设置为 nonefalse 来禁用优化。此外,Quarkus 还提供了一种折衷方案,其中无论应用程序的 Bean 是否未使用,都不会将其删除,而对于非应用程序类,优化正常进行。要使用此模式,请将 quarkus.arc.remove-unused-beans 设置为 fwkframework

What’s Removed?

Quarkus 首先识别形成依赖关系树根的所谓 unremovable Bean。一个很好的例子是 Jakarta REST 资源类或声明 @Scheduled 方法的 bean。

unremovable bean:

  • 被扩展排除在移除之外,或者

  • 具有通过 @Named 指定的名称,或者

  • declares an observer method.

unused bean:

  • is not unremovable, and

  • 不适用于依赖关系树中的任何注入点,并且

  • 没有声明任何可注入到依赖关系树中任何注入点的生成器,并且

  • 不适用于注入任何 jakarta.enterprise.inject.Instancejakarta.inject.Provider 注入点。

未使用的拦截器和装饰器与任何 Bean 无关。

使用 dev 模式(运行 ./mvnw clean compile quarkus:dev)时,可以看到关于要删除哪些 Bean 的更多信息:

  1. 在控制台中 - 只需在 application.properties 中启用 DEBUG 级别,即 quarkus.log.category."io.quarkus.arc.processor".level=DEBUG

  2. 在相关的 Dev UI 页面中

How To Eliminate False Positives

用户可以通过使用 @io.quarkus.arc.Unremovable 注释 Bean(即使它们满足上面指定的所有规则)来指示容器不要删除其任何特定 Bean. 此注释可以在类、生成器方法或字段上声明。

由于这并不总是可能的,因此可以选择通过 application.properties 实现相同的功能. quarkus.arc.unremovable-types 属性接受一个字符串值列表,这些字符串值基于名称或包匹配 Bean。

Table 2. Value Examples

Value

Description

org.acme.Foo

匹配 Bean 类的全限定名

org.acme.*

匹配 Bean 类的包为 org.acme 的 Bean

org.acme.**

匹配 Bean 类的包以 org.acme 开头的 Bean

Bar

匹配 Bean 类的简单名称

Example application.properties
quarkus.arc.unremovable-types=org.acme.Foo,org.acme.*,Bar

此外,扩展可以通过生成 UnremovableBeanBuildItem 淘汰误报。

Default Beans

Quarkus 添加了 CDI 当前不支持的一种功能,即有条件地声明一个 Bean,如果没有任何可用方法(Bean 类、生成器、合成 Bean……)声明具有相同类型和限定符的 Bean。这是使用 @io.quarkus.arc.DefaultBean 注释完成的,最好通过示例进行说明。

假设有一个 Quarkus 扩展,它声明了几个 CDI 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
    }
}

这个想法是,扩展自动为用户配置内容,消除了大量样板代码 - 我们可以在任何需要的地方 @Inject 一个 Tracer. 现在想象一下,在我们的应用程序中,我们要使用已配置的 Tracer,但我们需要对其进行一些自定义,例如通过提供一个自定义 Reporter. 应用程序中唯一需要的就是类似以下内容的东西:

@Dependent
public class CustomTracerConfiguration {

    @Produces
    public Reporter reporter(){
        // create a custom Reporter
    }
}

@DefaultBean 允许扩展(或其他任何代码)提供默认值,同时在以任何 Quarkus 支持的方式提供该类型的 Bean 时退出。

默认Bean可以可选地声明`@jakarta.annotation.Priority`。如果没有定义优先级,则假定为`@Priority(0)`。优先级值用于Bean排序,并在类型安全解析期间消除多个匹配的默认Bean的歧义。

@Dependent
public class CustomizedDefaultConfiguration {

    @Produces
    @DefaultBean
    @Priority(100)
    public Configuration customizedConfiguration(){
        // create a customized default Configuration
        // this will have priority over previously defined default bean
    }
}

Enabling Beans for Quarkus Build Profile

Quarkus新增了CDI当前不支持的功能,即通过`@io.quarkus.arc.profile.IfBuildProfile`和`@io.quarkus.arc.profile.UnlessBuildProfile`注解,在启用Quarkus构建时间配置文件时有条件地启用Bean。当与`@io.quarkus.arc.DefaultBean`结合使用时,这些注解允许为不同的构建配置文件创建不同的Bean配置。

例如,假设一个应用程序包含一个名为`Tracer`的Bean,它在测试或开发模式下不需要执行任何操作,但对生产制品来说工作正常。创建这样的Bean的优雅方法如下:

@Dependent
public class TracerConfiguration {

    @Produces
    @IfBuildProfile("prod")
    public Tracer realTracer(Reporter reporter, Configuration configuration) {
        return new RealTracer(reporter, configuration);
    }

    @Produces
    @DefaultBean
    public Tracer noopTracer() {
        return new NoopTracer();
    }
}

如果相反,需要`Tracer`Bean也在开发模式下工作,并且仅在测试中默认什么也不做,那么`@UnlessBuildProfile`将是理想选择。代码如下所示:

@Dependent
public class TracerConfiguration {

    @Produces
    @UnlessBuildProfile("test") // this will be enabled for both prod and dev build time profiles
    public Tracer realTracer(Reporter reporter, Configuration configuration) {
        return new RealTracer(reporter, configuration);
    }

    @Produces
    @DefaultBean
    public Tracer noopTracer() {
        return new NoopTracer();
    }
}

运行时配置文件对使用`@IfBuildProfile`和`@UnlessBuildProfile`进行Bean解析绝对没有影响。

还可以在构造型上使用`@IfBuildProfile`和`@UnlessBuildProfile`。

Enabling Beans for Quarkus Build Properties

Quarkus新增了CDI当前不支持的功能,即通过`@io.quarkus.arc.properties.IfBuildProperty`和`@io.quarkus.arc.properties.UnlessBuildProperty`注解,在Quarkus构建时间属性具有/不具有特定值时有条件地启用Bean。当与`@io.quarkus.arc.DefaultBean`结合使用时,此注解允许为不同的构建属性创建不同的Bean配置。

我们上面使用`Tracer`提到的场景也可以通过以下方式实现:

@Dependent
public class TracerConfiguration {

    @Produces
    @IfBuildProperty(name = "some.tracer.enabled", stringValue = "true")
    public Tracer realTracer(Reporter reporter, Configuration configuration) {
        return new RealTracer(reporter, configuration);
    }

    @Produces
    @DefaultBean
    public Tracer noopTracer() {
        return new NoopTracer();
    }
}

@IfBuildProperty`和@UnlessBuildProperty`是可以重复的注解,即仅当这些注解定义的条件得到满足时,才启用Bean。

如果相反,只需要在`some.tracer.enabled`属性不是`false`时才使用`RealTracer`Bean,那么`@UnlessBuildProperty`将是理想选择。代码如下所示:

@Dependent
public class TracerConfiguration {

    @Produces
    @UnlessBuildProperty(name = "some.tracer.enabled", stringValue = "false")
    public Tracer realTracer(Reporter reporter, Configuration configuration) {
        return new RealTracer(reporter, configuration);
    }

    @Produces
    @DefaultBean
    public Tracer noopTracer() {
        return new NoopTracer();
    }
}

在运行时设置的属性对使用`@IfBuildProperty`进行Bean解析绝对没有影响。

还可以在构造型上使用`@IfBuildProperty`和`@UnlessBuildProperty`。

Declaring Selected Alternatives

在CDI中,可以使用`@Priority`为应用程序全局选择一个备用Bean,或者使用`beans.xml`描述符为Bean归档选择一个备用Bean。Quarkus简化了Bean发现,忽略了`beans.xml`的内容。

但是,还可以使用统一配置为应用程序选择替代方案。quarkus.arc.selected-alternatives`属性接受一个字符串值列表,这些字符串值用于匹配备用Bean。如果任何值匹配,则`Integer#MAX_VALUE`的优先级用于相关Bean。通过@Priority`声明或从构造型继承的优先级将被覆盖。

Table 3. Value Examples

Value

Description

org.acme.Foo

匹配声明生产者的Bean类的完全限定名称或Bean类

org.acme.*

匹配 Bean 类的包为 org.acme 的 Bean

org.acme.**

匹配 Bean 类的包以 org.acme 开头的 Bean

Bar

匹配声明生产者的Bean类的简单名称或Bean类

Example application.properties
quarkus.arc.selected-alternatives=org.acme.Foo,org.acme.*,Bar

Simplified Producer Method Declaration

在CDI中,生产者方法必须始终使用`@Produces`进行注解。

class Producers {

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

  @Produces
  @ApplicationScoped
  MyService produceService() {
    return new MyService(coolProperty);
  }
}

在Quarkus中,如果使用作用域注解、构造型或限定符对生产者方法进行注解,则可以完全跳过`@Produces`注解。

class Producers {

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

  @ApplicationScoped
  MyService produceService() {
    return new MyService(coolProperty);
  }
}

Interception of Static Methods

拦截器规范明确指出,_around-invoke_方法不能被声明为静态方法。但是,此限制主要是由技术限制引起的。而且,由于Quarkus是一个面向构建时间的堆栈,它允许额外的类转换,因此这些限制不再适用。可以用拦截器绑定对非私有静态方法进行注解:

class Services {

  @Logged 1
  static BigDecimal computePrice(long amount) { 2
    BigDecimal price;
    // Perform computations...
    return price;
  }
}
1 `Logged`是拦截器绑定。
2 如果存在与 `Logged`关联的拦截器,则每次调用方法都会被拦截。

Limitations

  • 出于向后兼容性原因,仅考虑了 method-level bindings(否则,声明类级别绑定的 Bean 类的静态方法会突然被拦截)

  • 私有静态方法永远不会被拦截

  • 出于显而易见的原因,InvocationContext#getTarget()`返回 `null;因此,并非所有现有的拦截器在拦截静态方法时都能正常工作

拦截器可以使用 `InvocationContext.getMethod()`来检测静态方法并相应地调整行为。

Ability to handle 'final' classes and methods

在普通 CDI 中,标记为 final`和/或具有 `final`方法的类不适合用于创建代理,这意味着拦截器和普通的作用域 Bean 不能正常工作。尝试将 CDI 与 Kotlin 等备选 JVM 语言搭配使用时,这种情况非常常见,其中类和方法默认情况下为 `final

然而,当 quarkus.arc.transform-unproxyable-classes`设置为 `true(这是默认值)时,Quarkus 可以克服这些限制。

Container-managed Concurrency

没有适用于 CDI Bean 的标准并发控制机制。尽管如此,Bean 实例仍可以从多个线程并发共享和访问。在这种情况下,它应该是线程安全的。你可以使用标准的 Java 结构(volatilesynchronizedReadWriteLock`等)或让容器控制并发访问。Quarkus 为此拦截器绑定提供了 `@io.quarkus.arc.Lock`和内置拦截器。与拦截 Bean 的上下文实例关联的每个拦截器实例都包含一个带有非公平排序策略的单独 `ReadWriteLock

io.quarkus.arc.Lock`是常规拦截器绑定,因此可以将其用于具有任何作用域的任何 Bean。但是,对于“共享”作用域(例如,@Singleton`和 @ApplicationScoped)尤其有用。

Container-managed Concurrency Example
import io.quarkus.arc.Lock;

@Lock 1
@ApplicationScoped
class SharedService {

  void addAmount(BigDecimal amount) {
    // ...changes some internal state of the bean
  }

  @Lock(value = Lock.Type.READ, time = 1, unit = TimeUnit.SECONDS) 2 3
  BigDecimal getAmount() {
    // ...it is safe to read the value concurrently
  }
}
1 类上声明的 @Lock(映射到 @Lock(Lock.Type.WRITE))指示容器锁定 Bean 实例,以便对任何业务方法进行任何调用,即客户端具有“独占访问权”,并且不允许并发调用。
2 `@Lock(Lock.Type.READ)`覆盖在类级别中指定的数值。这意味着任何数量的客户端都可以并发调用该方法,除非 Bean 实例被 `@Lock(Lock.Type.WRITE)`锁定。
3 你还可以指定“等待时间”。如果在给定时间内无法获取锁,将抛出 LockException

Repeatable interceptor bindings

Quarkus 仅支持 `@Repeatable`拦截器绑定注释。

将拦截器绑定到组件时,可以在方法上声明多个 `@Repeatable`注释。在类和类型上声明的可重复拦截器绑定不受支持,因为在与拦截器规范的相互作用周围存在一些悬而未决的问题。这可能会在将来添加。

例如,假设我们有一个清除缓存的拦截器。相应的拦截器绑定将称为 @CacheInvalidateAll`并声明为 `@Repeatable。如果我们想同时清除两个缓存,我们将添加 `@CacheInvalidateAll`两次:

@ApplicationScoped
class CachingService {
  @CacheInvalidateAll(cacheName = "foo")
  @CacheInvalidateAll(cacheName = "bar")
  void heavyComputation() {
    // ...
    // some computation that updates a lot of data
    // and requires 2 caches to be invalidated
    // ...
  }
}

这是拦截器的使用方法。那么如何创建拦截器呢?

在声明拦截器的拦截器绑定时,可以像往常一样向拦截器类添加多个 `@Repeatable`注释。当注释成员为 `@Nonbinding`时这是无用的,例如 `@Cached`注释的情况,但在其他情况下则很重要。

例如,假设我们有一个拦截器,可以将方法调用自动记录到特定目标。拦截器绑定的注释 `@Logged`将有一个称为 `target`的成员,它指定要存储日志的位置。我们的实现可以限制为控制台日志记录和文件日志记录:

@Interceptor
@Logged(target = "console")
@Logged(target = "file")
class NaiveLoggingInterceptor {
  // ...
}

其他拦截器可用于将方法调用记录到不同的目标。

Caching the Result of Programmatic Lookup

在某些情况下,通过注入的 `jakarta.enterprise.inject.Instance`和 `Instance.get()`以编程方式获取一个 Bean 实例是实用可行的。然而,根据规范, `get()`方法必须标识匹配的 Bean 并获得一个上下文引用。因此,在每次调用 `get()`时,都会返回一个 `@Dependent`Bean 的新实例。此外,此实例是注入的 `Instance`的一个从属对象。这种行为是明确规定的,但它可能会导致意外错误和内存泄漏。因此,Quarkus 提供了 `io.quarkus.arc.WithCaching`注解。使用此注解进行注解的已注入 `Instance`会缓存 `Instance#get()`操作的结果。结果在第一次调用时计算好,并且在所有后续调用(即使是 `@Dependent`Bean)中,都会返回相同的值。

class Producer {

  AtomicLong nextLong = new AtomicLong();
  AtomicInteger nextInt = new AtomicInteger();

   @Dependent
   @Produces
   Integer produceInt() {
     return nextInt.incrementAndGet();
   }

   @Dependent
   @Produces
   Long produceLong() {
     return nextLong.incrementAndGet();
   }
}

class Consumer {

  @Inject
  Instance<Long> longInstance;

  @Inject
  @WithCaching
  Instance<Integer> intInstance;

  // this method should always return true
  // Producer#produceInt() is only called once
  boolean pingInt() {
    return intInstance.get().equals(intInstance.get());
  }

  // this method should always return false
  // Producer#produceLong() is called twice per each pingLong() invocation
  boolean pingLong() {
    return longInstance.get().equals(longInstance.get());
  }
}

还可以通过 io.quarkus.arc.InjectableInstance.clearCache()`清除缓存值。在这种情况下,您需要注入 Quarkus 特有的 `io.quarkus.arc.InjectableInstance`而不是 `jakarta.enterprise.inject.Instance

Declaratively Choose Beans That Can Be Obtained by Programmatic Lookup

有时缩小可以通过 `jakarta.enterprise.inject.Instance`编程查找获取的 Bean 集合非常有用。通常,用户需要基于运行时配置属性选择接口的适当实现。

想象一下,我们有两个 Bean,它们实现了接口 org.acme.Service。除非您的实现声明一个 CDI 限定符,否则不能直接注入 org.acme.Service。但是,您可以转而注入 Instance<Service>,然后遍历所有实现并手动选择正确的实现。或者,您也可以使用 @LookupIfProperty`和 `@LookupUnlessProperty`注解。@LookupIfProperty`表示仅当运行时配置属性与提供的值相匹配时,才获取 Bean。另一方面,`@LookupUnlessProperty`表示仅当运行时配置属性与提供的值不相匹配时,才获取 Bean。

@LookupIfProperty Example
 interface Service {
    String name();
 }

 @LookupIfProperty(name = "service.foo.enabled", stringValue = "true")
 @ApplicationScoped
 class ServiceFoo implements Service {

    public String name() {
       return "foo";
    }
 }

 @ApplicationScoped
 class ServiceBar implements Service {

    public String name() {
       return "bar";
    }
 }

 @ApplicationScoped
 class Client {

    @Inject
    Instance<Service> service;

    void printServiceName() {
       // This will print "bar" if the property "service.foo.enabled" is NOT set to "true"
       // If "service.foo.enabled" is set to "true" then service.get() would result in an AmbiguousResolutionException
       System.out.println(service.get().name());
    }
 }

Injecting Multiple Bean Instances Intuitively

在 CDI 中,可通过实现 `java.lang.Iterable`的 `jakarta.enterprise.inject.Instance`注入多个 Bean 实例(也称为上下文引用)。然而,这并非直观。因此,Quarkus 中引入了一种新方式 - 您可使用 `io.quarkus.arc.All`限定符对 `java.util.List`进行注解。执行查找时,列表中元素的类型用作必需类型。

@ApplicationScoped
public class Processor {

     @Inject
     @All
     List<Service> services; 1 2
}
1 注入的实例是 disambiguated_Bean 的上下文引用的 _immutable list
2 对于此注入点,必需类型是 Service,并且未声明任何其他限定符。

列表按照 io.quarkus.arc.InjectableBean#getPriority()`定义的优先级排序。优先级越高,排列越靠前。通常情况下,@jakarta.annotation.Priority`注解可用于为类 Bean、生产者方法或生产者字段分配优先级。

如果一个注入点未声明 @All`以外的其他限定符,则使用 `@Any,即行为等同于 @Inject @Any Instance<Service>

您还可以注入一个包装在 `io.quarkus.arc.InstanceHandle`中的 Bean 实例列表。如果您需要检查相关的 Bean 元数据,这可能非常有用。

@ApplicationScoped
public class Processor {

     @Inject
     @All
     List<InstanceHandle<Service>> services;

     public void doSomething() {
       for (InstanceHandle<Service> handle : services) {
         if (handle.getBean().getScope().equals(Dependent.class)) {
           handle.get().process();
           break;
         }
       }
     }
}

类型变量和通配符都不是 @All List<>`注入点的合法类型参数,即不支持 `@Inject @All List<?> all,而且会导致发生部署错误。

还可以通过 `Arc.container().listAll()`方法以编程方式获取所有 Bean 实例句柄的列表。

Ignoring Class-Level Interceptor Bindings for Methods and Constructors

如果管理 Bean 在类级别声明拦截器绑定注解,则相应的 `@AroundInvoke`拦截器将适用于所有业务方法。类似地,相应的 `@AroundConstruct`拦截器将适用于 Bean 构造器。

例如,假设我们有一个带有 `@Logged`绑定注解的日志拦截器,以及一个带有 `@Traced`绑定注解的追踪拦截器:

@ApplicationScoped
@Logged
public class MyService {
    public void doSomething() {
        ...
    }

    @Traced
    public void doSomethingElse() {
        ...
    }
}

在此示例中,`doSomething`和 `doSomethingElse`都将被假想的日志拦截器拦截。另外,`doSomethingElse`方法将被假想的追踪拦截器拦截。

现在,如果该 `@Traced`拦截器还执行了所有必要的日志记录,我们希望跳过此方法的 `@Logged`拦截器,但将其保留在所有其他方法中。要做到这一点,您可以使用 `@NoClassInterceptors`对此方法进行注解:

@Traced
@NoClassInterceptors
public void doSomethingElse() {
    ...
}

`@NoClassInterceptors`注解可用于方法和构造器,表示此方法和构造器忽略所有类级别拦截器。换句话说,如果一个方法/构造器用 `@NoClassInterceptors`进行注解,则适用于此方法/构造器的唯一拦截器是在该方法/构造器上直接声明的拦截器。

此注解只影响业务方法拦截器 (@AroundInvoke) 和构造器生命周期回调拦截器 (@AroundConstruct)。

Exceptions Thrown By An Asynchronous Observer Method

如果异步观察器抛出了异常,那么 CompletionStage 方法返回的 fireAsync() 异常完成,以便事件生产者可以做出适当的反应。但是,如果事件生产者不关心,那么异常将被静默忽略。因此,Quarkus 默认会记录一条错误消息。还可以实现一个自定义的 AsyncObserverExceptionHandler。实现此接口的 Bean 应当是 @jakarta.inject.Singleton@jakarta.enterprise.context.ApplicationScoped

NoopAsyncObserverExceptionHandler
@Singleton
public class NoopAsyncObserverExceptionHandler implements AsyncObserverExceptionHandler {

  void handle(Throwable throwable, ObserverMethod<?> observerMethod, EventContext<?> eventContext) {
    // do nothing
  }

}

Intercepted self-invocation

Quarkus 支持所谓的拦截自身调用,或者仅支持自拦截——一种场景,在其中 CDI Bean 从其他方法中调用其自己的拦截方法,同时触发任何相关拦截器。这是一项非标准功能,因为 CDI 规范并没有定义自拦截是否应该运行。

假设我们有一个有两个方法的 CDI Bean,其中一个方法带有 @Transactional 拦截器绑定:

@ApplicationScoped
public class MyService {

  @Transactional 1
  void doSomething() {
    // some application logic
  }

  void doSomethingElse() {
    doSomething();2
  }

}
1 一个或多个拦截器绑定;@Transactional 仅仅是一个示例。
2 非拦截方法从具有关联绑定的同一 Bean 调用另一个方法;这将触发拦截。

在上面的示例中,调用 doSomething() 方法的任何代码都将触发拦截,在这种情况下,方法变为事务性。无论调用是否直接来自 MyService Bean(例如 MyService#doSomethingElse),还是来自其他 Bean。

Intercepting Producer Methods and Synthetic Beans

默认情况下,仅对托管 Bean(也称为基于类的 Bean)支持拦截。为了支持生产者方法和合成 Bean 的拦截,CDI 规范包括 InterceptionFactory,这是一个面向运行时的概念,因此无法在 Quarkus 中得到支持。

相反,Quarkus 有自己的 API: InterceptionProxy@BindingsSourceInterceptionProxyInterceptionFactory 非常相似:它创建一个在将方法调用转发到目标实例之前应用 @AroundInvoke 拦截器的代理。@BindingsSource 注释允许在拦截的类处于外部且无法更改的情况下设置拦截器绑定。

import io.quarkus.arc.InterceptionProxy;

@ApplicationScoped
class MyProducer {
    @Produces
    MyClass produce(InterceptionProxy<MyClass> proxy) { (1)
        return proxy.create(new MyClass()); (2)
    }
}
1 声明一个 InterceptionProxy&lt;MyClass&gt; 类型的注入点。这意味着在构建时,将生成一个 MyClass 的子类,来完成拦截和转发。请注意,类型参数必须与生产者方法的返回类型相同。
2 为给定的 MyClass 实例创建一个拦截代理实例。在所有拦截器运行之后,方法调用将被转发到此目标实例。

在此示例中,拦截器绑定从 MyClass 类中读取。

请注意,InterceptionProxy 仅支持在拦截器类中声明的 @AroundInvoke 拦截器。其他类型的拦截,以及在目标类及其超类中声明的 @AroundInvoke 拦截器不受支持。

拦截的类应该是 proxyable,因此不应该是 final,不应该有非私有的 final 方法,并且应该有一个非私有的零参数构造函数。如果不满足要求,那么如果 enabled,字节码转换将尝试修复它,但请注意,添加零参数构造函数并不总是可能的。

通常,产生的类来自外部库,根本不包含拦截器绑定注释。为了支持这种情况,可以在 InterceptionProxy 参数中声明 @BindingsSource 注释:

import io.quarkus.arc.BindingsSource;
import io.quarkus.arc.InterceptionProxy;

abstract class MyClassBindings { (1)
    @MyInterceptorBinding
    abstract String doSomething();
}

@ApplicationScoped
class MyProducer {
    @Produces
    MyClass produce(@BindingsSource(MyClassBindings.class) InterceptionProxy<MyClass> proxy) { (2)
        return proxy.create(new MyClass());
    }
}
1 一个反映了 MyClass 结构并包含拦截器绑定的类。
2 @BindingsSource 注释表明,MyClass 的拦截器绑定应该从 MyClassBindings 中读取。

bindings source 的概念是 InterceptionFactory.configure() 的构建时友好等价物。

生产者方法拦截和合成 Bean 拦截仅适用于实例方法。Interception of Static Methods 不支持生产者方法和合成 Bean。

Declaring @BindingsSource

@BindingsSource 注释指定了一个反映拦截类结构的类。然后从该类中读取拦截器绑定,并将其视为在拦截类中声明。

具体来说:绑定源类上声明的类级拦截器绑定被视为拦截类中的类级绑定。在绑定源类上声明的方法级拦截器绑定被视为具有与被拦截类的同名、返回值类型、参数类型和 static 标记的方法的方法级绑定。

通常让绑定源类和方法 abstract 以便您可以不必编写方法正文:

abstract class MyClassBindings {
    @MyInterceptorBinding
    abstract String doSomething();
}

因为此类从未被实例化,并且其方法从未被调用,所以这没问题,但是也可以创建一个非 abstract 类:

class MyClassBindings {
    @MyInterceptorBinding
    String doSomething() {
        return null; (1)
    }
}
1 方法正文不重要。

请注意,对于泛型类,类型变量名还必须是相同的。例如,对于以下类:

class MyClass<T> {
    T doSomething() {
        ...
    }

    void doSomethingElse(T param) {
        ...
    }
}

绑定源类还必须将 T 用作类型变量的名称:

abstract class MyClassBindings<T> {
    @MyInterceptorBinding
    abstract T doSomething();
}

您无需仅仅因为它们存在于被拦截类中而声明未注释的方法。如果您想向部分方法添加方法级绑定,您只需声明应该具有拦截器绑定的方法。如果您只想添加类级绑定,您根本无需声明任何方法。

这些注释可能存在于绑定源类上:

  • interceptor bindings:在类和方法上

  • stereotypes: on the class

  • @NoClassInterceptors: on the methods

绑定源类上存在的任何其他注释都会被忽略。

Synthetic Beans

在合成 bean 中使用 InterceptionProxy 是相似的。

首先,您必须声明您的合成 bean 注入 InterceptionProxy

public void register(RegistrationContext context) {
    context.configure(MyClass.class)
            .types(MyClass.class)
            .injectInterceptionProxy() (1)
            .creator(MyClassCreator.class)
            .done();
}
1 再次,这意味着在构建时,生成了 MyClass 的子类执行拦截和转发。

其次,您必须从 BeanCreator 中的 SyntheticCreationalContext 中获取 InterceptionProxy 并使用它:

public MyClass create(SyntheticCreationalContext<MyClass> context) {
    InterceptionProxy<MyClass> proxy = context.getInterceptionProxy(); (1)
    return proxy.create(new MyClass());
}
1 获取上面声明的 MyClassInterceptionProxy。也可以使用 getInjectedReference() 方法,传递 TypeLiteral,但 getInterceptionProxy() 更简单。

@BindingsSource 还有一个等效项。injectInterceptionProxy() 方法具有带参数的重载:

public void register(RegistrationContext context) {
    context.configure(MyClass.class)
            .types(MyClass.class)
            .injectInterceptionProxy(MyClassBindings.class) (1)
            .creator(MyClassCreator.class)
            .done();
}
1 参数是绑定源类。

Pitfalls with Reactive Programming

CDI 是一个纯粹的同步框架。其异步概念非常有限,完全基于线程池和线程卸载。因此,在将 CDI 与反应式编程一起使用时有许多陷阱。

Detecting When Blocking Is Allowed

io.quarkus.runtime.BlockingOperationControl#isBlockingAllowed() 方法可用于检测当前线程是否允许阻塞。当它不允许而您需要执行阻塞操作时,您必须将其卸载到其他线程。最简单的方法是使用 Vertx.executeBlocking() 方法:

import io.quarkus.runtime.BlockingOperationControl;

@ApplicationScoped
public class MyBean {
    @Inject
    Vertx vertx;

    @PostConstruct
    void init() {
        if (BlockingOperationControl.isBlockingAllowed()) {
            somethingThatBlocks();
        } else {
            vertx.executeBlocking(() -> {
                somethingThatBlocks();
                return null;
            });
        }
    }

    void somethingThatBlocks() {
        // use the file system or JDBC, call a REST service, etc.
        Thread.sleep(5000);
    }
}

Build Time Extensions

Quarkus 集成了构建时优化,以便提供即时启动和低内存占用。此方法的缺点是无法支持 CDI 便携式扩展。不过,大部分功能可以使用 Quarkus extensions实现。有关详情,请参阅 integration guide

[id="development-mode"] Dev mode

在开发模式中,自动注册两个特殊端点,以 JSON 格式提供一些基本的调试信息:

这些端点仅在开发模式下可用,即通过 mvn quarkus:dev(或 ./gradlew quarkusDev)运行应用程序时。

Monitoring Business Method Invocations and Events

在开发模式中,还可以启用对业务方法调用和触发事件的监控。只需将 quarkus.arc.dev-mode.monitoring-enabled`配置属性设置为 `true,并浏览相关的 Dev UI 页面。

Strict Mode

默认情况下,ArC 不会执行 CDI 规范要求的所有验证。它还在很多方面改进了 CDI 的可用性,其中一些方面直接违反规范。

为了通过 CDI Lite TCK,ArC 还有一个 _strict_模式。此模式会启用其他验证,并禁用与规范冲突的特定改进。

要启用严格模式,请使用以下配置:

quarkus.arc.strict-compatibility=true

其他一些功能还会影响规范兼容性:

要获得更接近规范的行为,应禁用这些功能。

建议应用程序使用默认的非严格模式,这使得 CDI 更易于使用。严格模式的“严格性”(在 CDI 规范之上执行其他验证和禁用改进的集合)可能会随时间而变化。

ArC Configuration Reference

Unresolved include directive in modules/ROOT/pages/cdi-reference.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-arc.adoc[]