You already know Build Compatible Extensions
03 Dec 2021 - Ladislav Thon
As you have learned from a previous post, CDI Lite comes with a new extension API called Build Compatible Extensions. In this post, you will learn more about this API by comparison with Portable Extensions and my hope is that by the end, you will see that you already know Build Compatible Extensions.
If you don’t know Portable Extensions, don’t worry. The Build Compatible Extensions API has detailed documentation and all examples in this text should be easy to follow for anyone.
Extension API
Build Compatible Extensions, similarly to Portable Extensions, are service providers for the BuildCompatibleExtension interface.
Contrary to Portable Extensions, Build Compatible Extensions do not declare observer methods for CDI events.
This is a major design goal of the entire CDI Lite effort: make it possible to perform bean discovery before the application is started, such as during compilation.
At that point in time, the CDI container obviously doesn’t exist, so it can’t deliver events.
That said, Build Compatible Extensions are still pretty close to what you’re used to. Instead of observer methods, you declare methods annotated with one of the execution phase annotations. On these methods, you then declare parameters that let you affect the bean discovery process. Each execution phase allows a certain set of parameter types to be declared.
The Build Compatible Extensions API has 5 execution phases. These phases roughly correspond to Portable Extensions lifecycle events:
| Build Compatible Extensions | Portable Extensions |
|---|---|
@Discovery |
BeforeBeanDiscovery |
@Enhancement |
ProcessAnnotatedType |
@Registration |
ProcessBean, ProcessObserverMethod (read-only) |
@Synthesis |
AfterBeanDiscovery |
@Validation |
AfterDeploymentValidation |
This covers the most important events in Portable Extensions.
One significant part is intentionally missing, and that’s an equivalent of the ProcessBeanAttributes event, plus the modification part of ProcessObserverMethod.
If deemed necessary, these could be added to Build Compatible Extensions in the future.
A simple Hello, world! extension would look like this:
// the corresponding META-INF/services/jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension file must also exist
public class MyExtension implements BuildCompatibleExtension {
@Discovery
public void hello(Messages msg) {
msg.info("Hello, world!");
}
}
Messages is one of the types that extension methods can declare as a parameter.
Let’s take a look at all the phases and compare them to Portable Extensions in more detail.
@Discovery
In @Discovery, you can add types to be scanned during bean discovery and register custom meta-annotations.
Extension methods annotated @Discovery can declare parameters of these types:
MetaAnnotations: register custom meta-annotations (qualifiers, interceptor bindings, stereotypes, and scopes)ScannedClasses: add types to the set of types discovered during type discovery, so that they are scanned during bean discoveryMessages: logging and validation
This is very similar to the BeforeBeanDiscovery event:
| Build Compatible Extensions | Portable Extensions |
|---|---|
ScannedClasses.add |
BeforeBeanDiscovery.addAnnotatedType |
MetaAnnotations.addQualifier |
BeforeBeanDiscovery.configureQualifier |
MetaAnnotations.addInterceptorBinding |
BeforeBeanDiscovery.configureInterceptorBinding |
MetaAnnotations.addStereotype |
BeforeBeanDiscovery.addStereotype |
MetaAnnotations.addContext |
BeforeBeanDiscovery.addScope + AfterBeanDiscovery.addContext |
One noticeable difference is that when adding a custom scope, you don’t provide a context object. Instead, you provide a class of the context object, which is then instantiated at runtime (and so must have a public zero-parameter constructor).
@Enhancement
In @Enhancement, you can look at discovered types and modify annotations on them.
Extension methods annotated @Enhancement can declare parameters of these types:
ClassInfo: inspect classes that satisfy given criteria (see below)MethodInfo: inspect methods that satisfy given criteria (see below)FieldInfo: inspect fields that satisfy given criteria (see below)ClassConfig: transform annotations on classes that satisfy given criteria (see below)MethodConfig: transform annotations on methods that satisfy given criteria (see below)FieldConfig: transform annotations on fields that satisfy given criteria (see below)Types: utility to create instances ofTypeMessages: logging and validation
Each @Enhancement method must declare exactly 1 parameter of type ClassInfo, MethodInfo, FieldInfo, ClassConfig, MethodConfig, or FieldConfig.
When the method declares a parameter of type ClassInfo or ClassConfig, it is called once for each matching type.
When it declares a parameter of type MethodInfo or MethodConfig, it is called once for each method and constructor of each matching type.
Finally, when it declares a parameter of type FieldInfo or FieldConfig, it is called once for each field of each matching type.
How are these “matching types” determined?
The @Enhancement annotation itself has members that, collectively, express this set:
typesis a simple set of matching types – this member is mandatory;withSubtypesis a boolean flag that says whether all subtypes of giventypesare also matching;withAnnotationsis a set of annotation types that must be present on the types so that they match.
The types and withSubtypes members corresponds to a type argument that you would apply to the ProcessAnnotatedType generic event type.
The withAnnotations member corresponds to the @WithAnnotations annotation.
As an example, let’s move a qualifier from one class to another:
// 1. a qualifier annotation
@Qualifier
@Retention(RUNTIME)
public @interface MyQualifier {
}
// 2. a service interface
public interface MyService {
String hello();
}
// 3. two implementations, one with qualifier and the other unqualified
@Singleton
@MyQualifier
public class MyFooService implements MyService {
@Override
public String hello() {
return "foo";
}
}
@Singleton
public class MyBarService implements MyService {
@Override
public String hello() {
return "bar";
}
}
// 4. a class that uses the service
@Singleton
public class MyServiceUser {
@Inject
@MyQualifier
MyService myService;
}
// 5. the extension
public class MyExtension implements BuildCompatibleExtension {
@Enhancement(types = MyFooService.class)
public void foo(ClassConfig clazz) {
clazz.removeAnnotation(it -> it.name().equals(MyQualifier.class.getName()));
}
@Enhancement(types = MyBarService.class)
public void bar(ClassConfig clazz) {
clazz.addAnnotation(MyQualifier.class);
}
}
The API is used somewhat differently than ProcessAnnotatedType, because in Portable Extensions, you first have to get an AnnotatedTypeConfigurator.
From there, however, the correspondence is pretty clear:
| Build Compatible Extensions | Portable Extensions |
|---|---|
ClassConfig.info |
AnnotatedTypeConfigurator.getAnnotated |
ClassConfig.addAnnotation |
AnnotatedTypeConfigurator.add |
ClassConfig.removeAnnotation |
AnnotatedTypeConfigurator.remove |
ClassConfig.removeAllAnnotations |
AnnotatedTypeConfigurator.removeAll |
ClassConfig.constructors |
AnnotatedTypeConfigurator.constructors |
ClassConfig.methods |
AnnotatedTypeConfigurator.methods |
ClassConfig.fields |
AnnotatedTypeConfigurator.fields |
MethodConfig.info |
AnnotatedMethodConfigurator.getAnnotated, AnnotatedConstructorConfigurator.getAnnotated |
MethodConfig.addAnnotation |
AnnotatedMethodConfigurator.add, AnnotatedConstructorConfigurator.add |
MethodConfig.removeAnnotation |
AnnotatedMethodConfigurator.remove, AnnotatedConstructorConfigurator.remove |
MethodConfig.removeAllAnnotations |
AnnotatedMethodConfigurator.removeAll, AnnotatedConstructorConfigurator.removeAll |
MethodConfig.params |
AnnotatedMethodConfigurator.params, AnnotatedConstructorConfigurator.params |
ParameterConfig.info |
AnnotatedParameterConfigurator.getAnnotated |
ParameterConfig.addAnnotation |
AnnotatedParameterConfigurator.add |
ParameterConfig.removeAnnotation |
AnnotatedParameterConfigurator.remove |
ParameterConfig.removeAllAnnotations |
AnnotatedParameterConfigurator.removeAll |
FieldConfig.info |
AnnotatedFieldConfigurator.getAnnotated |
FieldConfig.addAnnotation |
AnnotatedFieldConfigurator.add |
FieldConfig.removeAnnotation |
AnnotatedFieldConfigurator.remove |
FieldConfig.removeAllAnnotations |
AnnotatedFieldConfigurator.removeAll |
ProcessAnnotatedType.getAnnotatedType corresponds to declaring a parameter of type ClassInfo.
That type is described below, in the Langugae Model section.
Careful reader will note that ProcessAnnotatedType allows creating a whole new AnnotatedType manually and supplying it to the container.
Build Compatible Extensions do not allow this style of usage, only the simplified and more straightforward *Configurator style.
@Registration
In @Registration, you can look at registered beans, interceptors, and observers.
Extension methods annotated @Registration can declare parameters of these types:
BeanInfo: look at registered beans that satisfy given criteria (see below)InterceptorInfo: look at registered interceptors that satisfy given criteria (see below)ObserverInfo: look at registered observers that satisfy given criteria (see below)Types: utility to create instances ofTypeMessages: logging and validation
Each @Registration method must declare exactly 1 parameter of type BeanInfo, InterceptorInfo, or ObserverInfo.
When the method declares a parameter of type BeanInfo, it is called once for each bean whose set of bean types contains at least one given type.
When it declares a parameter of type InterceptorInfo, it is called once for each interceptor whose set of bean types contains at least one given type.
Finally, when it declares a parameter of type ObserverInfo, it is called once for each observer whose observed event type is assignable to at least one given type.
Similarly to @Enhancement, these “given types” come from the @Registration annotation itself.
The annotation has a single mandatory member called types whose value is the set of “given types”.
The types member corresponds to a type argument that you would apply to the ProcessBean or ProcessObserverMethod generic event types.
Again, Portable Extensions are somewhat different in usage, but there’s a direct correspondence:
| Build Compatible Extensions | Portable Extensions |
|---|---|
BeanInfo |
Bean obtained from ProcessBean |
InterceptorInfo |
Interceptor obtained from ProcessBean |
ObserverInfo |
ObserverMethod obtained from ProcessObserverMethod |
@Synthesis
In @Synthesis, you can register synthetic beans and observers.
Extension methods annotated @Synthesis can declare parameters of these types:
SyntheticComponents: register synthetic beans and observersTypes: utility to create instances ofTypeMessages: logging and validation
As an example, let’s create a synthetic bean. Synthetic observers are very similar.
// 1. a bean class
public class MyPojo {
public final String data;
public MyPojo(String data) {
this.data = data;
}
}
// 2. bean creation function
public class MyPojoCreator implements SyntheticBeanCreator<MyPojo> {
@Override
public MyPojo create(Instance<Object> lookup, Parameters params) {
String name = params.get("name", String.class);
return new MyPojo("Hello " + name);
}
}
// 3. bean disposal function
public static class MyPojoDisposer implements SyntheticBeanDisposer<MyPojo> {
@Override
public void dispose(MyPojo instance, Instance<Object> lookup, Parameters params) {
System.out.println("disposing " + instance.data);
}
}
// 4. the extension
public static class MyExtension implements BuildCompatibleExtension {
@Synthesis
public void synthesise(SyntheticComponents syn) {
syn.addBean(MyPojo.class)
.type(MyPojo.class)
.withParam("name", "World")
.createWith(MyPojoCreator.class)
.disposeWith(MyPojoDisposer.class);
}
}
An important observation is that the MyExtension code runs during bean discovery, which may be during application compilation, while the MyPojoCreator and MyPojoDisposer functions run at runtime.
The Parameters map is transferred from the extension code to the runtime code.
What kind of parameter types are possible?
Essentially anything that you can put into an annotation, including annotations themselves.
Comparison with Portable Extensions is straightforward:
| Build Compatible Extensions | Portable Extensions |
|---|---|
SyntheticComponents.addBean |
AfterBeanDiscovery.addBean |
SyntheticComponents.addObserver |
AfterBeanDiscovery.addObserverMethod |
Similarly to @Enhancement, Build Compatible Extensions do not have an equivalent of creating a whole new Bean or ObserverMethod object from scratch.
Only the simplified and more straightforward *Configurator style is present.
@Validation
In @Validation, you can perform custom validation.
Extension methods annotated @Validation can declare parameters of these types:
Messages: logging and validationTypes: utility to create instances ofType
Comparison with Portable Extensions is obvious:
| Build Compatible Extensions | Portable Extensions |
|---|---|
Messages.error |
AfterDeploymentValidation.addDeploymentProblem |
At this point, you may be asking, what’s the real difference? Isn’t this just a different set of names for existing concepts? Is a new API truly necessary?
The answer, again, comes from the architectural constraint that all this must be implementable during application compilation.
This means the CDI container can’t be running, but it also means that using reflection is costly or even forbidden.
Portable Extensions, unfortunately, are very reflection-heavy.
That’s why, in addition to a new extension API, we also had to create a new language model API.
All those weird types called ClassInfo etc. are part of that.
In the following section, we’ll describe the API in more detail and compare with existing similar APIs.
Language Model
The jakarta.enterprise.lang.model API models the Java language from a high-level perspective.
It is completely reflection-free, though it may easily be implemented on top of reflection.
The core value is that it can also be implemented on top of other representations, such as javax.lang.model or Jandex.
The model is structured as a hierarchy of types.
The root in this hierarchy is AnnotationTarget.
That is anything that can be annotated:
- declarations, such as classes, methods or fields;
- types, such as reference types, type variables or wildcard types.
Annotations are represented by AnnotationInfo.
An AnnotationInfo has a String-keyed map of annotation members, represented by AnnotationMember.
| Build Compatible Extensions | Portable Extensions | Reflection | Annotation Processing | Jandex |
|---|---|---|---|---|
AnnotationTarget |
jakarta.enterprise.inject.spi.Annotated |
java.lang.reflect.AnnotatedElement |
javax.lang.model.AnnotatedConstruct |
org.jboss.jandex.AnnotationTarget |
AnnotationInfo |
none, uses java.lang.Annotation |
none | javax.lang.model.AnnotationMirror |
org.jboss.jandex.AnnotationInstance |
AnnotationMember |
none1 | none | javax.lang.model.element.AnnotationValue |
org.jboss.jandex.AnnotationValue |
- Reflective access is possible, but usually not necessary.
As mentioned above, there are two kinds of AnnotationTargets: declarations and types.
Therefore, we have DeclarationInfo as the top-level type for representing Java declarations, and Type as the top-level type for representing Java types.
Declarations are:
- packages;
- classes, including interfaces, enums, annotations, and records;
- fields;
- methods, including constructors;
- method parameters;
- record components.
| Build Compatible Extensions | Portable Extensions | Reflection | Annotation Processing | Jandex |
|---|---|---|---|---|
DeclarationInfo |
none, just jakarta.enterprise.inject.spi.Annotated |
none4 | javax.lang.model.element.Element |
none, just org.jboss.jandex.AnnotationTarget |
PackageInfo |
none1 | java.lang.Package |
javax.lang.model.element.PackageElement |
none5 |
ClassInfo |
jakarta.enterprise.inject.spi.AnnotatedType2 |
java.lang.Class |
javax.lang.model.element.TypeElement |
org.jboss.jandex.ClassInfo |
FieldInfo |
jakarta.enterprise.inject.spi.AnnotatedField2 |
java.lang.reflect.Field |
javax.lang.model.element.VariableElement |
org.jboss.jandex.FieldInfo |
MethodInfo |
jakarta.enterprise.inject.spi.AnnotatedMethod, jakarta.enterprise.inject.spi.AnnotatedConstructor2 |
java.lang.reflect.Method, java.lang.reflect.Constructor |
javax.lang.model.element.ExecutableElement |
org.jboss.jandex.MethodInfo |
ParameterInfo |
jakarta.enterprise.inject.spi.AnnotatedParameter2 |
java.lang.reflect.Parameter |
javax.lang.model.element.VariableElement |
org.jboss.jandex.MethodParameterInfo |
RecordComponentInfo |
none3 | java.lang.reflect.RecordComponent |
javax.lang.model.element.RecordComponentElement |
org.jboss.jandex.RecordComponentInfo |
- Relies on reflection.
- Provides access to the corresponding reflection object.
- CDI doesn’t reflect the existence of records yet.
- There are types such as
java.lang.reflect.GenericDeclarationorjava.lang.reflect.AccessibleObject, but nothing directly corresponding, - It is possible to obtain a
ClassInfoforpackage-info.class,
Types are:
- the
voidpseudo-type; - primitive types, such as
int; - class types, such as
String; - array types, such as
int[]orString[][]; - parameterized types, such as
List<String>; - type variables, such as
Twhen used in a class or method that declares a type parameterT; - wildcard types, such as
? extends Number.
| Build Compatible Extensions | Portable Extensions | Reflection | Annotation Processing | Jandex |
|---|---|---|---|---|
Type |
none1 | java.lang.reflect.Type2 |
javax.lang.model.type.TypeMirror |
org.jboss.jandex.Type |
VoidType |
none1 | java.lang.Class2 |
javax.lang.model.type.NoType |
org.jboss.jandex.VoidType |
PrimitiveType |
none1 | java.lang.Class2 |
javax.lang.model.type.PrimitiveType |
org.jboss.jandex.PrimitiveType |
ClassType |
none1 | java.lang.Class2 |
javax.lang.model.type.DeclaredType |
org.jboss.jandex.ClassType |
ArrayType |
none1 | java.lang.Class, java.lang.reflect.GenericArrayType2 |
javax.lang.model.type.ArrayType |
org.jboss.jandex.ArrayType |
ParameterizedType |
none1 | java.lang.reflect.ParameterizedType2 |
javax.lang.model.type.DeclaredType |
org.jboss.jandex.ParameterizedType |
TypeVariable |
none1 | java.lang.reflect.TypeVariable2 |
javax.lang.model.type.TypeVariable |
org.jboss.jandex.TypeVariable, org.jboss.jandex.UnresolvedTypeVariable |
WildcardType |
none1 | java.lang.reflect.WildcardType2 |
javax.lang.model.type.WildcardType |
org.jboss.jandex.WildcardType |
- Relies on reflection.
- There’s also an alternative hierarchy rooted at
java.lang.reflect.AnnotatedType.
Conclusion
It’s been a ride, but you’ve seen the most important bits of the Build Compatible Extensions API. You’ve also seen how it compares to other APIs, particularly to Portable Extensions.
By now, you hopefully realize that while there are some necessary differences, you would be able to write an extension just fine.
If not, the cdi-dev mailing list is a great place to ask!