Solving problems with Quarkus extensions (2/n)
We are all good: 2 posts make a series!
If you haven’t looked at the first post of this series, I invite you to read it!
Problem of the day: A library is using the @Inject
annotation to handle its internal injection and, when used on beans, that will conflict with the CDI injection we have in Quarkus.
Leading to the impossibility for the CDI layer to inject these objects as they are not CDI beans.
Some context
As for the first post of the series, this post is based on my work on the Quarkus GitHub App extension that allows you to develop GitHub Apps based on Quarkus at light speed with very little boilerplate.
The newest feature of this extension is the ability to easily develop comment-based commands in your GitHub apps.
For instance, do something when a user posts a @bot do-something
in a comment of a pull request.
While it is possible to implement it all by yourself with the standard features of Quarkus GitHub App, we developed an additional extension to make things even easier.
Implementing a comment-based command with this extension is as easy as:
@Cli(name = "@bot", commands = { DoSomething.class })
public class MyFirstCli {
@Command(name = "do-something")
static class DoSomething implements Runnable {
@Override
public void run() {
// do something
}
}
}
The run()
method of the DoSomething
class will be called any time a user posts @bot do-something
as a comment in an issue or pull request.
These are the basics but the extension has a ton of other features such as reaction-based feedback, scopes, permissions…
This extension is based on the Airline library. This library is designed to easily parse and execute command lines. While originally designed to develop CLI applications, it is a perfect fit for our usage.
One problem that we have with this library is that it uses the @Inject
annotation for injecting some objects into commands such as GlobalMetadata
:
@Command(name = "do-something")
static class DoSomething implements Runnable {
@Inject
GlobalMetadata metadata;
@Override
public void run() {
// do something
}
}
This is a problem for us as this @Inject
annotation is used by CDI injection and, in the context of our extension, the @Command
classes are CDI beans.
Thus, this particular @Inject
annotation will also be interpreted by ArC, our CDI implementation, and ArC will try to inject GlobalMetadata
as a CDI bean… and fail because it is not a CDI bean.
Suffice to say it won’t work very well and we need to fix it.
Not making |
How can we work around this?
Ideally, the Airline library wouldn’t use the @Inject
annotation for its internal purpose
and the good news is, in the latest versions, the annotation used for injection can be specified.
But for the sake of the exercise, let’s stick to the previous Airline version.
So now what?
The set of classes the Airline library is susceptible to inject is limited: it is used to inject a limited number of classes and to handle composition (i.e. sharing components across several commands).
For these use cases, we somehow need ArC to ignore the injection points.
AnnotationTransformers to the rescue
If you are familiar with Quarkus, you are probably familiar with the notion of Jandex index. In Quarkus, we build indexes of the project annotations and these indexes are used by our core and extensions to find annotations (and more).
ArC, our CDI implementation, is one of the components that consumes the Jandex indexes.
Interestingly though, ArC does not consume the Jandex index as is:
Annotations transformers can add, remove, update existing annotations before consumption by ArC. These are used by several features in Quarkus, for instance Hibernate Validator interceptor support.
Annotations transformers do NOT modify the original classes, nor do they modify the Jandex indexes. Using annotations transfomers will solely impact ArC, our CDI implementation. |
This behavior is of great interest to us: we could hide the annotations from ArC using an annotations transformer while keeping them available for Airline to consume them via reflection.
Let’s create our annotations transformer:
public class HideAirlineInjectAnnotationsTransformer implements AnnotationsTransformer { (1)
private final IndexView index;
HideAirlineInjectAnnotationsTransformer(IndexView index) { (2)
this.index = index;
}
@Override
public boolean appliesTo(Kind kind) {
return Kind.FIELD == kind; (3)
}
@Override
public void transform(TransformationContext transformationContext) {
FieldInfo fieldInfo = transformationContext.getTarget().asField();
if (!fieldInfo.hasAnnotation(DotNames.INJECT)) { (4)
return;
}
if (fieldInfo.hasAnnotation(ARGUMENTS) ||
fieldInfo.hasAnnotation(OPTION) ||
GLOBAL_METADATA.equals(fieldInfo.type().name()) || (5)
COMMAND_GROUP_METADATA.equals(fieldInfo.type().name()) ||
COMMAND_METADATA.equals(fieldInfo.type().name()) ||
isComposition(fieldInfo)) { (6)
transformationContext.transform().remove(ai -> DotNames.INJECT.equals(ai.name())).done(); (7)
}
}
private boolean isComposition(FieldInfo fieldInfo) { (8)
Type fieldType = fieldInfo.type();
if (fieldType.kind() != Type.Kind.CLASS) {
return false;
}
ClassInfo fieldClass = index.getClassByName(fieldType.asClassType().name());
if (fieldClass == null) {
return false;
}
Set<DotName> fieldClassAnnotations = fieldClass.annotationsMap().keySet();
return fieldClassAnnotations.contains(ARGUMENTS) || fieldClassAnnotations.contains(OPTION);
}
}
1 | Our class implements AnnotationsTransformer . |
2 | We inject the Jandex index in our transformer as we will need it to detect composition. |
3 | We are only interested in fields so let’s apply our transformer to fields only. |
4 | If the field is not annotated with @Inject , it is of no interest to us. |
5 | If the field type is GlobalMetadata , GroupMetadata or CommandMetadata , we know it is the responsibility of Airline to inject it. |
6 | We are also detecting composition. |
7 | We remove the @Inject annotation from the transformed view visible to ArC.
Make sure you don’t forget to finalize the transformation with .done() . |
8 | For composition, we detect if the field is of a type that contains @Arguments or @Option annotations. |
Now that we have created our annotations transformer, we need to make sure Quarkus knows about it.
As usual, for the Quarkus build process, you just need to produce a BuildItem
to register the annotations transformer:
@BuildStep
public void beanConfig(CombinedIndexBuildItem index,
BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformer) {
annotationsTransformer
.produce(new AnnotationsTransformerBuildItem(new HideAirlineInjectAnnotationsTransformer(index.getIndex())));
}
And that’s it, from now on, the @Inject
annotations consumed by the Airline library will be hidden from ArC,
while still being visible from the Airline library, which uses reflection.
Regular CDI injection is still supported as only the @Inject
annotations handled by Airline are hidden from ArC.