Solving problems with custom Quarkus extensions
From time to time, I see tweets or articles claiming they don’t see the point of Quarkus because "who needs fast startup?", "I have plenty of memory" or "what is the point of live reload?".
I could write an article debunking these arguments and explain how the latter makes your development workflow much more efficient and how the former makes the latter possible, even if fast boots are not your thing. But for the sake of this blog post, let’s admit these persons are absolutely right and these are not good reasons to use Quarkus.
So now what? Back to <insert your favorite framework here>
? Not so fast…
Quarkus didn’t achieve fast startup and low memory footprint by using dark magic or lazy loading tricks but by entirely rethinking the way Java applications are bootstrapped. The whole point of Quarkus is to move as much work as possible to the build time and this journey made us create a framework to push work at build time that can be leveraged in Quarkus extensions.
A Quarkus extension? That sounds like a lot of work?
No, really, it is not. You can develop your own extensions very easily and they can solve some out of the ordinary problems in a very simple way.
Last week, one of of our users (hey, Juan!) asked this question on Zulip:
Hi! I’m trying to understand how to find classes with some criteria and add them to the dependency injection context, for example: I want to find all classes whose name ends with "MessageTransformer" and add them to the context, I want to find those classes in an external library, so I can’t add annotations to them.
Let’s see how we can solve this issue by developing a custom extension.
Creating the extension
Creating the extension is as simple as:
mvn io.quarkus:quarkus-maven-plugin:create-extension -DwithoutTests
It will ask for a groupId
- let’s keep the default org.acme
- and an extension id - I went for message-transformers-as-beans
.
Then you can import your new extension into your favorite IDE.
Structure of the extension
There is a lot to say about extensions but, in the context of this blog post, we will keep it short. The extension is composed of three Maven modules:
-
The parent module - nothing to see here
-
The deployment module - this is the one of interest for our blog post
-
The runtime module - in this blog post, we won’t modify it
Let’s keep it simple: the deployment module is what will be used at build time, the runtime module is what will be used at runtime.
In our case, we want to declare new beans and this is something we do at build time, so deployment module, here we come!
Processors and build steps
If you have a look at your deployment
module, you will see a MessageTransformersAsBeansProcessor
and you can see a method annotated with the @BuildStep
annotation in it.
Quarkus build is populated by these build steps and they are following a consumer/producer model with dependency injection.
The items being consumed and produced are called BuildItem
s.
The build step that is automatically generated is easy to understand.
It produces a FeatureBuildItem
which will be consumed by Quarkus startup and you will see the extension name in the list displayed by Quarkus at startup:
INFO [io.quarkus] my-app 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.13.2.Final) started in 0.221s.
INFO [io.quarkus] Profile prod activated.
INFO [io.quarkus] Installed features: [cdi, message-transformers-as-beans]
The Jandex index
Now that we are done with the scaffolding, let’s think a bit about what we want to achieve:
we need to find all the classes in a given package whose name ends with MessageTransformer
.
An important assumption of Quarkus is that the application lives in a closed world. You cannot dynamically add a jar at runtime to your Quarkus application and expect it to work.
While it can be seen as a limitation, it opens all sorts of possibilities, one of which is the ability to index the classes and their annotations to easily look them up.
This index, based on Jandex, is a very important part of the Quarkus bootstrap.
The Jandex index doesn’t contain all the classes around but is, by default, limited to the application classes and the dependencies containing either a pre-built index or an empty META-INF/beans.xml
.
In our case, we want to list the classes of an external dependency so we will need to add them to the index.
We can do that very easily by adding a build step to MessageTransformersAsBeansProcessor
:
@BuildStep
IndexDependencyBuildItem indexExternalDependency() {
return new IndexDependencyBuildItem("my.group.id", "my-artifact-id");
}
This will add the content of the my.group.id:my-artifact-id
jar to the index.
Declaring additional beans
Now that we have our classes indexed, we want to make them CDI beans.
This can be achieved by adding another build step:
@BuildStep
void declareMessageTransformersAsBean(CombinedIndexBuildItem index, (1)
BuildProducer<AdditionalBeanBuildItem> additionalBeans) { (2)
List<String> messageTransformers = index.getIndex().getKnownClasses().stream() (3)
.filter(ci -> !Modifier.isAbstract(ci.flags())) (4)
.map(ci -> ci.name().toString()) (5)
.filter(c -> c.startsWith("my.package.")) (6)
.filter(c -> c.endsWith("MessageTransformer")) (7)
.collect(Collectors.toList());
additionalBeans.produce(new AdditionalBeanBuildItem.Builder() (8)
.addBeanClasses(messageTransformers)
.setUnremovable() (9)
.setDefaultScope(DotNames.APPLICATION_SCOPED) (10)
.build());
}
1 | Consume the Jandex index |
2 | Inject the additional beans producer |
3 | Get all known classes from the index |
4 | Filter out abstract classes |
5 | Get the FQCN of the class |
6 | Only keep classes from the root package we target |
7 | Only keep MessageTransformer s |
8 | Produce an AdditionalBeanBuildItem |
9 | Make the beans unremovable to prevent ArC from removing the beans if they are only programatically consumed |
10 | Set the default scope to @ApplicationScoped - can be any CDI scope of your preference |
With this build step, any non-abstract class from our root package my.package
whose name ends with MessageTransformer
will be made an @ApplicationScoped
CDI bean.
Cherry on top, all this work is done at build time and you don’t need to scan your entire classpath at runtime.
Usually, we look up classes in the index with an interface, a superclass or an annotation. It is less brittle and faster than crawling the whole index and filter by name. But the point here was to do with the constraints of the user and it wasn’t an option to adapt the external dependency. |
That’s all, folks!
Obviously, this is a very simple example and you can do much more complex things with a Quarkus extension.
But the whole point of this blog post was to demonstrate that you can easily leverage our extension framework to solve real-life issues. And in ~10 minutes of coding, our problem is gone.
Next one?
Únete a nosotros
Valoramos mucho tus comentarios, así que por favor reporta errores, solicita mejoras… ¡Construyamos algo grandioso juntos!
Si eres un usuario de Quarkus o simplemente tienes curiosidad, no seas tímido y únete a nuestra acogedora comunidad:
-
proporcionar retroalimentación en GitHub;
-
escribir algo de código y enviar push a PR;
-
comentar con nosotros en Zulip y en nuestra lista de correo;
-
hacer tus preguntas en Stack Overflow.