- Quarkus - Introduction to Contexts and Dependency Injection
- 1. Q: OK. Let’s start simple. What is a bean?
- 2. Q: Wait a minute. What does “container-managed” mean?
- 3. Q: What is it good for?
- 4. Q: What does a bean look like?
- 5. Q: Nice. How does the dependency resolution work? I see no names or identifiers.
- 6. Q: Hm, wait a minute. What happens if multiple beans declare the same type?
- 7. Q: Can I use setter and constructor injection?
- 8. Q: You talked about some qualifiers?
- 9. Q: Looks good. What is the bean scope?
- 10. Q: What scopes can I actually use in my Quarkus application?
- 11. Q: I don’t undestand the concept of client proxies.
- 12. Q: OK. You said that there are several kinds of beans?
- 13. Q: OK, injection looks cool. What other services are provided?
- 14. Conclusion
Quarkus - Introduction to Contexts and Dependency Injection
In this guide we’re going to describe the basic principles of the Quarkus programming model that is based on the Contexts and Dependency Injection for Java 2.0 specification.
1. Q: OK. Let’s start simple. What is a bean?
A: Well, a bean is a container-managed object that supports a set of basic services, such as injection of dependencies, lifecycle callbacks and interceptors.
2. Q: Wait a minute. What does “container-managed” mean?
A: 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.
3. Q: What is it good for?
A: 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.
You’ve probably heard of the inversion of control (IoC) programming principle. Dependency injection is one of the implementation techniques of IoC. |
4. Q: What does a bean look like?
A: There are several kinds of beans. The most common ones are class-based beans:
Simple Bean Example
import javax.inject.Inject;
import javax.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 about interceptors later. |
5. Q: Nice. How does the dependency resolution work? I see no names or identifiers.
A: 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
.
6. Q: Hm, wait a minute. What happens if multiple beans declare the same type?
A: 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.
Your can use programmatic lookup via
|
7. Q: Can I use setter and constructor injection?
A: Yes, you can. In fact, in CDI the “setter injection” is superseded by more powerful initializer methods. Intializers may accept multiple parameters and don’t have to follow the JavaBean naming conventions.
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 | 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. |
8. Q: You talked about some qualifiers?
A: 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.
A qualifier type is a Java annotation defined as @Retention(RUNTIME)
and annotated with the @javax.inject.Qualifier
meta-annotation:
Qualifier Example
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Superior {}
The qualifiers of a bean are declared by annotating the bean class or producer method or field with the qualifier types:
Bean With Custom Qualifier Example
@Superior (1)
@ApplicationScoped
public class SuperiorTranslator extends Translator {
String translate(String sentence) {
// ...
}
}
1 | @Superior is a qualifier annotation. |
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.
9. Q: Looks good. What is the bean scope?
The scope of a bean determines the lifecycle of its instances, i.e. when and where an instance should be created and destroyed.
Every bean has exactly one scope. |
10. Q: What scopes can I actually use in my Quarkus application?
A: You can use all the built-in scopes mentioned by the specification except for javax.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 proxy. |
| 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 an |
There can be other custom scopes provided by Quarkus extensions. For example, quarkus-narayana-jta provides javax.transaction.TransactionScoped . |
11. Q: I don’t undestand the concept of client proxies.
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.
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 | 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.
12. Q: OK. You said that there are several kinds of beans?
A: Yes. In general, we distinguish:
Class beans
Producer methods
Producer fields
Synthetic beans
Synthetic beans are usually provided by extensions. Therefore, we not going to cover them in this guide. |
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.
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 | 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. |
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.
13. Q: OK, injection looks cool. What other services are provided?
13.1. Lifecycle Callbacks
A bean class may declare lifecycle @PostConstruct
and @PreDestroy
callbacks:
Lifecycle Callbacks Example
import javax.annotation.PostConstruct;
import javax.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. |
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. |
13.2. Interceptors
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.
Simple Interceptor Example
import javax.interceptor.Interceptor;
import javax.annotation.Priority;
@Logged (1)
@Priority(2020) (2)
@Interceptor (3)
public class LoggingInterceptor {
@Inject (4)
Logger logger;
@AroundInvoke (5)
Objec logInvocation(InvocationContext context) {
// ...log before
Objec ret = context.proceed(); (6)
// ...log after
return ret;
}
}
1 | This is an interceptor binding annotation that is used to bind our interceptor to a bean. Simply annotate a bean class with @Logged . |
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 instance may be the target of dependency injection. |
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. |
Instances of interceptors are dependent objects of the bean instance they intercept, i.e. a new interceptor instance is created for each intercepted bean. |
13.3. Events and Observers
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.
Simple Event Example
class TaskCompleted {
// ...
}
@ApplicationScoped
class ComplicatedService {
@Inject
Event<Task> event; (1)
void doSomething() {
// ...
event.fire(new TaskCompleted()); (2)
}
}
@ApplicationScoped
class Logger {
void onTaskCompleted(@Observes TaskCompleted task) { (3)
// ...log the task
}
}
1 | javax.enterprise.event.Event is used to fire events. |
2 | Fire the event synchronously. |
3 | This method is notified when a TaskCompleted event is fired. |
For more info about events/observers visit Weld docs. |
14. Conclusion
In this guide, we’ve covered some of the basic topics of the Quarkus programming model that is based on the Contexts and Dependency Injection for Java 2.0 specification. However, a full CDI implementation is not used under the hood. Quarkus only implements a subset of the CDI features - see also the list of supported features and the list of limitations. On the other hand, there are quite a few non-standard features and Quarkus-specific APIs. We believe that our efforts will drive the innovation of the CDI specification towards the build-time oriented developer stacks in the future.
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. |