Behind Quarkus Extension


A place to put my study log and comment

Quarkus provides tutorial and guide to explain what needs to do to create an extension. But these pages doesn’t explain what’s under the hood for these annotations @BuildStep, @Record and what is BuildItem really means. In this post, we’ll go through some lines code which are mainly generated bytecode by Quarkus annotation processor to better understand how quarkus works.

@BuildStep

This annotation is created to let Quarkus know this class should be processed to created some byte code in generated io.quarkus.runner.ApplicationImpl. The decompiled code is something like the following:

package io.quarkus.runner;

import io.quarkus.bootstrap.logging.InitialConfigurator;
import io.quarkus.bootstrap.runner.Timing;
import io.quarkus.deployment.steps.BootstrapConfigSetup;
import io.quarkus.deployment.steps.RuntimeConfigSetup;
import io.quarkus.deployment.steps.ArcProcessor.generateResources-1025303321;
import io.quarkus.deployment.steps.ArcProcessor.setupExecutor-1831044820;
import io.quarkus.deployment.steps.BannerProcessor.recordBanner-1279842229;
import io.quarkus.deployment.steps.BlockingOperationControlBuildStep.blockingOP558072755;
import io.quarkus.deployment.steps.ConfigBuildStep.registerConfigMappings510524965;
import io.quarkus.deployment.steps.ConfigBuildStep.validateConfigProperties1249763973;
import io.quarkus.deployment.steps.ConfigGenerationBuildStep.checkForBuildTimeConfigChange-1100481704;
import io.quarkus.deployment.steps.HttpSecurityProcessor.initBasicAuth583370107;
import io.quarkus.deployment.steps.LifecycleEventsBuildStep.startupEvent-858218658;
import io.quarkus.deployment.steps.LoggingResourceProcessor.setupLoggingRuntimeInit2028700189;
import io.quarkus.deployment.steps.LoggingResourceProcessor.setupLoggingStaticInit-1235809433;
import io.quarkus.deployment.steps.NativeImageConfigBuildStep.build163995889;
import io.quarkus.deployment.steps.NettyProcessor.eagerlyInitClass-1832577802;
import io.quarkus.deployment.steps.ResteasyCommonProcessor.setupResteasyInjection-1799175235;
import io.quarkus.deployment.steps.ResteasyStandaloneBuildStep.boot-614950547;
import io.quarkus.deployment.steps.ResteasyStandaloneBuildStep.staticInit-210558872;
import io.quarkus.deployment.steps.ShutdownListenerBuildStep.setupShutdown24485890;
import io.quarkus.deployment.steps.StaticResourcesProcessor.runtimeInit1493424519;
import io.quarkus.deployment.steps.StaticResourcesProcessor.staticInit-1777814589;
import io.quarkus.deployment.steps.SyntheticBeansProcessor.initRuntime-975230615;
import io.quarkus.deployment.steps.SyntheticBeansProcessor.initStatic1190120725;
import io.quarkus.deployment.steps.ThreadPoolSetup.createExecutor-168269452;
import io.quarkus.deployment.steps.VertxCoreProcessor.build-956362597;
import io.quarkus.deployment.steps.VertxCoreProcessor.eventLoopCount1012482323;
import io.quarkus.deployment.steps.VertxCoreProcessor.ioThreadDetector-1463825589;
import io.quarkus.deployment.steps.VertxHttpProcessor.bodyHandler1204734842;
import io.quarkus.deployment.steps.VertxHttpProcessor.cors-1956812358;
import io.quarkus.deployment.steps.VertxHttpProcessor.finalizeRouter749274288;
import io.quarkus.deployment.steps.VertxHttpProcessor.initializeRouter304369008;
import io.quarkus.deployment.steps.VertxHttpProcessor.openSocket-2064782366;
import io.quarkus.dev.appstate.ApplicationStateNotification;
import io.quarkus.runtime.Application;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.NativeImageRuntimePropertiesRecorder;
import io.quarkus.runtime.StartupContext;
import io.quarkus.runtime.StartupTask;
import io.quarkus.runtime.configuration.ProfileManager;
import io.quarkus.runtime.generated.Config;
import io.quarkus.runtime.util.StepTiming;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import org.graalvm.nativeimage.ImageInfo;
import org.jboss.logging.Logger;
import org.jboss.logmanager.handlers.DelayedHandler;

// $FF: synthetic class
public class ApplicationImpl extends Application {
    static Logger LOG;
    public static StartupContext STARTUP_CONTEXT;

    public ApplicationImpl() {
    }

    static {
        System.setProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager");
        System.setProperty("io.netty.allocator.maxOrder", "1");
        System.setProperty("io.netty.machineId", "32:be:fe:46:15:13:1a:a4");
        ProfileManager.setLaunchMode(LaunchMode.NORMAL);
        StepTiming.configureEnabled();
        Timing.staticInitStarted();
        Config.ensureInitialized();
        LOG = Logger.getLogger("io.quarkus.application");
        StartupContext var0 = new StartupContext();
        STARTUP_CONTEXT = var0;

        try {
            StepTiming.configureStart();
            ((StartupTask)(new io.quarkus.deployment.steps.VertxCoreProcessor.ioThreadDetector-1463825589())).deploy(var0);
            StepTiming.printStepTime(var0);
            ((StartupTask)(new blockingOP558072755())).deploy(var0);
            StepTiming.printStepTime(var0);
            ((StartupTask)(new setupLoggingStaticInit-1235809433())).deploy(var0);
            StepTiming.printStepTime(var0);
            ((StartupTask)(new build163995889())).deploy(var0);
            StepTiming.printStepTime(var0);
            ((StartupTask)(new staticInit-1777814589())).deploy(var0);
            StepTiming.printStepTime(var0);
            ((StartupTask)(new initStatic1190120725())).deploy(var0);
            StepTiming.printStepTime(var0);
            ((StartupTask)(new generateResources-1025303321())).deploy(var0);
            StepTiming.printStepTime(var0);
            ((StartupTask)(new setupResteasyInjection-1799175235())).deploy(var0);
            StepTiming.printStepTime(var0);
            ((StartupTask)(new staticInit-210558872())).deploy(var0);
            StepTiming.printStepTime(var0);
        } catch (Throwable var2) {
            ApplicationStateNotification.notifyStartupFailed(var2);
            var0.close();
            throw (Throwable)(new RuntimeException("Failed to start quarkus", var2));
        }
    }

    protected final void doStart(String[] var1) {
        System.setProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager");
        System.setProperty("io.netty.allocator.maxOrder", "1");
        System.setProperty("io.netty.machineId", "32:be:fe:46:15:13:1a:a4");
        NativeImageRuntimePropertiesRecorder.doRuntime();
        if (ImageInfo.inImageRuntimeCode()) {
            if (System.getProperty("javax.net.ssl.trustStore") != null) {
                LOG.warn("Setting the 'javax.net.ssl.trustStore' system property will not have any effect at runtime. Make sure to set this property at build time (for example by setting 'quarkus.native.additional-build-args=-J-Djavax.net.ssl.trustStore=someValue').");
            }

            if (System.getProperty("javax.net.ssl.trustStoreType") != null) {
                LOG.warn("Setting the 'javax.net.ssl.trustStoreType' system property will not have any effect at runtime. Make sure to set this property at build time (for example by setting 'quarkus.native.additional-build-args=-J-Djavax.net.ssl.trustStoreType=someValue').");
            }

            if (System.getProperty("javax.net.ssl.trustStoreProvider") != null) {
                LOG.warn("Setting the 'javax.net.ssl.trustStoreProvider' system property will not have any effect at runtime. Make sure to set this property at build time (for example by setting 'quarkus.native.additional-build-args=-J-Djavax.net.ssl.trustStoreProvider=someValue').");
            }

            if (System.getProperty("javax.net.ssl.trustStorePassword") != null) {
                LOG.warn("Setting the 'javax.net.ssl.trustStorePassword' system property will not have any effect at runtime. Make sure to set this property at build time (for example by setting 'quarkus.native.additional-build-args=-J-Djavax.net.ssl.trustStorePassword=someValue').");
            }
        }

        Timing.mainStarted();
        StartupContext var2 = STARTUP_CONTEXT;
        var2.setCommandLineArguments(var1);
        StepTiming.configureEnabled();
        String var3 = ProfileManager.getActiveProfile();

        try {
            StepTiming.configureStart();
            ((StartupTask)(new BootstrapConfigSetup())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new RuntimeConfigSetup())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new createExecutor-168269452())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new recordBanner-1279842229())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new setupExecutor-1831044820())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new cors-1956812358())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new eventLoopCount1012482323())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new build-956362597())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new bodyHandler1204734842())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new eagerlyInitClass-1832577802())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new initBasicAuth583370107())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new setupLoggingRuntimeInit2028700189())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new checkForBuildTimeConfigChange-1100481704())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new registerConfigMappings510524965())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new initRuntime-975230615())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new runtimeInit1493424519())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new validateConfigProperties1249763973())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new boot-614950547())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new initializeRouter304369008())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new finalizeRouter749274288())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new startupEvent-858218658())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new openSocket-2064782366())).deploy(var2);
            StepTiming.printStepTime(var2);
            ((StartupTask)(new setupShutdown24485890())).deploy(var2);
            StepTiming.printStepTime(var2);
            Timing.printStartupTime("hello", "1.0.0-SNAPSHOT", "1.9.2.Final", "cdi, resteasy, resteasy-jackson", var3, (boolean)0);
        } catch (Throwable var8) {
            DelayedHandler var6 = InitialConfigurator.DELAYED_HANDLER;
            if (!var6.isActivated()) {
                Handler[] var4 = new Handler[1];
                ConsoleHandler var5 = new ConsoleHandler();
                var4[0] = (Handler)var5;
                var6.setHandlers(var4);
            }

            var2.close();
            throw (Throwable)(new RuntimeException("Failed to start quarkus", var8));
        }
    }

    protected final void doStop() {
        STARTUP_CONTEXT.close();
    }

    public String getName() {
        return "hello";
    }
}

From the main class, you’ll find this is the class to config and start all things for a jaxrs service. It mainly start a vertx server and get this resource “deployed” :

package org.acme.resteasy;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/resteasy/hello")
public class ExampleResource {
    public ExampleResource() {
    }

    @GET
    @Produces({"text/plain"})
    public String hello() {
        return "hello";
    }
}

To understand what’s the purpose to create @BuildStep, we can look at a very simple example which print the “QUARKUS” banner during boot application.

1 public class BannerProcessor {
2    private static final Logger logger = Logger.getLogger(BannerProcessor.class);
3    @BuildStep(onlyIfNot = { IsTest.class })
4    @Record(ExecutionTime.RUNTIME_INIT)
5    public ConsoleFormatterBannerBuildItem recordBanner(BannerRecorder recorder, BannerConfig config,
6            BannerRuntimeConfig bannerRuntimeConfig) {
7        String bannerText = readBannerFile(config);
8        return new ConsoleFormatterBannerBuildItem(recorder.provideBannerSupplier(bannerText, bannerRuntimeConfig));
    }

The correspond generated class is something like :

public class BannerProcessor$recordBanner-1279842229 implements StartupTask {
    public BannerProcessor$recordBanner_1279842229/* $FF was: BannerProcessor$recordBanner-1279842229*/() {
    }

    public void deploy(StartupContext var1) {
        var1.setCurrentBuildStepName("BannerProcessor.recordBanner");
        Object[] var2 = new Object[2];
        this.deploy_0(var1, var2);
    }

    public void deploy_0(StartupContext var1, Object[] var2) {
        Object var3 = Config.BannerRuntimeConfig;
        var2[1] = var3;
        BannerRecorder var4 = new BannerRecorder();
        var2[0] = var4;
        Object var5 = var2[1];
        RuntimeValue var6 = ((BannerRecorder)var2[0]).provideBannerSupplier("__  ____  __  _____   ___  __ ____  ______ \n --/ __ \\/ / / / _ | / _ \\/ //_/ / / / __/ \n -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\\ \\   \n--\\___\\_\\____/_/ |_/_/|_/_/|_|\\____/___/   \n", (BannerRuntimeConfig)var5);
        var1.putValue("proxykey16", var6);
    }
}

@BuildStep Indicates that a given method is a build step that is run at deployment time to create the runtime output. Build steps are run concurrently at augmentation time to augment the application. They use a producer/consumer model, where a step is guaranteed not to be run until all items that it is consuming have been created. Producing and consuming is done via injection. This can be done via field injection, method parameter injection, or constructor parameter injection.

The following types are eligible for injection into a build step:

  • Any concrete subclass of SimpleBuildItem
  • List of any concrete subclass of MultiBuildItem
  • Consumer of any concrete subclass of BuildItem
  • Supplier of any concrete subclass of SimpleBuildItem
  • Optional instances whose value type is a subclass of SimpleBuildItem
  • Recorder classes, which are annotated with Recorder (method parameters only, if the method is annotated Record)
  • BytecodeRecorderImpl (method parameters only, if the method is annotated Record)

Injecting a SimpleBuildItem or a List of MultiBuildItem makes this step a consumer of these items, and as such will not be run until all producers of the relevant items has been run. Injecting a BuildProducer makes this class a producer of this item. Alternatively items can be produced by simply returning them from the method.

If field injection is used then every BuildStep method on the class will be a producer/consumer of these items, while method parameter injection is specific to an individual build step. In general method parameter injection should be the preferred approach as it is more fine grained. Note that a BuildStep will only be run if there is a consumer for items it produces. If nothing is interested in the produced item then it will not be run. A consequence of this is that it must be capable of producing at least one item (it does not actually have to produce anything, but it must have the ability to). A build step that cannot produce anything will never be run.

BuildItem instances must be immutable, as the producer/consumer model does not allow for mutating artifacts. Injecting a build item and modifying it is a bug waiting to happen, as this operation would not be accounted for in the dependency graph. -*- This is from quarkus javadoc and it explains the usage of build step very clear.

After looking at the above javadoc, we can look at BannerProcess line by line.

  • Line 3: Only include this build step if the given supplier class(IsTest) return false.
  • Line 4: @Record(ExecutionTime.RUNTIME_INIT) indicates the method Recorder parameter will be injected and this will be run in runtime instead of build time. We’ll talk about this later.
  • Line 5: BannerConfig and BannerRuntimeConfig are annotated with @ConfigRoot which indicts the instances of classes with this annotation will be made available to build steps or run time recorders, according to the {@linkplain #phase() phase} of the value, and BannerRecorder here is injected.
  • Line 8: ConsoleFormatterBannerBuildItem extends SimpleBuildItem is the return value , it indicts this build step produce ConsoleFormatterBannerBuildItem. This BuildItem will be finally consumed in LoggingResourceProcessor and print out to console.

@Record

From javadoc it explains this annotation is mainly for bytecode generation:

Indicates that this BuildStep method will also output recorded bytecode. If this annotation is present at least one method parameter must be a recorder object (i.e. a runtime object annotated with @Recorder). Any invocations made against this object will be recorded, and written out to bytecode to be invoked at runtime. The value() element determines when the generated bytecode is executed. If this is ExecutionTime.STATIC_INIT then it will be executed from a static init method, so will run at native image generation time. If this is ExecutionTime.RUNTIME_INIT then it will run from a main method at application start. There are some limitations on what can be recorded. Only the following objects are allowed as parameters to recording proxies: -primitives -String -Class -Objects returned from a previous recorder invocation -Objects with a no-arg constructor and getter/setters for all properties (or public fields) -Objects with a constructor annotated with @RecordableConstructor with parameter names that match field names -Any arbitrary object via the io.quarkus.deployment.recording.RecorderContext.registerSubstitution(Class, Class, Class) mechanism -arrays, lists and maps of the above

This annotation simply tells Quarkus, to be sepcific RuntimeRecorderImpl to generate byte code runtime/boot class to start the the server side components.

@Recorder

Indicates that the given type is a recorder that is used to record actions to be executed at runtime. Recorder classes must be non final and have a public no-arg constructor.

This is the main functional class which is responsible to providing main key component to serve the user request. Like the Resteasy reactive extension, the ResteasyReactiveRecorder creates two object: Deployment and ResteasyReactiveVertxHandler. ResteasyReactiveVertxHandler is an vertx handler, will finally be picked up by Vertx extension and running in vertx handler chain. The target of this ResteasyReactiveVertxHandler is this Resteasy Deployment.

RuntimeValue

Use a RuntimeValue wrapper for non-interface objects that are non-proxiable. Wrap the return object, if you want to pass one return object from one recorder method and passes to another one like what ResteasyReactiveRecorder does:

@Recorder
public class ResteasyReactiveRecorder extends ResteasyReactiveCommonRecorder {
    public RuntimeValue<Deployment> createDeployment(DeploymentInfo info,
            BeanContainer beanContainer,
            ShutdownContext shutdownContext, HttpBuildTimeConfig vertxConfig,
            RequestContextFactory contextFactory,
            BeanFactory<ResteasyReactiveInitialiser> initClassFactory) {
        ....
            }
            
    public Handler<RoutingContext> handler(RuntimeValue<Deployment> deploymentRuntimeValue) {
        Deployment deployment = deploymentRuntimeValue.getValue();
        RestInitialHandler initialHandler = new RestInitialHandler(deployment);
        return new ResteasyReactiveVertxHandler(initialHandler);
    }            

The above list class/annotation are the basic things are used to write a quarkus extension. Quarkus extension has the two parts code : deployment and runtime. Deployment processes all the things statically like annotation, analyze resource method to generate and record the byte code to avoid to handle this time consuming tasks.