Introduction to Contexts and Dependency Injection (CDI)
在本指南中,我们将介绍基于 Jakarta Contexts and Dependency Injection 4.1规范的 Quarkus 编程模型的基本原则。
In this guide we’re going to describe the basic principles of the Quarkus programming model that is based on the Jakarta Contexts and Dependency Injection 4.1 specification.
OK. Let’s start simple. What is a bean?
一个 Bean 是一个 container-managed 对象,它支持一系列基本服务,如依赖关系注入、生命周期回调和拦截器。
Well, a bean is a container-managed object that supports a set of basic services, such as injection of dependencies, lifecycle callbacks and interceptors.
Wait a minute. What does "container-managed" mean?
简而言之,你不直接控制对象实例的生命周期。相反,你可以通过声明性方式(如注释、配置等)影响生命周期。容器是你的应用程序运行的 environment。它创建和销毁 Bean 实例,将实例与指定上下文相关联,并将其注入到其他 Bean 中。
Simply put, you don’t control the lifecycle of the object instance directly. Instead, you can affect the lifecycle through declarative means, such as annotations, configuration, etc. The container is the environment where your application runs. It creates and destroys the instances of beans, associates the instances with a designated context, and injects them into other beans.
What is it good for?
应用程序开发人员可以专注于业务逻辑,而不是找出“在哪里和如何”获取包含所有依赖关系的已完全初始化的组件。
An application developer can focus on the business logic rather than finding out "where and how" to obtain a fully initialized component with all of its dependencies.
你可能听说过 inversion of control (IoC) 编程原则。依赖关系注入是 IoC 的实现技术之一。 |
You’ve probably heard of the inversion of control (IoC) programming principle. Dependency injection is one of the implementation techniques of IoC. |
What does a bean look like?
有几种类型的 Bean。最常见的是基于类的 Bean:
There are several kinds of beans. The most common ones are class-based beans:
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 | This is a scope annotation. It tells the container which context to associate the bean instance with. In this particular case, a single bean instance is created for the application and used by all other beans that inject Translator . |
2 | This is a field injection point. It tells the container that Translator depends on the Dictionary bean. If there is no matching bean the build fails. |
3 | This is an interceptor binding annotation. In this case, the annotation comes from the MicroProfile Metrics. The relevant interceptor intercepts the invocation and updates the relevant metrics. We will talk about interceptors later. |
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
的注入点。
That’s a good question.
In CDI the process of matching a bean to an injection point is type-safe.
Each bean declares a set of bean types.
In our example above, the Translator
bean has two bean types: Translator
and java.lang.Object
.
Subsequently, a bean is assignable to an injection point if the bean has a bean type that matches the required type and has all the required qualifiers.
We’ll talk about qualifiers later.
For now, it’s enough to know that the bean above is assignable to an injection point of type Translator
and 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
。这非常有用,因为每当容器无法为任何注入点找到明确的依赖项时,你的应用程序都会快速失败。
There is a simple rule: exactly one bean must be assignable to an injection point, otherwise the build fails.
If none is assignable the build fails with UnsatisfiedResolutionException
.
If multiple are assignable the build fails with AmbiguousResolutionException
.
This is very useful because your application fails fast whenever the container is not able to find an unambiguous dependency for any injection point.
你可以使用 You can use programmatic lookup via
|
Can I use setter and constructor injection?
是的,你可以。事实上,在 CDI 中,“setter 注入”已经被功能更强大的 initializer methods 取代。初始化器可以接受多个参数,并且不必遵守 JavaBean 命名约定。
Yes, you can. In fact, in CDI the "setter injection" is superseded by more powerful initializer methods. Initializers may accept multiple parameters and don’t have to follow the JavaBean naming conventions.
@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 | This is a constructor injection.
In fact, this code would not work in regular CDI implementations where a bean with a normal scope must always declare a no-args constructor and the bean constructor must be annotated with @Inject .
However, in Quarkus we detect the absence of no-args constructor and "add" it directly in the bytecode.
It’s also not necessary to add @Inject if there is only one constructor present. |
2 | An initializer method must be annotated with @Inject . |
3 | An initializer may accept multiple parameters - each one is an injection point. |
You talked about some qualifiers?
Qualifiers 是注解,帮助容器区分实现了相同类型的 Bean。正如我们之前所说,如果 Bean 具有所有必需的限定符,则可以将其分配给注入点。如果你在注入点未声明任何限定符,则假定 @Default
限定符。
Qualifiers are annotations that help the container to distinguish beans that implement the same type.
As we already said a bean is assignable to an injection point if it has all the required qualifiers.
If you declare no qualifier at an injection point the @Default
qualifier is assumed.
限定符类型是一个 Java 注解,定义为 @Retention(RUNTIME)
并使用 @jakarta.inject.Qualifier
元注解进行注解:
A qualifier type is a Java annotation defined as @Retention(RUNTIME)
and annotated with the @jakarta.inject.Qualifier
meta-annotation:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Superior {}
通过使用限定符类型对 Bean 类或生产者方法或域进行注解,可以声明 Bean 的限定符:
The qualifiers of a bean are declared by annotating the bean class or producer method or field with the qualifier types:
@Superior 1
@ApplicationScoped
public class SuperiorTranslator extends Translator {
String translate(String sentence) {
// ...
}
}
1 | @Superior is a qualifier annotation. |
这个 Bean 可以分配给 @Inject @Superior Translator
和 @Inject @Superior SuperiorTranslator
,但不能分配给 @Inject Translator
。原因是 @Inject Translator
在类型安全解析期间会自动转换为 @Inject @Default Translator
。由于我们的 SuperiorTranslator
未声明 @Default
,因此只有原始 Translator
Bean 可以分配。
This bean would be assignable to @Inject @Superior Translator
and @Inject @Superior SuperiorTranslator
but not to @Inject Translator
.
The reason is that @Inject Translator
is automatically transformed to @Inject @Default Translator
during typesafe resolution.
And since our SuperiorTranslator
does not declare @Default
only the original Translator
bean is assignable.
Looks good. What is the bean scope?
Bean 的范围决定了其实例的生命周期,即何时何地创建和销毁实例。
The scope of a bean determines the lifecycle of its instances, i.e. when and where an instance should be created and destroyed.
每个 bean 都只有一种作用域。 |
Every bean has exactly one scope. |
What scopes can I actually use in my Quarkus application?
你可以使用规范中提到的所有内置作用域,jakarta.enterprise.context.ConversationScoped
除外。
You can use all the built-in scopes mentioned by the specification except for jakarta.enterprise.context.ConversationScoped
.
Annotation | Description |
---|---|
|
A single bean instance is used for the application and shared among all injection points. The instance is created lazily, i.e. once a method is invoked upon the client_proxies. |
|
Just like |
|
The bean instance is associated with the current request (usually an HTTP request). |
|
This is a pseudo-scope. The instances are not shared and every injection point spawns a new instance of the dependent bean. The lifecycle of dependent bean is bound to the bean injecting it - it will be created and destroyed along with the bean injecting it. |
|
This scope is backed by a |
Quarkus 扩展可以提供其他自定义作用域。例如, |
There can be other custom scopes provided by Quarkus extensions. For example, |
@ApplicationScoped
and @Singleton
look very similar. Which one should I choose for my Quarkus application?
它取决于 ;-).
It depends ;-).
@Singleton
bean 没有 client proxy,因此当 bean 注入时,一个实例将会 created eagerly。相比之下,@ApplicationScoped
bean 的实例是 created lazily,即当首次对注入的实例调用一个方法时。
A @Singleton
bean has no client_proxies and hence an instance is created eagerly when the bean is injected. By contrast, an instance of an @ApplicationScoped
bean is created lazily, i.e.
when a method is invoked upon an injected instance for the first time.
此外,客户端代理仅委托方法调用,因此你不应该直接读/写注入的 @ApplicationScoped
bean 的字段。你可以安全地读/写注入 @Singleton
的字段。
Furthermore, client proxies only delegate method invocations and thus you should never read/write fields of an injected @ApplicationScoped
bean directly.
You can read/write fields of an injected @Singleton
safely.
@Singleton
应该具有稍好的性能,因为它没有间接调用(没有代理从上下文委托给当前实例)。
@Singleton
should have a slightly better performance because there is no indirection (no proxy that delegates to the current instance from the context).
另一方面,你不能使用 QuarkusMock 来模拟 @Singleton
bean。
On the other hand, you cannot mock @Singleton
beans using QuarkusMock.
@ApplicationScoped
bean 也可以在运行时销毁和重新创建。现有注入点只是能够正常工作,因为注入的代理委托给当前实例。
@ApplicationScoped
beans can be also destroyed and recreated at runtime.
Existing injection points just work because the injected proxy delegates to the current instance.
因此,我们建议默认情况下坚持使用 @ApplicationScoped
,除非有充分的理由使用 @Singleton
。
Therefore, we recommend to stick with @ApplicationScoped
by default unless there’s a good reason to use @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 类的容器构建。
Indeed, the client proxies could be hard to grasp, but they provide some useful functionality.
A client proxy is basically an object that delegates all method invocations to a target bean instance.
It’s a container construct that implements io.quarkus.arc.ClientProxy
and extends the bean class.
客户端代理只委托方法调用。因此,不要读写常规作用域 bean 的字段,否则你会使用非上下文的或陈旧的数据。
Client proxies only delegate method invocations. So never read or write a field of a normal scoped bean, otherwise you will work with non-contextual or stale data.
@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 | The Translator_ClientProxy instance is always injected instead of a direct reference to a contextual instance of the Translator bean. |
客户端代理允许:
Client proxies allow for:
-
Lazy instantiation - the instance is created once a method is invoked upon the proxy.
-
Ability to inject a bean with "narrower" scope to a bean with "wider" scope; i.e. you can inject a
@RequestScoped
bean into an@ApplicationScoped
bean. -
Circular dependencies in the dependency graph. Having circular dependencies is often an indication that a redesign should be considered, but sometimes it’s inevitable.
-
In rare cases it’s practical to destroy the beans manually. A direct injected reference would lead to a stale bean instance.
OK. You said that there are several kinds of beans?
对的。一般来说,我们区分:
Yes. In general, we distinguish:
-
Class beans
-
Producer methods
-
Producer fields
-
Synthetic beans
通常由扩展提供合成 bean。因此,我们不会在本指南中介绍它们。 |
Synthetic beans are usually provided by extensions. Therefore, we are not going to cover them in this guide. |
如果你需要对 bean 实例化进行更多控制,生产程序方法和字段会很有用。当你集成第三方的库时它们也十分有用 - 在这种情况下,你无法控制类源,也不能添加附加的注解等。
Producer methods and fields are useful if you need additional control over instantiation of a bean. They are also useful when integrating third-party libraries where you don’t control the class source and may not add additional annotations etc.
@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 | The container analyses the field annotations to build a bean metadata.
The type is used to build the set of bean types.
In this case, it will be double and java.lang.Object .
No scope annotation is declared and so it’s defaulted to @Dependent . |
2 | The container will read this field when creating the bean instance. |
3 | The container analyses the method annotations to build a bean metadata.
The return type is used to build the set of bean types.
In this case, it will be List<String> , Collection<String> , Iterable<String> and java.lang.Object .
No scope annotation is declared and so it’s defaulted to @Dependent . |
4 | The container will call this method when creating the bean instance. |
关于生产程序还有更多内容。你可以声明限定符,将依赖项注入到生产程序方法参数中,等等。你可以进一步了解生产程序,例如,在 Weld docs中。
There’s more about producers. You can declare qualifiers, inject dependencies into the producer methods parameters, etc. You can read more about producers for example in the Weld docs.
OK, injection looks cool. What other services are provided?
Lifecycle Callbacks
bean 类可以声明生命周期 `@PostConstruct`和 `@PreDestroy`回调:
A bean class may declare lifecycle @PostConstruct
and @PreDestroy
callbacks:
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@ApplicationScoped
public class Translator {
@PostConstruct 1
void init() {
// ...
}
@PreDestroy 2
void destroy() {
// ...
}
}
1 | This callback is invoked before the bean instance is put into service. It is safe to perform some initialization here. |
2 | This callback is invoked before the bean instance is destroyed. It is safe to perform some cleanup tasks here. |
保持回调逻辑“无副作用”是一个好习惯,即应避免在回调内调用其他 Bean。 |
It’s a good practice to keep the logic in the callbacks "without side effects", i.e. you should avoid calling other beans inside the callbacks. |
Interceptors
拦截器用于将横切关注点与业务逻辑分离。有一个独立规范 - Java 拦截器 - 用来定义基本编程模型和语义。
Interceptors are used to separate cross-cutting concerns from business logic. There is a separate specification - Java Interceptors - that defines the basic programming model and semantics.
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 | This is an interceptor binding annotation. See the following examples for how it’s used. |
2 | An interceptor binding annotation is always put on the interceptor type, and may be put on target types or methods. |
3 | Interceptor bindings are often @Inherited , but don’t have to be. |
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 | The interceptor binding annotation is used to bind our interceptor to a bean. Simply annotate a bean class with @Logged , as in the following example. |
2 | Priority enables the interceptor and affects the interceptor ordering. Interceptors with smaller priority values are called first. |
3 | Marks an interceptor component. |
4 | An interceptor may inject dependencies. |
5 | AroundInvoke denotes a method that interposes on business methods. |
6 | Proceed to the next interceptor in the interceptor chain or invoke the intercepted business method. |
拦截器的实例是它们所拦截的 Bean 实例的依赖项对象,即为每个被拦截的 Bean 创建一个新的拦截器实例。 |
Instances of interceptors are dependent objects of the bean instance they intercept, i.e. a new interceptor instance is created for each intercepted bean. |
import jakarta.enterprise.context.ApplicationScoped;
@Logged // <1> 2
@ApplicationScoped
public class MyService {
void doSomething() {
...
}
}
1 | The interceptor binding annotation is put on a bean class so that all business methods are intercepted. The annotation can also be put on individual methods, in which case, only the annotated methods are intercepted. |
2 | Remember that the @Logged annotation is @Inherited .
If there’s a bean class that inherits from MyService , the LoggingInterceptor will also apply to it. |
Decorators
修饰器类似于拦截器,但由于它们实现了具有业务语义的接口,因此能够实现业务逻辑。
Decorators are similar to interceptors, but because they implement interfaces with business semantics, they are able to implement business logic.
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 enables the decorator. Decorators with smaller priority values are called first. |
2 | @Decorator marks a decorator component. |
3 | The set of decorated types includes all bean types which are Java interfaces, except for java.io.Serializable . |
4 | Each decorator must declare exactly one delegate injection point. The decorator applies to beans that are assignable to this delegate injection point. |
5 | Decorators can inject other beans. |
6 | The decorator may invoke any method of the delegate object. And the container invokes either the next decorator in the chain or the business method of the intercepted instance. |
装饰器实例是其所拦截的 bean 实例的依赖对象,即为每个拦截的 bean 创建一个新的装饰器实例。 |
Instances of decorators are dependent objects of the bean instance they intercept, i.e. a new decorator instance is created for each intercepted bean. |
Events and Observers
bean 还可以生成和使用事件,以完全解耦的方式进行交互。任何 Java 对象都可以作为事件有效负载。可选限定符充当主题选择器。
Beans may also produce and consume events to interact in a completely decoupled fashion. Any Java object can serve as an event payload. The optional qualifiers act as topic selectors.
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 is used to fire events. |
2 | Fire the event synchronously. |
3 | This method is notified when a TaskCompleted event is fired. |
有关事件/观察者的更多信息,请访问 Weld docs。 |
For more info about events/observers visit 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。
In this guide, we’ve covered some basic topics of the Quarkus programming model that is based on the Jakarta Contexts and Dependency Injection 4.1 specification. Quarkus implements the CDI Lite specification, but not CDI Full. See also the list of supported features and limitations. There are also quite a few non-standard features and Quarkus-specific APIs.
如果您希望了解有关 Quarkus 特定功能和限制的更多信息,请访问 Quarkus CDI Reference Guide。我们还建议您阅读 CDI specification和 Weld documentation(Weld 是一种 CDI 参考实现),以便熟悉更复杂的话题。 |
If you wish to learn more about Quarkus-specific features and limitations there is a Quarkus CDI Reference Guide. We also recommend you to read the CDI specification and the Weld documentation (Weld is a CDI Reference Implementation) to get acquainted with more complex topics. |