Introduction to Contexts and Dependency Injection (CDI)

OK. Let’s start simple. What is a bean?

一个 Bean 是一个 container-managed 对象,它支持一系列基本服务,如依赖关系注入、生命周期回调和拦截器。

Wait a minute. What does "container-managed" mean?

简而言之,你不直接控制对象实例的生命周期。相反,你可以通过声明性方式(如注释、配置等)影响生命周期。容器是你的应用程序运行的 environment。它创建和销毁 Bean 实例,将实例与指定上下文相关联,并将其注入到其他 Bean 中。

What is it good for?

应用程序开发人员可以专注于业务逻辑,而不是找出“在哪里和如何”获取包含所有依赖关系的已完全初始化的组件。

你可能听说过 inversion of control (IoC) 编程原则。依赖关系注入是 IoC 的实现技术之一。

What does a bean look like?

有几种类型的 Bean。最常见的是基于类的 Bean:

Simple Bean Example
import jakarta.inject.Inject;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.metrics.annotation.Counted;

@ApplicationScoped 1
public class Translator {

    @Inject
    Dictionary dictionary; 2

    @Counted  3
    String translate(String sentence) {
      // ...
    }
}
1 这是一个范围注解。它告诉容器该用哪个上下文关联 Bean 实例。在这个特殊情况下,程序用于应用程序并由注入 Translator 的所有其他 Bean 使用。
2 这是一个域注入点。它告诉容器 Translator 依赖于 Dictionary Bean。如果没有匹配的 Bean,该版本将失败。
3 这是一个拦截器绑定注解。在这种情况下,注解来自 MicroProfile 度量标准。相关的拦截器会拦截调用并更新相关的度量标准。我们稍后讨论 interceptors

Nice. How does the dependency resolution work? I see no names or identifiers.

这是一个好问题。在 CDI 中,将 Bean 与注入点匹配的过程是 type-safe。每个 Bean 都声明了一组 Bean 类型。在我们上面的示例中,Translator Bean 具有两个 Bean 类型:Translatorjava.lang.Object。随后,如果 Bean 具有与 required type 匹配的 Bean 类型并具有所有 required qualifiers,则 Bean 可分配给注入点。我们稍后讨论限定符。现在,只需知道上面的 Bean 可以分配给类型为 Translatorjava.lang.Object 的注入点。

Hm, wait a minute. What happens if multiple beans declare the same type?

这里有一个简单的规则:exactly one bean must be assignable to an injection point, otherwise the build fails。如果没有任何可分配的,则版本将失败,并显示 UnsatisfiedResolutionException。如果有多个可分配的,则版本将失败,并显示 AmbiguousResolutionException。这非常有用,因为每当容器无法为任何注入点找到明确的依赖项时,你的应用程序都会快速失败。

你可以使用 jakarta.enterprise.inject.Instance 通过编程查找来解决运行时的模糊性,甚至可以遍历实现给定类型的所有 Bean:

public class Translator {

    @Inject
    Instance<Dictionary> dictionaries; 1

    String translate(String sentence) {
      for (Dictionary dict : dictionaries) { 2
         // ...
      }
    }
}
1 即使有多个 Bean 实现 Dictionary 类型,也不会导致这个注入点出现模糊性依赖。
2 jakarta.enterprise.inject.Instance extends Iterable.

Can I use setter and constructor injection?

是的,你可以。事实上,在 CDI 中,“setter 注入”已经被功能更强大的 initializer methods 取代。初始化器可以接受多个参数,并且不必遵守 JavaBean 命名约定。

Initialized and Constructor Injection Example
@ApplicationScoped
public class Translator {

    private final TranslatorHelper helper;

    Translator(TranslatorHelper helper) { 1
       this.helper = helper;
    }

    @Inject 2
    void setDeps(Dictionary dic, LocalizationService locService) { 3
      / ...
    }
}
1 这是一个构造函数注入。事实上,这段代码在常规 CDI 实现中不起作用,其中具有正常范围的 Bean 必须始终声明一个无参数构造函数,并且 Bean 构造函数必须使用 @Inject 进行注解。然而,在 Quarkus 中,我们检测到没有无参构造函数并直接在字节码中“添加”了它。如果只存在一个构造函数,则不必添加 @Inject
2 初始化器方法必须使用 @Inject 进行注解。
3 初始化器可以接受多个参数,每个参数都是一个注入点。

You talked about some qualifiers?

Qualifiers 是注解,帮助容器区分实现了相同类型的 Bean。正如我们之前所说,如果 Bean 具有所有必需的限定符,则可以将其分配给注入点。如果你在注入点未声明任何限定符,则假定 @Default 限定符。

限定符类型是一个 Java 注解,定义为 @Retention(RUNTIME) 并使用 @jakarta.inject.Qualifier 元注解进行注解:

Qualifier Example
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Superior {}

通过使用限定符类型对 Bean 类或生产者方法或域进行注解,可以声明 Bean 的限定符:

Bean With Custom Qualifier Example
@Superior 1
@ApplicationScoped
public class SuperiorTranslator extends Translator {

    String translate(String sentence) {
      // ...
    }
}
1 @Superiorqualifier annotation

这个 Bean 可以分配给 @Inject @Superior Translator@Inject @Superior SuperiorTranslator,但不能分配给 @Inject Translator。原因是 @Inject Translator 在类型安全解析期间会自动转换为 @Inject @Default Translator。由于我们的 SuperiorTranslator 未声明 @Default,因此只有原始 Translator Bean 可以分配。

Looks good. What is the bean scope?

Bean 的范围决定了其实例的生命周期,即何时何地创建和销毁实例。

每个 bean 都只有一种作用域。

What scopes can I actually use in my Quarkus application?

你可以使用规范中提到的所有内置作用域,jakarta.enterprise.context.ConversationScoped 除外。

Annotation Description

@jakarta.enterprise.context.ApplicationScoped

单个 bean 实例可用于应用程序,并且在所有注入点之间共享。该实例是延迟创建的,即一旦在一个方法调用 client proxy

@jakarta.inject.Singleton

@ApplicationScoped 相同,只是不使用客户端代理。当注入解析为 @Singleton bean 的注入点时,创建实例。

@jakarta.enterprise.context.RequestScoped

bean 实例与当前 request 关联(通常是 HTTP 请求)。

@jakarta.enterprise.context.Dependent

这是一个伪作用域。不共享实例,每个注入点都生成一个新的依赖 bean 实例。依赖 bean 的生命周期与注入它的 bean 相关联——它将与注入它的 bean 一起创建和销毁。

@jakarta.enterprise.context.SessionScoped

此作用域由 jakarta.servlet.http.HttpSession 对象支持。仅当 quarkus-undertow 扩展可用时,它才可用。

Quarkus 扩展可以提供其他自定义作用域。例如,quarkus-narayana-jta 提供 jakarta.transaction.TransactionScoped

@ApplicationScoped and @Singleton look very similar. Which one should I choose for my Quarkus application?

它取决于 ;-).

@Singleton bean 没有 client proxy,因此当 bean 注入时,一个实例将会 created eagerly。相比之下,@ApplicationScoped bean 的实例是 created lazily,即当首次对注入的实例调用一个方法时。

此外,客户端代理仅委托方法调用,因此你不应该直接读/写注入的 @ApplicationScoped bean 的字段。你可以安全地读/写注入 @Singleton 的字段。

@Singleton 应该具有稍好的性能,因为它没有间接调用(没有代理从上下文委托给当前实例)。

另一方面,你不能使用 QuarkusMock 来模拟 @Singleton bean。

@ApplicationScoped bean 也可以在运行时销毁和重新创建。现有注入点只是能够正常工作,因为注入的代理委托给当前实例。

因此,我们建议默认情况下坚持使用 @ApplicationScoped,除非有充分的理由使用 @Singleton

I don’t understand the concept of client proxies.

的确,https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1.html#client_proxies[client proxies, window=_blank]可能难以理解,但它们提供了一些有用的功能。客户端代理基本上是一个委托所有方法调用给目标 bean 实例的对象。它是一个实现了 `io.quarkus.arc.ClientProxy`并扩展了 bean 类的容器构建。

客户端代理只委托方法调用。因此,不要读写常规作用域 bean 的字段,否则你会使用非上下文的或陈旧的数据。

Generated Client Proxy Example
@ApplicationScoped
class Translator {

    String translate(String sentence) {
      // ...
    }
}

// The client proxy class is generated and looks like...
class Translator_ClientProxy extends Translator { 1

    String translate(String sentence) {
      // Find the correct translator instance...
      Translator translator = getTranslatorInstanceFromTheApplicationContext();
      // And delegate the method invocation...
      return translator.translate(sentence);
    }
}
1 始终注入 Translator_ClientProxy`实例,而不是对 `Translator bean 的 contextual instance的直接引用。

客户端代理允许:

  • 延迟实例化 - 一旦在代理上调用方法,就会创建实例。

  • 能够将范围“更窄”的 bean 注入到范围“更宽”的 bean;换言之,你可以将 `@RequestScoped`bean 注入到 `@ApplicationScoped`bean。

  • 依赖项图中存在循环依赖项。存在循环依赖项通常表示应该考虑重新设计,但有时这是不可避免的。

  • 在极少数情况下,手动销毁 bean 是明智的。直接注入的引用会导致过时的 bean 实例。

OK. You said that there are several kinds of beans?

对的。一般来说,我们区分:

  1. Class beans

  2. Producer methods

  3. Producer fields

  4. Synthetic beans

通常由扩展提供合成 bean。因此,我们不会在本指南中介绍它们。

如果你需要对 bean 实例化进行更多控制,生产程序方法和字段会很有用。当你集成第三方的库时它们也十分有用 - 在这种情况下,你无法控制类源,也不能添加附加的注解等。

Producers Example
@ApplicationScoped
public class Producers {

    @Produces 1
    double pi = Math.PI; 2

    @Produces 3
    List<String> names() {
       List<String> names = new ArrayList<>();
       names.add("Andy");
       names.add("Adalbert");
       names.add("Joachim");
       return names; 4
    }
}

@ApplicationScoped
public class Consumer {

   @Inject
   double pi;

   @Inject
   List<String> names;

   // ...
}
1 容器会分析字段注解,来构建 bean 元数据。_type_用于构建 bean 类型集。在这种情况下,它将是 double`和 `java.lang.Object。没有声明作用域注解,因此它默认为 @Dependent
2 容器在创建 bean 实例时将读取该字段。
3 容器分析方法注释以构建 Bean 元数据。return type 用于构建 Bean 类型集。在这种情况下,它将是 List&lt;String&gt;Collection&lt;String&gt;Iterable&lt;String&gt;java.lang.Object。没有声明范围注释,因此默认为 @Dependent
4 容器在创建 bean 实例时将调用此方法。

关于生产程序还有更多内容。你可以声明限定符,将依赖项注入到生产程序方法参数中,等等。你可以进一步了解生产程序,例如,在 Weld docs中。

OK, injection looks cool. What other services are provided?

Lifecycle Callbacks

bean 类可以声明生命周期 `@PostConstruct`和 `@PreDestroy`回调:

Lifecycle Callbacks Example
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

@ApplicationScoped
public class Translator {

    @PostConstruct 1
    void init() {
       // ...
    }

    @PreDestroy 2
    void destroy() {
      // ...
    }
}
1 该回调在 bean 实例被投入使用之前调用。在此处执行一些初始化是安全的。
2 该回调在销毁 bean 实例之前调用。在此处执行一些清理任务是安全的。

保持回调逻辑“无副作用”是一个好习惯,即应避免在回调内调用其他 Bean。

Interceptors

拦截器用于将横切关注点与业务逻辑分离。有一个独立规范 - Java 拦截器 - 用来定义基本编程模型和语义。

Simple Interceptor Binding Example
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.interceptor.InterceptorBinding;

@InterceptorBinding (1)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR}) (2)
@Inherited (3)
public @interface Logged {
}
1 这是一个拦截器绑定注解。请参阅以下示例了解其使用方式。
2 拦截器绑定注解始终放在拦截器类型上,可以放在目标类型或方法上。
3 拦截器绑定通常是 @Inherited,但不一定是。
Simple Interceptor Example
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;

@Logged (1)
@Priority(2020) (2)
@Interceptor (3)
public class LoggingInterceptor {

   @Inject (4)
   Logger logger;

   @AroundInvoke (5)
   Object logInvocation(InvocationContext context) {
      // ...log before
      Object ret = context.proceed(); (6)
      // ...log after
      return ret;
   }

}
1 拦截器绑定注解用于将拦截器绑定到 Bean。使用 `@Logged`对 Bean 类进行简单的注解,如下例所示。
2 `Priority`启用拦截器并影响拦截器顺序。优先级值较小的拦截器首先调用。
3 Marks an interceptor component.
4 拦截器可以注入依赖项。
5 `AroundInvoke`表示在业务方法上插入的方法。
6 转到拦截器链中的下一个拦截器或调用被拦截的业务方法。

拦截器的实例是它们所拦截的 Bean 实例的依赖项对象,即为每个被拦截的 Bean 创建一个新的拦截器实例。

Simple Example of Interceptor Usage
import jakarta.enterprise.context.ApplicationScoped;

@Logged // <1> 2
@ApplicationScoped
public class MyService {
   void doSomething() {
       ...
   }
}
1 拦截器绑定注解放在 Bean 类上,以便拦截所有业务方法。此注解也可以放在单个方法上,在这种情况下,只拦截带注解的方法。
2 请记住,@Logged`注解是 `@Inherited。如果某个 Bean 类继承自 MyService,则 `LoggingInterceptor`也会应用于它。

Decorators

修饰器类似于拦截器,但由于它们实现了具有业务语义的接口,因此能够实现业务逻辑。

Simple Decorator Example
import jakarta.decorator.Decorator;
import jakarta.decorator.Delegate;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.enterprise.inject.Any;

public interface Account {
   void withdraw(BigDecimal amount);
}

@Priority(10) 1
@Decorator 2
public class LargeTxAccount implements Account { 3

   @Inject
   @Any
   @Delegate
   Account delegate; 4

   @Inject
   LogService logService; 5

   void withdraw(BigDecimal amount) {
      delegate.withdraw(amount); 6
      if (amount.compareTo(1000) > 0) {
         logService.logWithdrawal(delegate, amount);
      }
   }

}
1 `@Priority`启用修饰器。优先级值较小的修饰器首先调用。
2 `@Decorator`标记修饰器组件。
3 装饰类型集合包括所有 Bean 类型,这些类型为 Java 接口,但 `java.io.Serializable`除外。
4 每个修饰器必须准确声明一个 delegate injection point。修饰器适用于可分配给此委托注入点的 Bean。
5 修饰器可以注入其他 Bean。
6 装饰器可以调用委托对象的任何方法。容器调用链中的下一个装饰器或拦截实例的业务方法。

装饰器实例是其所拦截的 bean 实例的依赖对象,即为每个拦截的 bean 创建一个新的装饰器实例。

Events and Observers

bean 还可以生成和使用事件,以完全解耦的方式进行交互。任何 Java 对象都可以作为事件有效负载。可选限定符充当主题选择器。

Simple Event Example
class TaskCompleted {
  // ...
}

@ApplicationScoped
class ComplicatedService {

   @Inject
   Event<TaskCompleted> event; 1

   void doSomething() {
      // ...
      event.fire(new TaskCompleted()); 2
   }

}

@ApplicationScoped
class Logger {

   void onTaskCompleted(@Observes TaskCompleted task) { 3
      // ...log the task
   }

}
1 `jakarta.enterprise.event.Event`用于触发事件。
2 Fire the event synchronously.
3 当触发 `TaskCompleted`事件时,会通知此方法。

有关事件/观察者的更多信息,请访问 Weld docs

Conclusion

在本指南中,我们介绍了 Quarkus 编程模型的一些基本主题,该模型基于 Jakarta Contexts and Dependency Injection 4.1规范。Quarkus 实现 CDI Lite 规范,但没有实现 CDI Full。另请参见 the list of supported features and limitations。还有相当多的 non-standard featuresQuarkus-specific APIs

如果您希望了解有关 Quarkus 特定功能和限制的更多信息,请访问 Quarkus CDI Reference Guide。我们还建议您阅读 CDI specificationWeld documentation(Weld 是一种 CDI 参考实现),以便熟悉更复杂的话题。