./attributes.adoc :numbered: :toc: :toclevels: 2
ArC, the CDI container, is bootstrapped at build time. The downside of this approach is that CDI Portable Extensions cannot be supported. Nevertheless, the functionality can be achieved using the Quarkus-specific extensions API.
The container is bootstrapped in multiple phases. From a high level perspective these phases go as follows:
-
Initialization
-
Bean discovery
-
Registration of synthetic components
-
Validation
In the initialization phase the preparatory work is being carried out and custom contexts are registered. Bean discovery is then the process where the container analyzes all application classes, identifies beans and wires them all together based on the provided metadata. Subsequently, the extensions can register synthetic components. Attributes of these components are fully controlled by the extensions, i.e. are not derived from an existing class. Finally, the deployment is validated. For example, the container validates every injection point in the application and fails the build if there is no bean that satisfies the given required type and qualifiers.
Tip
|
You can see more information about the bootstrap by enabling additional logging. Simply run the Maven build with -X or --debug and grep the lines that contain io.quarkus.arc . In the development mode, you can use quarkus.log.category."io.quarkus.arc.processor".level=DEBUG and two special endpoints are also registered automatically to provide some basic debug info in the JSON format.
|
Quarkus build steps can produce and consume various build items and hook into each phase. In the following sections we will describe all the relevant build items and common scenarios.
Classes and annotations are the primary source of bean-level metadata. The initial metadata are read from the bean archive index, an immutable Jandex index which is built from various sources during bean discovery. However, extensions can add, remove or transform the metadata at certain stages of the bootstrap. Moreover, extensions can also register synthetic components. This is an important aspect to realize when integrating CDI components in Quarkus.
This way, extensions can turn classes, that would be otherwise ignored, into beans and vice versa.
For example, a class that declares a @Scheduled
method is always registered as a bean even if it is not annotated with a bean defining annotation and would be normally ignored.
An UnsatisfiedResolutionException
indicates a problem during typesafe resolution.
Sometimes an injection point cannot be satisfied even if there is a class on the classpath that appears to be eligible for injection.
There are several reasons why a class is not recognized and also several ways to fix it.
In the first step we should identify the reason.
Quarkus has a simplified discovery. It might happen that the class is not part of the application index. For example, classes from the runtime module of a Quarkus extension are not indexed automatically.
Solution: Use the AdditionalBeanBuildItem
.
This build item can be used to specify one or more additional classes to be analyzed during the discovery.
Additional bean classes are transparently added to the application index processed by the container.
Important
|
It is not possible to conditionally enable/disable additional beans via the @IfBuildProfile , @UnlessBuildProfile , @IfBuildProperty and @UnlessBuildProperty annotations as described in cdi-reference.adoc and cdi-reference.adoc. Extensions should inspect the configuration or the current profile and only produce an AdditionalBeanBuildItem if really needed.
|
AdditionalBeanBuildItem
Example@BuildStep
AdditionalBeanBuildItem additionalBeans() {
return new AdditionalBeanBuildItem(SmallRyeHealthReporter.class, HealthServlet.class)); (1)
}
-
AdditionalBeanBuildItem.Builder
can be used for more complex use cases.
Bean classes added via AdditionalBeanBuildItem
are removable by default.
If the container considers them unused, they are just ignored.
However, you can use AdditionalBeanBuildItem.Builder.setUnremovable()
method to instruct the container to never remove bean classes registered via this build item.
See also Removing Unused Beans and Reason 3: Class Was Discovered and Has a Bean Defining Annotation but Was Removed for more details.
It is aso possible to set the default scope via AdditionalBeanBuildItem.Builder#setDefaultScope()
.
The default scope is only used if there is no scope declared on the bean class.
Note
|
If no default scope is specified the @Dependent pseudo-scope is used.
|
In Quarkus, the application is represented by a single bean archive with the bean discovery mode annotated
.
Therefore, bean classes that don’t have a bean defining annotation are ignored.
Bean defining annotations are declared on the class-level and include scopes, stereotypes and @Interceptor
.
Solution 1: Use the AutoAddScopeBuildItem
. This build item can be used to add a scope to a class that meets certain conditions.
AutoAddScopeBuildItem
Example@BuildStep
AutoAddScopeBuildItem autoAddScope() {
return AutoAddScopeBuildItem.builder().containsAnnotations(SCHEDULED_NAME, SCHEDULES_NAME) (1)
.defaultScope(BuiltinScope.SINGLETON) (2)
.build();
}
-
Find all classes annotated with
@Scheduled
. -
Add
@Singleton
as default scope. Classes already annotated with a scope are skipped automatically.
Solution 2: If you need to process classes annotated with a specific annotation then it’s possible to extend the set of bean defining annotations via the BeanDefiningAnnotationBuildItem
.
BeanDefiningAnnotationBuildItem
Example@BuildStep
BeanDefiningAnnotationBuildItem additionalBeanDefiningAnnotation() {
return new BeanDefiningAnnotationBuildItem(Annotations.GRAPHQL_API); (1)
}
-
Add
org.eclipse.microprofile.graphql.GraphQLApi
to the set of bean defining annotations.
Bean classes added via BeanDefiningAnnotationBuildItem
are not removable by default, i.e. the resulting beans must not be removed even if they are considered unused.
However, you can change the default behavior.
See also Removing Unused Beans and Reason 3: Class Was Discovered and Has a Bean Defining Annotation but Was Removed for more details.
It is also possible to specify the default scope. The default scope is only used if there is no scope declared on the bean class.
Note
|
If no default scope is specified the @Dependent pseudo-scope is used.
|
The container attempts to remove all unused beans during the build by default.
This optimization allows for framework-level dead code elimination.
In few special cases, it’s not possible to correctly identify an unused bean.
In particular, Quarkus is not able to detect the usage of the CDI.current()
static method yet.
Extensions can eliminate possible false positives by producing an UnremovableBeanBuildItem
.
UnremovableBeanBuildItem
Example@BuildStep
UnremovableBeanBuildItem unremovableBeans() {
return UnremovableBeanBuildItem.targetWithAnnotation(STARTUP_NAME); (1)
}
-
Make all classes annotated with
@Startup
unremovable.
It is likely that the annotation class is not part of the application index. For example, classes from the runtime module of a Quarkus extension are not indexed automatically.
Solution: Use the AdditionalBeanBuildItem
as described in Reason 1: Class Is Not discovered.
In some cases, it’s useful to be able to modify the metadata.
Quarkus provides a powerful alternative to javax.enterprise.inject.spi.ProcessAnnotatedType
.
With an AnnotationsTransformerBuildItem
it’s possible to override the annotations that exist on bean classes.
For example, you might want to add an interceptor binding to a specific bean class. Here is how to do it:
AnnotationsTransformerBuildItem
Example@BuildStep
AnnotationsTransformerBuildItem transform() {
return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {
public boolean appliesTo(org.jboss.jandex.AnnotationTarget.Kind kind) {
return kind == org.jboss.jandex.AnnotationTarget.Kind.CLASS; (1)
}
public void transform(TransformationContext context) {
if (context.getTarget().asClass().name().toString().equals("org.acme.Bar")) {
context.transform().add(MyInterceptorBinding.class).done(); (2)
}
}
});
}
-
The transformer is only applied to classes.
-
If the class name equals to
org.acme.Bar
then add@MyInterceptorBinding
. Don’t forget to invokeTransformation#done()
.
Note
|
Keep in mind that annotation transformers must be produced before the bean discovery starts. |
Build steps can query the transformed annotations for a given annotation target via the TransformedAnnotationsBuildItem
.
TransformedAnnotationsBuildItem
Example@BuildStep
void queryAnnotations(TransformedAnnotationsBuildItem transformedAnnotations, BuildProducer<MyBuildItem> myBuildItem) {
ClassInfo myClazz = ...;
if (transformedAnnotations.getAnnotations(myClazz).isEmpty()) { (1)
myBuildItem.produce(new MyBuildItem());
}
}
-
TransformedAnnotationsBuildItem.getAnnotations()
will return a possibly transformed set of annotations.
Note
|
There are other build items specialized in transformation: Use Case - Additional Interceptor Bindings and Use Case - Injection Point Transformation. |
Consumers of BeanDiscoveryFinishedBuildItem
can easily inspect all class-based beans, observers and injection points registered in the application.
However, synthetic beans and observers are not included because this build item is produced before the synthetic components are registered.
Additionaly, the bean resolver returned from BeanDiscoveryFinishedBuildItem#getBeanResolver()
can be used to apply the type-safe resolution rules, e.g. to find out whether there is a bean that would satisfy certain combination of required type and qualifiers.
BeanDiscoveryFinishedBuildItem
Example@BuildStep
void doSomethingWithNamedBeans(BeanDiscoveryFinishedBuildItem beanDiscovery, BuildProducer<NamedBeansBuildItem> namedBeans) {
List<BeanInfo> namedBeans = beanDiscovery.beanStream().withName().collect(toList())); (1)
namedBeans.produce(new NamedBeansBuildItem(namedBeans));
}
-
The resulting list will not contain
@Named
synthetic beans.
Consumers of SynthesisFinishedBuildItem
can easily inspect all beans, observers and injection points registered in the application. Synthetic beans and observers are included because this build item is produced after the synthetic components are registered.
Additionaly, the bean resolver returned from SynthesisFinishedBuildItem#getBeanResolver()
can be used to apply the type-safe resolution rules, e.g. to find out whether there is a bean that would satisfy certain combination of required type and qualifiers.
SynthesisFinishedBuildItem
Example@BuildStep
void doSomethingWithNamedBeans(SynthesisFinishedBuildItem synthesisFinished, BuildProducer<NamedBeansBuildItem> namedBeans) {
List<BeanInfo> namedBeans = synthesisFinished.beanStream().withName().collect(toList())); (1)
namedBeans.produce(new NamedBeansBuildItem(namedBeans));
}
-
The resulting list will contain
@Named
synthetic beans.
Sometimes it is practical to be able to register a synthetic bean.
Bean attributes of a synthetic bean are not derived from a Java class, method or field.
Instead, all the attributes are defined by an extension.
In regular CDI, this could be achieved using the AfterBeanDiscovery.addBean()
methods.
Solution: If you need to register a synthetic bean then use the SyntheticBeanBuildItem
.
SyntheticBeanBuildItem
Example 1@BuildStep
SyntheticBeanBuildItem syntheticBean() {
return SyntheticBeanBuildItem.configure(String.class)
.qualifiers(new MyQualifierLiteral())
.creator(mc -> mc.returnValue(mc.load("foo"))) (1)
.done();
}
-
Generate the bytecode of the
javax.enterprise.context.spi.Contextual#create(CreationalContext<T>)
implementation.
The output of a bean configurator is recorded as bytecode. Therefore, there are some limitations in how a synthetic bean instance is created at runtime. You can:
-
Generate the bytecode of the
Contextual#create(CreationalContext<T>)
method directly viaExtendedBeanConfigurator.creator(Consumer<MethodCreator>)
. -
Pass a
io.quarkus.arc.BeanCreator
implementation class viaExtendedBeanConfigurator#creator(Class<? extends BeanCreator<U>>)
, and possibly specify some parameters viaExtendedBeanConfigurator#param()
. -
Produce the runtime instance through a proxy returned from a
@Recorder
method and set it viaExtendedBeanConfigurator#runtimeValue(RuntimeValue<?>)
orExtendedBeanConfigurator#supplier(Supplier<?>)
.
SyntheticBeanBuildItem
Example 2@BuildStep
@Record(STATIC_INIT) (1)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
return SyntheticBeanBuildItem.configure(Foo.class).scope(Singleton.class)
.runtimeValue(recorder.createFoo()) (2)
.done();
}
-
By default, a synthetic bean is initialized during
STATIC_INIT
. -
The bean instance is supplied by a value returned from a recorder method.
It is possible to mark a synthetic bean to be initialized during RUNTIME_INIT
.
See the Three Phases of Bootstrap and Quarkus Philosophy for more information about the difference between STATIC_INIT
and RUNTIME_INIT
.
RUNTIME_INIT
SyntheticBeanBuildItem
Example@BuildStep
@Record(RUNTIME_INIT) (1)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
return SyntheticBeanBuildItem.configure(Foo.class).scope(Singleton.class)
.setRuntimeInit() (2)
.runtimeValue(recorder.createFoo())
.done();
}
-
The recorder must be executed in the
ExecutionTime.RUNTIME_INIT
phase. -
The bean instance is initialized during
RUNTIME_INIT
.
Important
|
Synthetic beans initialized during @BuildStep
@Record(RUNTIME_INIT)
@Consume(SyntheticBeansRuntimeInitBuildItem.class) (1)
void accessFoo(TestRecorder recorder) {
recorder.foo(); (2)
}
|
Note
|
It is also possible to use the BeanRegistrationPhaseBuildItem to register a synthetic bean. However, we recommend extension authors to stick with SyntheticBeanBuildItem which is more idiomatic for Quarkus.
|
Similar to synthetic beans, the attributes of a synthetic observer method are not derived from a Java method. Instead, all the attributes are defined by an extension.
Solution: If you need to register a synthetic observer, use the ObserverRegistrationPhaseBuildItem
.
Important
|
A build step that consumes the ObserverRegistrationPhaseBuildItem should always produce an ObserverConfiguratorBuildItem or at least inject a BuildProducer for this build item, otherwise it could be ignored or processed at the wrong time (e.g. after the correct CDI bootstrap phase).
|
ObserverRegistrationPhaseBuildItem
Example@BuildStep
void syntheticObserver(ObserverRegistrationPhaseBuildItem observerRegistrationPhase,
BuildProducer<MyBuildItem> myBuildItem,
BuildProducer<ObserverConfiguratorBuildItem> observerConfigurationRegistry) {
observerConfigurationRegistry.produce(new ObserverConfiguratorBuildItem(observerRegistrationPhase.getContext()
.configure()
.beanClass(DotName.createSimple(MyBuildStep.class.getName()))
.observedType(String.class)
.notify(mc -> {
// do some gizmo bytecode generation...
})));
myBuildItem.produce(new MyBuildItem());
}
The output of a ObserverConfigurator
is recorded as bytecode.
Therefore, there are some limitations in how a synthetic observer is invoked at runtime.
Currently, you must generate the bytecode of the method body directly.
No problem.
You can generate the bytecode of a bean class manually and then all you need to do is to produce a GeneratedBeanBuildItem
instead of GeneratedClassBuildItem
.
GeneratedBeanBuildItem
Example@BuildStep
void generatedBean(BuildProducer<GeneratedBeanBuildItem> generatedBeans) {
ClassOutput beansClassOutput = new GeneratedBeanGizmoAdaptor(generatedBeans); (1)
ClassCreator beanClassCreator = ClassCreator.builder().classOutput(beansClassOutput)
.className("org.acme.MyBean")
.build();
beanClassCreator.addAnnotation(Singleton.class);
beanClassCreator.close(); (2)
}
-
io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor
makes it easy to produceGeneratedBeanBuildItem
s from Gizmo constructs. -
The resulting bean class is something like
public class @Singleton MyBean { }
.
Sometimes extensions need to inspect the beans, observers and injection points, then perform additional validations and fail the build if something is wrong.
Solution: If an extension needs to validate the deployment it should use the ValidationPhaseBuildItem
.
Important
|
A build step that consumes the ValidationPhaseBuildItem should always produce a ValidationErrorBuildItem or at least inject a BuildProducer for this build item, otherwise it could be ignored or processed at the wrong time (e.g. after the correct CDI bootstrap phase).
|
@BuildStep
void validate(ValidationPhaseBuildItem validationPhase,
BuildProducer<MyBuildItem> myBuildItem,
BuildProducer<ValidationErrorBuildItem> errors) {
if (someCondition) {
errors.produce(new ValidationErrorBuildItem(new IllegalStateException()));
myBuildItem.produce(new MyBuildItem());
}
}
Tip
|
You can easily filter all registered beans via the convenient BeanStream returned from the ValidationPhaseBuildItem.getContext().beans() method.
|
Sometimes extensions need to extend the set of built-in CDI contexts.
Solution: If you need to register a custom context, use the ContextRegistrationPhaseBuildItem
.
Important
|
A build step that consumes the ContextRegistrationPhaseBuildItem should always produce a ContextConfiguratorBuildItem or at least inject a BuildProducer for this build item, otherwise it could be ignored or processed at the wrong time (e.g. after the correct CDI bootstrap phase).
|
ContextRegistrationPhaseBuildItem
Example
@BuildStep
ContextConfiguratorBuildItem registerContext(ContextRegistrationPhaseBuildItem phase) {
return new ContextConfiguratorBuildItem(phase.getContext().configure(TransactionScoped.class).normal().contextClass(TransactionContext.class));
}
Additionally, each extension that registers a custom CDI context via ContextRegistrationPhaseBuildItem
should also produce the CustomScopeBuildItem
in order to contribute the custom scope annotation name to the set of bean defining annotations.
CustomScopeBuildItem
Example
@BuildStep
CustomScopeBuildItem customScope() {
return new CustomScopeBuildItem(DotName.createSimple(TransactionScoped.class.getName()));
}
In rare cases it might be handy to programmatically register an existing annotation that is not annotated with @javax.interceptor.InterceptorBinding
as an interceptor binding.
This is similar to what CDI achieves through BeforeBeanDiscovery#addInterceptorBinding()
.
We are going to use InterceptorBindingRegistrarBuildItem
to get it done.
InterceptorBindingRegistrarBuildItem
Example@BuildStep
InterceptorBindingRegistrarBuildItem addInterceptorBindings() {
return new InterceptorBindingRegistrarBuildItem(new InterceptorBindingRegistrar() {
@Override
public List<InterceptorBinding> getAdditionalBindings() {
return List.of(InterceptorBinding.of(NotAnInterceptorBinding.class));
}
});
}
Sometimes it might be useful to register an existing annotation that is not annotated with @javax.inject.Qualifier
as a CDI qualifier.
This is similar to what CDI achieves through BeforeBeanDiscovery#addQualifier()
.
We are going to use QualifierRegistrarBuildItem
to get it done.
QualifierRegistrarBuildItem
Example@BuildStep
QualifierRegistrarBuildItem addQualifiers() {
return new QualifierRegistrarBuildItem(new QualifierRegistrar() {
@Override
public Map<DotName, Set<String>> getAdditionalQualifiers() {
return Collections.singletonMap(DotName.createSimple(NotAQualifier.class.getName()),
Collections.emptySet());
}
});
}
Every now and then it is handy to be able to change the qualifiers of an injection point programmatically.
You can do just that with InjectionPointTransformerBuildItem
.
The following sample shows how to apply transformation to injection points with type Foo
that contain qualifier MyQualifier
:
InjectionPointTransformerBuildItem
Example@BuildStep
InjectionPointTransformerBuildItem transformer() {
return new InjectionPointTransformerBuildItem(new InjectionPointsTransformer() {
public boolean appliesTo(Type requiredType) {
return requiredType.name().equals(DotName.createSimple(Foo.class.getName()));
}
public void transform(TransformationContext context) {
if (context.getQualifiers().stream()
.anyMatch(a -> a.name().equals(DotName.createSimple(MyQualifier.class.getName())))) {
context.transform()
.removeAll()
.add(DotName.createSimple(MyOtherQualifier.class.getName()))
.done();
}
}
});
}
Note
|
In theory, you can use an AnnotationsTransformer to achieve the same goal. However, there are few differences that make InjectionPointsTransformer more suitable for this particular task: (1) annotation transformers are applied to all classes during bean discovery, whereas InjectionPointsTransformer is only applied to discovered injection points after bean discovery; (2) with InjectionPointsTransformer you don’t need to handle various types of injection points (field, parameters of initializer methods, etc.).
|
The ResourceAnnotationBuildItem
can be used to specify resource annotations that make it possible to resolve non-CDI injection points, such as Jakarta EE resources.
An integrator must also provide a corresponding io.quarkus.arc.ResourceReferenceProvider
service provider implementation.
ResourceAnnotationBuildItem
Example@BuildStep
void setupResourceInjection(BuildProducer<ResourceAnnotationBuildItem> resourceAnnotations, BuildProducer<GeneratedResourceBuildItem> resources) {
resources.produce(new GeneratedResourceBuildItem("META-INF/services/io.quarkus.arc.ResourceReferenceProvider",
MyResourceReferenceProvider.class.getName().getBytes()));
resourceAnnotations.produce(new ResourceAnnotationBuildItem(DotName.createSimple(MyAnnotation.class.getName())));
}
Any of the above extensions that operates with BuildExtension.BuildContext
can leverage certain build time metadata that are generated during build.
The built-in keys located in io.quarkus.arc.processor.BuildExtension.Key
are:
- ANNOTATION_STORE
-
Contains an
AnnotationStore
that keeps information about allAnnotationTarget
annotations after application of annotation transformers - INJECTION_POINTS
-
Collection<InjectionPointInfo>
containing all injection points - BEANS
-
Collection<BeanInfo>
containing all beans - REMOVED_BEANS
-
Collection<BeanInfo>
containing all the removed beans; see Removing unused beans for more information - OBSERVERS
-
Collection<ObserverInfo>
containing all observers - SCOPES
-
Collection<ScopeInfo>
containing all scopes, including custom ones - QUALIFIERS
-
Map<DotName, ClassInfo>
containing all qualifiers - INTERCEPTOR_BINDINGS
-
Map<DotName, ClassInfo>
containing all interceptor bindings - STEREOTYPES
-
Map<DotName, StereotypeInfo>
containing all stereotypes
To get hold of these, simply query the extension context object for given key.
Note that these metadata are made available as build proceeds which means that extensions can only leverage metadata that were built before the extensions are invoked.
If your extension attempts to retrieve metadata that wasn’t yet produced, null
will be returned.
Here is a summary of which extensions can access which metadata:
- AnnotationsTransformer
-
Shouldn’t rely on any metadata as it could be used at any time in any phase of the bootstrap
- ContextRegistrar
-
Has access to
ANNOTATION_STORE
,QUALIFIERS
,INTERCEPTOR_BINDINGS
,STEREOTYPES
- InjectionPointsTransformer
-
Has access to
ANNOTATION_STORE
,QUALIFIERS
,INTERCEPTOR_BINDINGS
,STEREOTYPES
- ObserverTransformer
-
Has access to
ANNOTATION_STORE
,QUALIFIERS
,INTERCEPTOR_BINDINGS
,STEREOTYPES
- BeanRegistrar
-
Has access to
ANNOTATION_STORE
,QUALIFIERS
,INTERCEPTOR_BINDINGS
,STEREOTYPES
,BEANS
(class-based beans only),OBSERVERS
(class-based observers only),INJECTION_POINTS
- ObserverRegistrar
-
Has access to
ANNOTATION_STORE
,QUALIFIERS
,INTERCEPTOR_BINDINGS
,STEREOTYPES
,BEANS
,OBSERVERS
(class-based observers only),INJECTION_POINTS
- BeanDeploymentValidator
-
Has access to all build metadata