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 BuildItems.

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 MessageTransformers
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?

Come Join Us

We value your feedback a lot so please report bugs, ask for improvements…​ Let’s build something great together!

If you are a Quarkus user or just curious, don’t be shy and join our welcoming community: