DEV Community

Efim Smykov
Efim Smykov

Posted on • Edited on

How Quarkus use build time to start your application faster

In this topic, we will try to undestand basics of how Quarkus augments application. We will cover how build process works, structure of resulted artifact and how it is launched.

What is Quarkus

Quarkus is framework for Java (primary) and Kotlin with focus on the cloud development.

What are benefits of Quarkus

  • Quick startup
  • Low memory consumption
  • Build time DI resolution

How Quarkus build application

At build time (also referred to as deployment time), Quarkus does most of the work. Let's establish the basics of how Quarkus builds our application.

Quarkus application build flow

Once all application classes are compiled, Quarkus build starts. During bootstrap, it prepares for the build, e.g. resolves all application dependencies. Next, Quarkus performs deployment (build) and produces an artifact. Let's take a closer look at each step related to Quarkus.

Quarkus application build flow (JVM mode)

Bootstrap phase

Before run build Quarkus does:

  • Resolve application dependencies.
  • Build build chain.
  • Produce initial build items into build chain.

Core build components

Build process consists from two basic blocks build step and build item.

Build item

They can be:

  • initial and non-initial
  • simple and multi

Initial Build Items

Initial build items are created and passed to the build chain by Quarkus and cannot be produced by build steps.
For example, the LaunchModeBuildItem indicates the launch type, such as production, development, or testing.

Simple build item

Can only be produced in one instance (and by only one build step)
For example, the CombinedIndexBuildItem contains index, which build from the application classes and dependencies that contain a certain marker file.

Multi build item

Can be produced any amount of time by any amount of build steps
For example, the AdditionalBeanBuildItem is produced by extensions to dynamically specify a bean.

Build step

  • Method with @BuildStep annotation.
  • Consume build items.
  • Produce build items by returning a value from a method or using BuildProducer<BuildItem>
  • Can be recorded and invoked at application startup.

Build step recording

The build step recording process works as follows:

  1. A proxy is created for the recorder passed in method using an InvocationHandler
  2. The proxy records all invocation information.
  3. Also proxy is created for all returned values from the invoked methods.
  4. Bytecode to be invoked at application startup is recorded based on the information gathered by the proxies.
  5. Recorded bytecode produced as StaticBytecodeRecorderBuildItem or MainBytecodeRecorderBuildItem and collected in MainClassBuildStep where runner class is generated.

InvocationHandler dummy implementation:



new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// do something on method invocation
}
}

Enter fullscreen mode Exit fullscreen mode




(Simplified) Build process

Build chain model

The resulting build chain can be represented as an oriented graph, with build items and build steps as nodes. Quarkus determines which build steps do not depend on any other build steps and starts their execution. After they have finished, it counts down the counter at the dependent build steps; if the counter equals 0, they start execution. All executions are done in parallel.

Pre DI build phase

Pre DI build phase
This scheme illustrates how Quarkus collects bean archives for dependency injection (DI). The process begins by collecting all archives, regardless of whether they contain beans or not. Next, Quarkus selects the bean archives from the collected archives.

Initially, collect all application classes. In case of a multi-module application, only classes in the main application module are indexed at this step, while others are treated as dependencies.

Next, the ApplicationArchiveBuildStep processes all application dependencies. Dependencies are indexed if they meet at least one of the following criteria:

  • Dependency contains one of the specified marker files, such as META-INF/jandex.idx or META-INF/beans.xml. Additional marker files can be specified using the AdditionalApplicationArchiveMarkerBuildItem
  • Dependency is included in one of the produced IndexDependencyBuildItem
  • Dependency is included in one of the initial AdditionalApplicationArchiveBuildItem build items, which cannot be produced by the user.

Also Quarkus index all classes specified in AdditionalIndexedClassBuildItem

These archives combined into CombinedIndexBuildItem, which consumed by BeanArchiveProcessor

Then BeanArchiveProcessor create bean archives index. Archive is treated as a bean archive if it meets at least one of the following criteria:

  • Archive contains META-INF/beans.xml
  • Archive contains at least one class with bean defining annotation, for example @ApplicationScoped
  • Archive specified as bean archive in BeanArchivePredicateBuildItem

DI build phase

During build time, Quarkus performs the following tasks:

  • Resolves all injection points.
  • Generates static proxies for beans to avoid reflection.
  • Generates bytecode to set up DI at application startup.

Create artifact phase

Result artifact generated in JarResultBuildStep. Quarkus has several artifact formats. For JVM default and recommended is fast-jar. It has followed structure.

Build a structure

How Quarkus starts application

Quarkus offers several launch modes, but here we will focus only on the production mode.

To start the application in production mode, launch the target/quarkus-app/quarkus-run.jar JAR. The QuarkusEntryPoint main class will be loaded from lib/boot/

The next step is for QuarkusEntryPoint to read the quarkus-application.dat file, from which it creates the RunnerClassLoader, the class loader for the Quarkus application. Using the information from quarkus-application.dat, the RunnerClassLoader will attempt to load a class from the parent class loader (JVM class loader) or from indexed jars, which is faster than the JVM class loader.

Then, the generated ApplicationImpl runs, where the static block contains static recordings, and the start method contains runtime recordings. It looks something like this:



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

public ApplicationImpl() {
super(false);
}

static {
// invoke static recordings
}

protected final void doStart(String[] var1) {
// invoke runtime recordings
}

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

public String getName() {
return "simple-app";
}
}

Enter fullscreen mode Exit fullscreen mode




Summary

We covered class indexing, build-time preparation for dependency injection, artifact structure, and application startup. While there are other important topics such as native executable creation, more advanced overview of dependency injection, and server implementation, it would be best to address them separately.

Top comments (0)