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)
Context
Let's pretend that we are working for a big company (Say Acme Company ©) that has many Java applications and wants to migrate to Quarkus.
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.
So following the guide we will just do:
mvn io.quarkus.platform:quarkus-maven-plugin:3.6.1:create-extension -N \
-DgroupId=org.acme \
-DextensionId=configuration-provider
💡 Be sure to update this plugin version to the most recent one...
It will generate the following folder tree :
1️⃣ - Root folderProject file folder
├── configuration-provider 1️⃣
│ ├── pom.xml 2️⃣
│ ├── deployment 3️⃣
│ ├── runtime 4️⃣
│ │ └── src
│ │ └── main
│ │ ├── java
│ │ └── resources
│ │ └── META-INF
│ │ └── quarkus-extension.yaml 5️⃣
│ ├── integration-tests 6️⃣
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 ConfigSourceFactory
path 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️⃣
public interface EnvironmentRuntimeConfiguration {
/**
* The environment provider server URL.
*
* [NOTE]
* ====
* Value must be a valid `URI`
* ====
*
* @asciidoclet 4️⃣
*/
@WithName("environment.url") 3️⃣
URI url();
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.
The AcmeConfigSource
The AcmeConfigSource
is pretty straightforward:
AcmeConfigSource open
public class AcmeConfigSource implements ConfigSource {
// Ignorable Code
public AcmeConfigSource(EnvironmentRuntimeConfiguration runtimeConfiguration) {
environmentProviderClient = new EnvironmentProviderClient(runtimeConfiguration.url());
String pattern = "(?<env>.*)\\.(?<key>.*)";
// Create a Pattern object
patternMatcher = Pattern.compile(pattern);
}
@Override
public String getValue(String propertyName) {
if (Predicate.not(isAcme)
.or(isProviderConfiguration)
.test(propertyName))
return null;
// Now create matcher object.
Matcher m = patternMatcher.matcher(propertyName);
if (m.find()) {
Map<String, String> env = environmentProviderClient.getEnvironment(m.group("env"));
if (env != null) {
return env.get(m.group("key"));
}
}
return null;
}
// 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:
@BuildStep
void askForApplicationScan(
ApplicationIndexBuildItem index, 1️⃣
BuildProducer<AcmeEnvironmentBuildItem> buildProducer 2️⃣) {
index.getIndex().getAnnotations(ConfigProperty.class)
.stream()
.map(AnnotationInstance::values)
.flatMap(List::stream)
.filter(value -> value.asString().startsWith("acme"))
.findFirst()
.ifPresent(annotationInstance -> buildProducer.produce(new AcmeEnvironmentBuildItem()));
}
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:
@BuildStep
void envConfigSourceFactory(
AcmeEnvironmentBuildItem acmeEnvironmentBuildItem, 1️⃣
BuildProducer<RunTimeConfigBuilderBuildItem> runTimeConfigBuilder 2️⃣) {
if (acmeEnvironmentBuildItem != null) {
runTimeConfigBuilder.produce(
new RunTimeConfigBuilderBuildItem(
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 Quarkus cli
, since my extension isn't part of any platform):
. io.quarkus:picocli
. org.acme:configuration-provider
And create the following class :
package org.acme.picocli;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import picocli.CommandLine;
@CommandLine.Command
public class Starter implements Runnable{
@Inject
Logger log;
@ConfigProperty(name = "env.snowcamp.title")
String snowcampConfTitle;
@ConfigProperty(name = "env.snowcamp.author")
String snowcampConfAuthor;
@Override
public void run() {
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:
@ConfigRoot(
prefix = "acme", 1️⃣
name = "", 2️⃣
phase = ConfigPhase.BUILD_TIME 3️⃣)
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:
Generic container = new GenericContainer("quay.io/jtama/acme-provider")
.withNetwork(Network.SHARED) 1️⃣
.withExposedPorts(8080); 2️⃣
container.start(); 3️⃣
return new DevServicesResultBuildItem.RunningDevService(DEV_SERVICE_LABEL,
container.getContainerId(), 4️⃣
container::close, 5️⃣
Map.of("acme.environment.url", "http://%s:%d".formatted(container.getHost(), container.getPort()))) 6️⃣;
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 :
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
<optional>true</optional>
</dependency>
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
:
public static class ReactiveResteasyEnabled
implements BooleanSupplier {
@Override
public boolean getAsBoolean() {
return QuarkusClassLoader.
isClassPresentAtRuntime( 1️⃣
"org.jboss.resteasy.reactive.ResponseHeader");
}
}
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️⃣
public void correctApproximations(
ApplicationIndexBuildItem applicationIndexBuildItem,
BuildProducer<AnnotationsTransformerBuildItem> transformers) {
logger.infof("Correcting your approximations if any. We'll see at runtime !");
transformers.produce(
new AnnotationsTransformerBuildItem(
new RestMethodCorrector()));
return;
}
1️⃣ This build step will only execute if needed
The AnnotationsTransformer
code is pretty straight forward:
private static class RestMethodCorrector
implements AnnotationsTransformer {
public static final DotName RESPONSE_HEADER =
DotName.createSimple(
org.jboss.resteasy.reactive.ResponseHeader.class);
public static final AnnotationValue HEADER_NAME =
AnnotationValue.createStringValue(
"name",
"X-ApproximationCorrector");
public static final AnnotationValue HEADER_VALUE =
AnnotationValue.createStringValue(
"ignored",
"It's more JSON over http really.");
public static final AnnotationValue HEADER_VALUES =
AnnotationValue.createArrayValue(
"value",
List.of(HEADER_VALUE));
@Override
public boolean appliesTo(AnnotationTarget.Kind kind) {
return AnnotationTarget.Kind.METHOD == kind; 1️⃣
}
@Override
public void transform(
AnnotationsTransformer.TransformationContext context) {
MethodInfo method = context.getTarget().asMethod();
if (isRestEndpoint.test(method)) { 2️⃣
Transformation transform = 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:
. io.quarkus:quarkus-resteasy-reactive-jackson
. org.acme:configuration-provider
And create the following enpoint:
package org.acme.enpoints;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@Path("/acme")
public class AcmeResource {
@ConfigProperty(name = "env.devoxxFR.title")
String devoxxFRConfTitle;
@ConfigProperty(name = "env.devoxxFR.author")
String devoxxFRConfAuth;
@GET
public String hellodevoxxFR() {
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️⃣
public void correctApproximations(
AcmeConfigurationBuildTimeConfiguration compileConfiguration, 2️⃣
ApplicationIndexBuildItem applicationIndexBuildItem,
BuildProducer<AnnotationsTransformerBuildItem> transformers,
ThisIsNotRestLogger thisIsNotRestLogger 2️⃣) {
if (compileConfiguration.strict.isRestStrict) { 3️⃣
logger.infof("Correcting your approximations if any. We'll see at runtime !");
transformers.produce(new
AnnotationsTransformerBuildItem(new
RestMethodCorrector()));
return;
}
Stream<MethodInfo> restEndpoints = applicationIndexBuildItem
.getIndex()
.getKnownClasses()
.stream()
.flatMap(
classInfo -> classInfo.methods().stream())
.filter(isRestEndpoint);
thisIsNotRestLogger.youAreNotDoingREST(restEndpoints
.map(this::getMessage)
.toList()); 4️⃣
}
private String getMessage(MethodInfo methodInfo) {
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 :
package org.acme.configurationProvider.runtime;
import io.quarkus.runtime.annotations.Recorder;
import org.jboss.logging.Logger;
import java.util.List;
@Recorder 1️⃣
public class ThisIsNotRestLogger {
private static final Logger logger = Logger.getLogger(ThisIsNotRestLogger.class);
public void youAreNotDoingREST(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 ?"
The added header is no longer here.
Documenting
Remember when I showed you the @asciidoclet
tag in the javadoc ?
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 !
Quarkus Approximation Corrector
Welcome to Quarkiverse!
Congratulations and thank you for creating a new Quarkus extension project in Quarkiverse!
Feel free to replace this content with the proper description of your new project and necessary instructions how to use and contribute to it.
You can find the basic info, Quarkiverse policies and conventions in the Quarkiverse wiki.
In case you are creating a Quarkus extension project for the first time, please follow Building My First Extension guide.
Other useful articles related to Quarkus extension development can be found under the Writing Extensions guide category on the Quarkus.io website.
Thanks again, good luck and have fun!
Documentation
The documentation for this extension should be maintained as part of this repository and it is stored in the docs/
directory.
The layout should follow the Antora's Standard File and Directory Set.
Once the docs are ready to be published, please open…
. There you will find the finished extension, its integration tests, picocli usage, and the acme centralized tech conference referential.
Top comments (0)