编辑此页面

上下文和依赖注入 (CDI) 简介

在本指南中,我们将介绍 Quarkus 编程模型的基本原则,该模型基于 Jakarta Contexts and Dependency Injection 4.1 规范。CDI 参考指南介绍了 Bean 的发现、非标准功能和配置属性。CDI 集成指南更详细地介绍了常见的 CDI 相关集成用例,以及解决方案的示例代码。

1. 好的。让我们从简单的开始。什么是 Bean?

Bean 是一个容器管理的对象,它支持一组基本服务,例如依赖注入、生命周期回调和拦截器。

2. 等一下。“容器管理”是什么意思?

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

3. 它有什么用?

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

您可能听说过控制反转(IoC)编程原则。依赖注入是 IoC 的一种实现技术。

4. Bean 是什么样子的?

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

简单的 Bean 示例
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 实例与哪个上下文关联。在本例中,为应用程序创建单个 Bean 实例,并由所有其他注入 Translator 的 Bean 使用。
2 这是一个字段注入点。它告诉容器 Translator 依赖于 Dictionary Bean。如果没有匹配的 Bean,则构建失败。
3 这是一个拦截器绑定注解。在本例中,该注解来自 MicroProfile Metrics。相关的拦截器拦截调用并更新相关的指标。我们稍后将讨论拦截器

5. 很好。依赖项解析是如何工作的?我没有看到任何名称或标识符。

这是一个好问题。在 CDI 中,将 Bean 匹配到注入点的过程是类型安全的。每个 Bean 声明一组 Bean 类型。在上面的示例中,Translator Bean 有两种 Bean 类型:Translatorjava.lang.Object。随后,如果 Bean 具有与所需类型匹配的 Bean 类型,并且具有所有必需的限定符,则该 Bean 可分配给注入点。我们稍后将讨论限定符。现在,只要知道上面的 Bean 可分配给类型为 Translatorjava.lang.Object 的注入点就足够了。

6. 嗯,等一下。如果多个 Bean 声明相同的类型会发生什么?

有一个简单的规则:必须只有一个 Bean 可分配给注入点,否则构建失败。如果没有可分配的,则构建失败,并出现 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 扩展了 Iterable

7. 我可以使用 Setter 和构造函数注入吗?

是的,你可以。事实上,在 CDI 中,“Setter 注入”被更强大的初始化方法取代。初始化程序可以接受多个参数,并且不必遵循 JavaBean 命名约定。

初始化和构造函数注入示例
@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 实现中无法工作,在常规 CDI 实现中,具有正常范围的 Bean 必须始终声明一个无参数构造函数,并且 Bean 构造函数必须使用 @Inject 注释。但是,在 Quarkus 中,我们检测到缺少无参数构造函数,并直接在字节码中“添加”它。如果只存在一个构造函数,也不需要添加 @Inject
2 初始化方法必须使用 @Inject 注释。
3 初始化程序可以接受多个参数 - 每个参数都是一个注入点。

8. 您谈到了一些限定符?

限定符是帮助容器区分实现相同类型的 Bean 的注解。正如我们已经说过的,如果 Bean 具有所有必需的限定符,则该 Bean 可分配给注入点。如果您在注入点声明了任何限定符,则假定为 @Default 限定符。

限定符类型是定义为 @Retention(RUNTIME) 并使用 @jakarta.inject.Qualifier 元注解注释的 Java 注解。

限定符示例
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Superior {}

Bean 的限定符通过使用限定符类型注释 Bean 类或生成器方法或字段来声明。

具有自定义限定符的 Bean 示例
@Superior (1)
@ApplicationScoped
public class SuperiorTranslator extends Translator {

    String translate(String sentence) {
      // ...
    }
}
1 @Superior 是一个 限定符注解

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

您可能熟悉的 @Named 限定符在 CDI 中与其他限定符有些不同。有关为什么通常不应使用它的原因,请参阅 CDI 参考指南中的相应部分

9. 看起来不错。什么是 Bean 范围?

Bean 的范围确定了 Bean 实例的生命周期,即实例应何时以及在何处创建和销毁。

每个 Bean 都恰好有一个范围。

10. 我实际上可以在我的 Quarkus 应用程序中使用哪些范围?

您可以使用规范中提到的所有内置范围,但 jakarta.enterprise.context.ConversationScoped 除外。

注解 描述

@jakarta.enterprise.context.ApplicationScoped

单个 Bean 实例用于应用程序,并在所有注入点之间共享。该实例是延迟创建的,即一旦在客户端代理上调用了方法。

@jakarta.inject.Singleton

@ApplicationScoped 类似,只是不使用客户端代理。当注入解析为 @Singleton Bean 的注入点时,将创建该实例。

@jakarta.enterprise.context.RequestScoped

Bean 实例与当前请求(通常是 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

11. @ApplicationScoped@Singleton 看起来非常相似。我应该为我的 Quarkus 应用程序选择哪一个?

这取决于 ;-)。

@Singleton Bean 没有客户端代理,因此当注入 Bean 时,实例会被急切创建。相比之下,@ApplicationScoped Bean 的实例是延迟创建的,即当第一次在注入的实例上调用方法时。

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

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

另一方面,您不能使用 QuarkusMock 模拟 @Singleton Bean。

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

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

12. 我不理解客户端代理的概念。

事实上,客户端代理可能很难掌握,但它们提供了一些有用的功能。客户端代理基本上是一个将所有方法调用委托给目标 Bean 实例的对象。它是一个容器构造,实现了 io.quarkus.arc.ClientProxy 并扩展了 Bean 类。

客户端代理只委托方法调用。因此,永远不要读取或写入普通范围 Bean 的字段,否则您将使用非上下文或过时的数据。
生成的客户端代理示例
@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 的上下文实例

客户端代理允许

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

  • 能够将具有“较窄”范围的 Bean 注入到具有“较宽”范围的 Bean 中;即,您可以将 @RequestScoped Bean 注入到 @ApplicationScoped Bean 中。

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

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

13. 好的。您说过 Bean 有几种类型?

是的。通常,我们区分

  1. 类 Bean

  2. 生成器方法

  3. 生成器字段

  4. 合成 Bean

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

如果您需要额外控制 Bean 的实例化,生成器方法和字段非常有用。当集成第三方库时,它们也很有用,在第三方库中,您无法控制类源,并且可能无法添加其他注解等。

生成器示例
@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 元数据。类型用于构建 Bean 类型集。在本例中,它将是 doublejava.lang.Object。未声明范围注解,因此默认值为 @Dependent
2 容器将在创建 Bean 实例时读取此字段。
3 容器分析方法注解以构建 Bean 元数据。返回类型用于构建 Bean 类型集。在本例中,它将是 List<String>Collection<String>Iterable<String>java.lang.Object。未声明范围注解,因此默认值为 @Dependent
4 容器将在创建 Bean 实例时调用此方法。

有关生成器的更多信息。您可以声明限定符、将依赖项注入到生成器方法参数等。您可以在例如 Weld 文档中阅读有关生成器的更多信息。

14. 好的,注入看起来很酷。还提供了哪些其他服务?

14.1. 生命周期回调

Bean 类可以声明生命周期 @PostConstruct@PreDestroy 回调。

生命周期回调示例
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。

14.2. 拦截器

拦截器用于将跨领域关注点与业务逻辑分离。有一个单独的规范 - Java 拦截器 - 定义了基本的编程模型和语义。

简单的拦截器绑定示例
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 的,但不必是。
简单的拦截器示例
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 标记拦截器组件。
4 拦截器可以注入依赖项。
5 AroundInvoke 表示一个介入业务方法的方法。
6 继续到拦截器链中的下一个拦截器或调用拦截的业务方法。
拦截器的实例是它们拦截的 Bean 实例的依赖对象,即为每个拦截的 Bean 创建一个新的拦截器实例。
拦截器使用的简单示例
import jakarta.enterprise.context.ApplicationScoped;

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

14.3. 装饰器

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

简单的装饰器示例
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
   @Decorated
   Bean<Account> delegateInfo; (5)


   @Inject
   LogService logService; (6)

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

}
1 @Priority 启用装饰器。首先调用优先级值较小的装饰器。
2 @Decorator 标记装饰器组件。
3 装饰类型集包括所有作为 Java 接口的 Bean 类型,java.io.Serializable 除外。
4 每个装饰器必须声明恰好一个委托注入点。装饰器适用于可分配给此委托注入点的 Bean。
5 可以使用 @Decorated 限定符获取有关装饰 Bean 的信息。
6 装饰器可以注入其他 Bean。
7 装饰器可以调用委托对象的任何方法。容器调用链中的下一个装饰器或拦截实例的业务方法。
装饰器的实例是它们拦截的 Bean 实例的依赖对象,即为每个拦截的 Bean 创建一个新的装饰器实例。

14.4. 事件和观察者

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

简单的事件示例
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 同步触发事件。
3 当触发 TaskCompleted 事件时,会通知此方法。
有关事件/观察者的更多信息,请访问 Weld 文档

15. 结论

在本指南中,我们介绍了一些 Quarkus 编程模型的基本主题,该模型基于 Jakarta Contexts and Dependency Injection 4.1 规范。Quarkus 实现了 CDI Lite 规范,但未实现 CDI Full。另请参阅支持的功能和限制列表。还有相当多的非标准功能Quarkus 特定的 API

如果您想了解有关 Quarkus 特定功能和限制的更多信息,请参阅 Quarkus CDI 参考指南。我们还建议您阅读 CDI 规范Weld 文档(Weld 是 CDI 参考实现),以熟悉更复杂的主题。

相关内容