Quarkus: Greener, Better, Faster, Stronger

Kosmik - Dec 22 '23 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

💡 Be sure to update this plugin version to the most recent one...

It will generate the following folder tree :

Project 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️⃣
Enter fullscreen mode Exit fullscreen mode

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 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();
Enter fullscreen mode Exit fullscreen mode

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.

(jump back to the end)

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
}
Enter fullscreen mode Exit fullscreen mode

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()));
    }
Enter fullscreen mode Exit fullscreen mode

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.");
    }
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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("*********************************");
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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️⃣)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create or retrieve a container and get the necessary values to access it.
  2. 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️⃣;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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");
        }
    }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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️⃣
       }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ?"
Enter fullscreen mode Exit fullscreen mode

\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());
}
Enter fullscreen mode Exit fullscreen mode

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...");
    }

}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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 ?"
Enter fullscreen mode Exit fullscreen mode

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

Version

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