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.
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:
- A proxy is created for the recorder passed in method using an
InvocationHandler
- The proxy records all invocation information.
- Also proxy is created for all returned values from the invoked methods.
- Bytecode to be invoked at application startup is recorded based on the information gathered by the proxies.
- Recorded bytecode produced as
StaticBytecodeRecorderBuildItem
orMainBytecodeRecorderBuildItem
and collected inMainClassBuildStep
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
}
}
(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
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
orMETA-INF/beans.xml
. Additional marker files can be specified using theAdditionalApplicationArchiveMarkerBuildItem
- 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.
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";
}
}
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)