Introduction to Contexts and Dependency Injection (CDI)
在本指南中,我们将介绍基于 Jakarta Contexts and Dependency Injection 4.1规范的 Quarkus 编程模型的基本原则。
- OK. Let’s start simple. What is a bean?
- Wait a minute. What does "container-managed" mean?
- What is it good for?
- What does a bean look like?
- Nice. How does the dependency resolution work? I see no names or identifiers.
- Hm, wait a minute. What happens if multiple beans declare the same type?
- Can I use setter and constructor injection?
- You talked about some qualifiers?
- Looks good. What is the bean scope?
- What scopes can I actually use in my Quarkus application?
@ApplicationScoped
and@Singleton
look very similar. Which one should I choose for my Quarkus application?- I don’t understand the concept of client proxies.
- OK. You said that there are several kinds of beans?
- OK, injection looks cool. What other services are provided?
- Conclusion
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:
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 类型:Translator
和 java.lang.Object
。随后,如果 Bean 具有与 required type 匹配的 Bean 类型并具有所有 required qualifiers,则 Bean 可分配给注入点。我们稍后讨论限定符。现在,只需知道上面的 Bean 可以分配给类型为 Translator
和 java.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
。这非常有用,因为每当容器无法为任何注入点找到明确的依赖项时,你的应用程序都会快速失败。
你可以使用
|
Can I use setter and constructor injection?
是的,你可以。事实上,在 CDI 中,“setter 注入”已经被功能更强大的 initializer methods 取代。初始化器可以接受多个参数,并且不必遵守 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 实现中不起作用,其中具有正常范围的 Bean 必须始终声明一个无参数构造函数,并且 Bean 构造函数必须使用 @Inject 进行注解。然而,在 Quarkus 中,我们检测到没有无参构造函数并直接在字节码中“添加”了它。如果只存在一个构造函数,则不必添加 @Inject 。 |
2 | 初始化器方法必须使用 @Inject 进行注解。 |
3 | 初始化器可以接受多个参数,每个参数都是一个注入点。 |
You talked about some qualifiers?
Qualifiers 是注解,帮助容器区分实现了相同类型的 Bean。正如我们之前所说,如果 Bean 具有所有必需的限定符,则可以将其分配给注入点。如果你在注入点未声明任何限定符,则假定 @Default
限定符。
限定符类型是一个 Java 注解,定义为 @Retention(RUNTIME)
并使用 @jakarta.inject.Qualifier
元注解进行注解:
@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 是 qualifier annotation。 |
这个 Bean 可以分配给 @Inject @Superior Translator
和 @Inject @Superior SuperiorTranslator
,但不能分配给 @Inject Translator
。原因是 @Inject Translator
在类型安全解析期间会自动转换为 @Inject @Default Translator
。由于我们的 SuperiorTranslator
未声明 @Default
,因此只有原始 Translator
Bean 可以分配。
What scopes can I actually use in my Quarkus application?
你可以使用规范中提到的所有内置作用域,jakarta.enterprise.context.ConversationScoped
除外。
Annotation | Description |
---|---|
|
单个 bean 实例可用于应用程序,并且在所有注入点之间共享。该实例是延迟创建的,即一旦在一个方法调用 client proxy。 |
|
与 |
|
bean 实例与当前 request 关联(通常是 HTTP 请求)。 |
|
这是一个伪作用域。不共享实例,每个注入点都生成一个新的依赖 bean 实例。依赖 bean 的生命周期与注入它的 bean 相关联——它将与注入它的 bean 一起创建和销毁。 |
|
此作用域由 |
Quarkus 扩展可以提供其他自定义作用域。例如, |
@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 的字段,否则你会使用非上下文的或陈旧的数据。
@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?
对的。一般来说,我们区分:
-
Class beans
-
Producer methods
-
Producer fields
-
Synthetic beans
通常由扩展提供合成 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 元数据。_type_用于构建 bean 类型集。在这种情况下,它将是 double`和 `java.lang.Object 。没有声明作用域注解,因此它默认为 @Dependent 。 |
2 | 容器在创建 bean 实例时将读取该字段。 |
3 | 容器分析方法注释以构建 Bean 元数据。return type 用于构建 Bean 类型集。在这种情况下,它将是 List<String> 、Collection<String> 、Iterable<String> 和 java.lang.Object 。没有声明范围注释,因此默认为 @Dependent 。 |
4 | 容器在创建 bean 实例时将调用此方法。 |
关于生产程序还有更多内容。你可以声明限定符,将依赖项注入到生产程序方法参数中,等等。你可以进一步了解生产程序,例如,在 Weld docs中。
OK, injection looks cool. What other services are provided?
Lifecycle Callbacks
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。 |
Interceptors
拦截器用于将横切关注点与业务逻辑分离。有一个独立规范 - 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 | Marks an interceptor component. |
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 。如果某个 Bean 类继承自 MyService ,则 `LoggingInterceptor`也会应用于它。 |
Decorators
修饰器类似于拦截器,但由于它们实现了具有业务语义的接口,因此能够实现业务逻辑。
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 对象都可以作为事件有效负载。可选限定符充当主题选择器。
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 features和 Quarkus-specific APIs。
如果您希望了解有关 Quarkus 特定功能和限制的更多信息,请访问 Quarkus CDI Reference Guide。我们还建议您阅读 CDI specification和 Weld documentation(Weld 是一种 CDI 参考实现),以便熟悉更复杂的话题。 |