So you heard of Quarkus and how it's blazingly fast, and you want to take your share of the cake.
You come from a world where sharing code is all about doing maven modules, and if you come from the Spring world, scanning it when your application starts. Well, you can forget about jar scanning. If Quarkus is faster and greener, it's -amongst other things- because it works at compile time as much as possible. And this is done through the extension mechanism.
So if you want to provide services to others and keep that fast frictionless feeling, you will have to do so.
In this article we will walk you through the scaffolding of an extension to a finished one, making use of it's best capabilities. I won't cover them all. Firstly because I don't know them all, secondly because it would take much more than an article...
I will try to keep a focus on speedy startups, and minimal memory consumption: the smaller, the better.
To do that, Quarkus offers us a framework to do most of the job during the compilation/packaging phase, so that the application startup is left with the least possible tasks.
💡 You can follow along the article or deep dive directly in the extension code
If Quarkus claims to be greener, it is due to the fact that it does all it can to reduce memory footprint. Small artifact leads to a smaller classloader, that in turn leads less memory consumption.
⚠️ Warning
Writing an exension is not always the way. There are many things that can be done without the burden. There are even good articles on how to not write an extension (here's one from Loïc Mathieu)
Our company has its own centralized tech conferences referential provider, and we want to facilitate its use to the developers. The preferred way to go with Quarkus is to create a custom ConfigSourceProvider.
So this is what we will be implementing here by putting the code in an extension so that is can be shareable with all the company applications, and can also provide some other services.
🚧 Scaffolding the extension
Creating an extension is pretty well documented in the Quarkus guides.
1️⃣ - Root folder
2️⃣ - Parent maven project descriptor
3️⃣ - Compile time module, where all the magic will lie
4️⃣ - Runtime module, where our config source will be
5️⃣ - Extension descriptor
6️⃣ - Will contain our integration tests, consisting of an application using the extension and some test assertions
In the above schema all folders are classic modules with a standard structure
The runtime module
The runtime module contains plain Quarkus flavoured java code, so we won't cover every detail but just highlight some interesting bits.
Please keep in mind that the extension provides values by calling the Acme Referential through its REST API. So it depends on a java rest client that needs to be configured, let's say that at rhe very least it needs a URL.
The config source factory
I have chosen to follow the ConfigSourceFactorypath to instantiate my ConfigSource bean. It gives me more possibilities as to how I can give context and instantiate the ConfigSource. I just don't need the registration part, as it will be done in the deployment module.
Configuring the REST API client
External configuration mapping is done by the EnvironmentRuntimeConfiguration interface.
@ConfigRoot(phase=ConfigPhase.RUN_TIME)1️⃣@ConfigMapping(prefix="acme")2️⃣publicinterfaceEnvironmentRuntimeConfiguration{/**
* The environment provider server URL.
*
* [NOTE]
* ====
* Value must be a valid `URI`
* ====
*
* @asciidoclet 4️⃣
*/@WithName("environment.url")3️⃣URIurl();
1️⃣ - This says that this bean is used at runtime
2️⃣ - Specifies the root property key
3️⃣ - Overrides defaults property key with acme.environment.url.
4️⃣ - Note the annotation that will be useful when generating the extension configuration.
publicclassAcmeConfigSourceimplementsConfigSource{// Ignorable CodepublicAcmeConfigSource(EnvironmentRuntimeConfigurationruntimeConfiguration){environmentProviderClient=newEnvironmentProviderClient(runtimeConfiguration.url());Stringpattern="(?<env>.*)\\.(?<key>.*)";// Create a Pattern objectpatternMatcher=Pattern.compile(pattern);}@OverridepublicStringgetValue(StringpropertyName){if(Predicate.not(isAcme).or(isProviderConfiguration).test(propertyName))returnnull;// Now create matcher object.Matcherm=patternMatcher.matcher(propertyName);if(m.find()){Map<String,String>env=environmentProviderClient.getEnvironment(m.group("env"));if(env!=null){returnenv.get(m.group("key"));}}returnnull;}// more mandatory code}
This is about all there is to say about the runtime module.
The deployment module
Now let's dive into the deployment module. The code executed at compile time is usually placed in classes called processors.
Now I need to tell Quarkus that it needs to execute code during the compile time. That is done by annotating methods with @BuildStep. Those methods produce AND consume BuildItem. There are many of them, and one that will certainly suit your need.
There is no simple way to order multiple step methods, but Quarkus will make sure the build items requested by a build step exist before invoking them, and that is how we can order buildsteps.
Getting application scan result
In a class called org.acme.configurationProvider.deploymen.EnvironmentInjectorProcessor, I've created the following method:
1️⃣ - The method is asking to get an index of the classes present in the application that's using the extension
2️⃣ - Here it's requesting the build producer that is used to gather all produced AcmeEnvironmentBuildItem.
The index lets us interact with it through many entry points, and amongst them (method names are self-explanatory): getClassesInPackage, getAnnotations, getAllKnownImplementors, and many more.
In this case, I want to make sure there is a real need for this extension, and that means at least one appearance of a ConfigProperty with a key starting with acme..
The AcmeEnvironmentBuildItem is an empty build item that I am using only to illustrate BuildStep ordering, also this is not very usual that an extension makes sure it's needed, but I want to highlight that it's possible.
I also created a second method:
@BuildStepvoidenvConfigSourceFactory(AcmeEnvironmentBuildItemacmeEnvironmentBuildItem,1️⃣BuildProducer<RunTimeConfigBuilderBuildItem>runTimeConfigBuilder2️⃣){if(acmeEnvironmentBuildItem!=null){runTimeConfigBuilder.produce(newRunTimeConfigBuilderBuildItem(AcmeConfigSourceFactoryBuilder.class.getName()));return;}logger.warn("You shoud not use this extension if you don't need it.");}
1️⃣ - It consumes the AcmeEnvironmentBuildItem, that creates the build step AcmeEnvironmentBuildItem
2️⃣ - And it produces a RunTimeConfigBuilderBuildItem
We can see that the method only produces a RunTimeConfigBuilderBuildItem if needed. This build item provides a way to register our AcmeConfigSourceFactoryBuilder for runtime phase.
If the given AcmeEnvironmentBuildItem is null, the buidstep will warn the user, asking them if they are sure the extension is needed, letting them know that they should probably remove the dependency.
When building their applications, the following log will be produced.
WARN [o.a.c.d.EnvironmentInjectorProcessor]
You shoud not use this extension if you don't need it.
Testing the extension
To test the extension, I will create a simple application using two extensions (not using the Quarkuscli, since my extension isn't part of any platform):
packageorg.acme.picocli;importjakarta.inject.Inject;importorg.eclipse.microprofile.config.inject.ConfigProperty;importorg.jboss.logging.Logger;importpicocli.CommandLine;@CommandLine.CommandpublicclassStarterimplementsRunnable{@InjectLoggerlog;@ConfigProperty(name="env.snowcamp.title")StringsnowcampConfTitle;@ConfigProperty(name="env.snowcamp.author")StringsnowcampConfAuthor;@Overridepublicvoidrun(){log.info("******** WELCOME ! ********");log.infof("Welcome %s, that will present: \"%s\"%n",snowcampConfAuthor,snowcampConfTitle);log.info("*********************************");}}
Now launching my application I get :
2023-12-13 08:30:48,502 ERROR [i.q.r.Application]
Failed to start application (with profile [dev]):
java.lang.RuntimeException: Failed to start quarkus
// Long stacktrace
Caused by: io.smallrye.config.ConfigValidationException:
Configuration validation failed:
java.util.NoSuchElementException:
SRCFG00014:
The config property acme.environment.url is required but it could not be found in any config source
// more stacktrace
Oh yes of course, I need to pass the acme provider URL, no issues... Except I don't have a dev environment provider at my disposal...
The Dev Service
Here comes the Dev Service 🚀.
Until now we have seen how Quarkus can help us to reduce the number of beans and jar scanning at runtime by doing all those tasks at compile time. That will help us be greener and faster, but not really stronger from a developer experience perspective.
Dev Services supports the automatic provisioning of unconfigured third party services in development and test mode. They can be provided by extension leveraging (usually) TestContainer library.
So our extension will do just that.
The configuration
As was the case for the runtime, I will use configuration classes, this time with the following:
1️⃣ - The properties key prefix
2️⃣ - By default extension properties are within the quarkus namespace (that would make quarkus.acme.*), and not being on of the core or quarkiverse extensions, I decided to not use the default namespace
3️⃣ - Those properties will only be editable at compile time.
The configuration will contain two properties :
acme.devservices.enabled
Allows enabling/disabling the Dev Service for this extension. The default value is true
acme.devservices.image-name
Allow to override the image used for this devservice. The default value is quay.io/jtama/acme-provider
The Dev Dervice processor
I now need to make use of this configuration. So I create a org.acme.configurationProvider.deployment.devservice.DevServicesProcessor class. I will only show the most relevant part of the processor's code.
The class has two responsabilties:
Create or retrieve a container and get the necessary values to access it.
Tell Quarkus that there is a running Dev Service, to configure the application accordingly.
All the code shown in this chapter will be a simplified version of the real extension for a better fit to this format.
Running the container
To run the container, we will leverage the testcontainer lib:
1️⃣ - Use the Docker shared network
2️⃣ - Tells test container that this container listens on port 8080, and that it needs to be mapped.
3️⃣ - Start the container, and wait until it's ready
4️⃣ - Retrieve the container id
5️⃣ - Gives a closeable, that will allow for stopping the container when the application stops
6️⃣ - Provides a map of properties that will be used to wire up the application.
In this sample you can see that testcontainer allows us to retrieve values that were dynamically generated when we started the container, such as the container host or exposed port.
Quarkus will then tie everything together auto-magically, so that your application is configured to use this Dev Service.
This sample code has been simplified to hell, the original class has 170+ lines of code, but that's enough to demonstrate how easy it is to provide a new Dev Service for an extension.
Restarting the application
If I try to start my application again I get the following log :
2023-12-22 09:13:46,579 INFO [org.acm.con.dep.dev.DevServicesProcessor] (build-25) Dev Services for Acme Env started on http://localhost:49221
2023-12-22 09:13:46,582 INFO [org.acm.con.dep.dev.DevServicesProcessor] (build-25) Other Quarkus applications in dev mode will find the instance automatically. For Quarkus applications in production mode, you can connect to this by starting your application with -Dacme.environment.url=http://localhost:49221
___ ___ _ _
/ _ \_ __ ___ ___ _ __ ___ _ __ / __\ ___| |_| |_ ___ _ __
/ /_\/'__/ _ \/ _ \ '_ \ / _ \ '__| /__\/// _ \ __| __/ _ \ '__|
/ /_\\| | | __/ __/ | | | __/ | / \/\ __/ |_| || __/ |
\____/|_| \___|\___|_| |_|\___|_| \_____/\___|\__|\__\___|_|
___ _ _
/ __\_ _ ___| |_ ___ _ __ ___| |_ _ __ ___ _ __ __ _ ___ _ __
/ _\/ _` / __| __/ _ \ '__| / __| __| '__/ _ \|'_ \ / _` |/ _ \ '__|
/ / | (_| \__ \ || __/ | \__ \ |_| | | (_) | | | | (_| | __/ |
\/\__,_|___/\__\___|_| |___/\__|_| \___/|_| |_|\__, |\___|_|
|___/
Powered by Quarkus 3.6.4
2023-12-22 09:13:47,319 INFO [io.quarkus] (Quarkus Main Thread) configuration-provider-picocli-tests 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.6.4) started in 10.341s.
2023-12-22 09:13:47,320 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2023-12-22 09:13:47,320 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, configuration-provider, picocli]
2023-12-22 09:13:47,398 INFO [org.acm.con.it.Starter] (Quarkus Main Thread)******** WELCOME !********
2023-12-22 09:13:47,398 INFO [org.acm.con.it.Starter] (Quarkus Main Thread) Welcome j.tama, that will present: "Quarkus: Greener, Better, Faster, Stronger"
2023-12-22 09:13:47,398 INFO [org.acm.con.it.Starter] (Quarkus Main Thread)*********************************
2023-12-22 09:13:47,404 INFO [io.quarkus] (Quarkus Main Thread) configuration-provider-picocli-tests stopped in 0.005s
This means that both acme.snowcamp.title, acme.snowcam.author properties have been injected in the Starter command and used.
Sliding down the wrong slope
Seeing how easy it was to give applications new capabilities I decided to go further. In our team, we have someone whose greatest battle is that speaking about REST API's is heretic. He taught us how REST can't be by its very nature an API. So I decided to help him in his fight, implementing a fault rectifier. I thus added a ThisIsNotRestTransformerProcessor in the deployment module.
As I said, we want to focus on the Greener and faster mojo. So at all cost we want to keep :
. Small artifacts, that will lead to smaller class loader, less memory consumption and quicker start-up times.
. Applications that only do what they really need, so, no useless extension dependencies.
To do that I will add the quarkus-resteasy-reactive extension to the runtime module with a special hint :
What that means, is that this extension won't be present in the final application artifact unless explicitly asked by the application.
Adding/removing/modifying annotations
I will now add a new @BuildStep to the processor, but I want it to be triggered, only if the @ResponseHeader is present at runtime, meaning only if the application has added the quarkus-resteasy-reactive extension to its dependencies.
To do this I will first create a java.util.function.BooleanSupplier:
1️⃣ Notice how Quarkus helps me find out if a class will be present at runtime
All I have to do is use it:
@BuildStep(onlyIf=ReactiveResteasyEnabled.class)1️⃣publicvoidcorrectApproximations(ApplicationIndexBuildItemapplicationIndexBuildItem,BuildProducer<AnnotationsTransformerBuildItem>transformers){logger.infof("Correcting your approximations if any. We'll see at runtime !");transformers.produce(newAnnotationsTransformerBuildItem(newRestMethodCorrector()));return;}
1️⃣ This build step will only execute if needed
The AnnotationsTransformer code is pretty straight forward:
privatestaticclassRestMethodCorrectorimplementsAnnotationsTransformer{publicstaticfinalDotNameRESPONSE_HEADER=DotName.createSimple(org.jboss.resteasy.reactive.ResponseHeader.class);publicstaticfinalAnnotationValueHEADER_NAME=AnnotationValue.createStringValue("name","X-ApproximationCorrector");publicstaticfinalAnnotationValueHEADER_VALUE=AnnotationValue.createStringValue("ignored","It's more JSON over http really.");publicstaticfinalAnnotationValueHEADER_VALUES=AnnotationValue.createArrayValue("value",List.of(HEADER_VALUE));@OverridepublicbooleanappliesTo(AnnotationTarget.Kindkind){returnAnnotationTarget.Kind.METHOD==kind;1️⃣}@Overridepublicvoidtransform(AnnotationsTransformer.TransformationContextcontext){MethodInfomethod=context.getTarget().asMethod();if(isRestEndpoint.test(method)){2️⃣Transformationtransform=context.transform();3️⃣transform.add(RESPONSE_HEADER,HEADER_VALUES);4️⃣transform.done();5️⃣}}}
1️⃣ - Only applies transformations to method, because it's what @ResponseHeader targets
2️⃣ - If the given is an enpoint, i.e. is annotated with one of the following: @GET,@PUT,@POST,@DELETE,@PATCH
3️⃣ - Starts a new transformation
4️⃣ - Adds the @ResponseHeader to the method with correct values
5️⃣ - Ends the transformation
And we are done !
Testing the newly added header
I will now create another simple application using:
packageorg.acme.enpoints;importjakarta.ws.rs.GET;importjakarta.ws.rs.Path;importorg.eclipse.microprofile.config.inject.ConfigProperty;@Path("/acme")publicclassAcmeResource{@ConfigProperty(name="env.devoxxFR.title")StringdevoxxFRConfTitle;@ConfigProperty(name="env.devoxxFR.author")StringdevoxxFRConfAuth;@GETpublicStringhellodevoxxFR(){return"Welcome %s, that will present: \"%s\"".formatted(devoxxFRConfAuth,devoxxFRConfTitle);}}
If I now starts the application and trigger the endpoint with httpie:
> http localhost:8080/acme/foo
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
X-ApproximationCorrector: It's more JSON over http really.
content-length: 91
Welcome Malvin le Martien, that will present: "Why does Elmyra Duff love animals so much ?"
\o/ That's a success ! My endpoint is magically augmented!
That may be a bit to much though. I think I will let the developers tell me if they want to be strict about this or not.
If they don't (and that will be the default behaviour), I will only log something at application startup. If they do want strictness, we will fallback to annotation transformation.
I will need to introduce one last feature for this.
Recording bytecode for later invocation.
First of all, let's introduce a new property for the compile phase: acme.strict.rest. Indeed, the extension deployment code is run during the compilation phase, so if we want the developers to be able to pilot their behaviour we need early configuration. The property will default to false.
I then have to slightly modify my build step:
@BuildStep(onlyIf=ReactiveResteasyEnabled.class)@Record(ExecutionTime.RUNTIME_INIT)1️⃣publicvoidcorrectApproximations(AcmeConfigurationBuildTimeConfigurationcompileConfiguration,2️⃣ApplicationIndexBuildItemapplicationIndexBuildItem,BuildProducer<AnnotationsTransformerBuildItem>transformers,ThisIsNotRestLoggerthisIsNotRestLogger2️⃣){if(compileConfiguration.strict.isRestStrict){3️⃣logger.infof("Correcting your approximations if any. We'll see at runtime !");transformers.produce(newAnnotationsTransformerBuildItem(newRestMethodCorrector()));return;}Stream<MethodInfo>restEndpoints=applicationIndexBuildItem.getIndex().getKnownClasses().stream().flatMap(classInfo->classInfo.methods().stream()).filter(isRestEndpoint);thisIsNotRestLogger.youAreNotDoingREST(restEndpoints.map(this::getMessage).toList());4️⃣}privateStringgetMessage(MethodInfomethodInfo){return"You think you method \"%s#%s\" is doing rest but it's more JSON over HTTP actually.".formatted(methodInfo.declaringClass().toString(),methodInfo.toString());}
1️⃣ - Tells Quarkus this @BuildStep is doing byte code recording
2️⃣ - Injects configuration and recorder
3️⃣ - Add annotations only if in strict mode
4️⃣ - Else invoke recorder with list of messages
And the recorder is a classical class :
packageorg.acme.configurationProvider.runtime;importio.quarkus.runtime.annotations.Recorder;importorg.jboss.logging.Logger;importjava.util.List;@Recorder1️⃣publicclassThisIsNotRestLogger{privatestaticfinalLoggerlogger=Logger.getLogger(ThisIsNotRestLogger.class);publicvoidyouAreNotDoingREST(List<String>warnings){logger.errorf("%s******** You should Listen *********%s%s%s%s",System.lineSeparator(),System.lineSeparator(),String.join(System.lineSeparator(),warnings),System.lineSeparator(),"If I had been stricter I would have changed your code...");}}
1️⃣ - Hey Quarkus, I'm a recorder !
Notice even though this method is invoked during build time, it will be invoked each time the application starts :
___ ___ _ _
/ _ \_ __ ___ ___ _ __ ___ _ __ / __\ ___| |_| |_ ___ _ __
/ /_\/'__/ _ \/ _ \ '_ \ / _ \ '__| /__\/// _ \ __| __/ _ \ '__|
/ /_\\| | | __/ __/ | | | __/ | / \/\ __/ |_| || __/ |
\____/|_| \___|\___|_| |_|\___|_| \_____/\___|\__|\__\___|_|
___ _ _
/ __\_ _ ___| |_ ___ _ __ ___| |_ _ __ ___ _ __ __ _ ___ _ __
/ _\/ _` / __| __/ _ \ '__| / __| __| '__/ _ \|'_ \ / _` |/ _ \ '__|
/ / | (_| \__ \ || __/ | \__ \ |_| | | (_) | | | | (_| | __/ |
\/\__,_|___/\__\___|_| |___/\__|_| \___/|_| |_|\__, |\___|_|
|___/
Powered by Quarkus 3.6.4
2023-12-22 11:16:06,281 ERROR [org.acm.con.run.ThisIsNotRestLogger] (Quarkus Main Thread)******** You should Listen *********
You think you method "org.acme.configurationProvider.it.AcmeResource#java.lang.String hellodevoxxFR(java.lang.String event)" is doing rest but it's more JSON over HTTP actually.
You think you method "org.acme.configurationProvider.it.CustomResourceUtils#java.lang.String hello()" is doing rest but it's more JSON over HTTP actually.
If I had been stricter I would have changed your code... [Error Occurred After Shutdown]
If I now retrigger my endpoint, ,I get the following result :
http localhost:8080/acme/foo
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
content-length: 91
Welcome Malvin le Martien, that will present: "Why does Elmyra Duff love animals so much ?"
Well scaffolding an extension also generates a docs module which leverages Antora, and with minimal effort, we can produce a nice and clean documentation.
All the configuration documentation has been automatically generated from the javadoc
Conclusion
I've only scratched the surface here, but I hope to have eased your path in writing an extension.
Please remember that extension maintainers are to be greatly accountable for keeping things small and fast.
Oh, and please don't mix things up on an extension, like I did. That was for demonstration only purpose, please do not try to reproduce this at home...
Happy coding !
The source code
All the sources (and a bit more) I've shown in this article can be found in the following repository. Please feel free to clone/fork it and play with it !