上下文和依赖注入 (CDI) 简介
在本指南中,我们将介绍 Quarkus 编程模型的基本原则,该模型基于 Jakarta Contexts and Dependency Injection 4.1 规范。CDI 参考指南介绍了 Bean 的发现、非标准功能和配置属性。CDI 集成指南更详细地介绍了常见的 CDI 相关集成用例,以及解决方案的示例代码。
2. 等一下。“容器管理”是什么意思?
简单来说,您不直接控制对象实例的生命周期。相反,您可以通过声明性方式(例如注解、配置等)影响生命周期。容器是您的应用程序运行的环境。它创建和销毁 Bean 的实例,将实例与指定的上下文关联,并将它们注入到其他 Bean 中。
3. 它有什么用?
应用程序开发人员可以专注于业务逻辑,而不是找出“在哪里以及如何”获取一个完全初始化的组件及其所有依赖项。
您可能听说过控制反转(IoC)编程原则。依赖注入是 IoC 的一种实现技术。 |
4. 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 类型:Translator
和 java.lang.Object
。随后,如果 Bean 具有与所需类型匹配的 Bean 类型,并且具有所有必需的限定符,则该 Bean 可分配给注入点。我们稍后将讨论限定符。现在,只要知道上面的 Bean 可分配给类型为 Translator
和 java.lang.Object
的注入点就足够了。
6. 嗯,等一下。如果多个 Bean 声明相同的类型会发生什么?
有一个简单的规则:必须只有一个 Bean 可分配给注入点,否则构建失败。如果没有可分配的,则构建失败,并出现 UnsatisfiedResolutionException
。如果有多个可分配的,则构建失败,并出现 AmbiguousResolutionException
。这非常有用,因为每当容器无法为任何注入点找到明确的依赖项时,您的应用程序都会快速失败。
您可以使用通过
|
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 类或生成器方法或字段来声明。
@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 参考指南中的相应部分。 |
10. 我实际上可以在我的 Quarkus 应用程序中使用哪些范围?
您可以使用规范中提到的所有内置范围,但 jakarta.enterprise.context.ConversationScoped
除外。
注解 | 描述 |
---|---|
|
单个 Bean 实例用于应用程序,并在所有注入点之间共享。该实例是延迟创建的,即一旦在客户端代理上调用了方法。 |
|
与 |
|
Bean 实例与当前请求(通常是 HTTP 请求)相关联。 |
|
这是一个伪范围。实例不共享,每个注入点都会生成一个新的依赖 Bean 实例。依赖 Bean 的生命周期绑定到注入它的 Bean - 它将与注入它的 Bean 一起创建和销毁。 |
|
此范围由 |
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 有几种类型?
是的。通常,我们区分
-
类 Bean
-
生成器方法
-
生成器字段
-
合成 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 类型集。在本例中,它将是 double 和 java.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。