1. Overview

Quarkus is a framework composed of a core and a set of extensions. The core is based on Context and Dependency Injection (CDI) and extensions are usually meant to integrate a third-party framework by exposing their primary components as CDI beans.

In this tutorial, we’ll focus on how to write a Quarkus extension assuming a basic understanding of Quarkus.

2. What’s a Quakus Extension

A Quarkus extension is simply a module that can run on top of a Quarkus application. The Quarkus application itself is a core module with a set of other extensions.

The most common use case for such an extension is to get a third-party framework running on top of a Quarkus application.

3. Running Liquibase in a Plain Java Application

Let’s try and implement an extension for integrating Liquibase, a tool for database change management.

But before we dive in, we first need to show how to run a Liquibase migration from a Java main method. This will hugely facilitate implementing the extension.

The entry point for the Liquibase framework is the Liquibase API. To use this, we need a changelog file, a ClassLoader for accessing this file, and a Connection to the underlying database:

Connection c = DriverManager.getConnection("jdbc:h2:mem:testdb", "user", "password");
ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor();
String changLogFile = "db/liquibase-changelog-master.xml";
Liquibase liquibase = new Liquibase(changLogFile, resourceAccessor, new JdbcConnection(c));

Having this instance, we simply call the update() method which updates the database to match the changelog file.

liquibase.update(new Contexts());

The goal is to expose Liquibase as a Quarkus extension. That is, providing a database configuration and changelog file through Quarkus Configuration and then producing the Liquibase API as a CDI bean. This provides a means for recording migration invocation for later execution.

4. How to Write a Quarkus Extension

Technically speaking, a Quarkus extension is a Maven multi-module project composed of two modules. The first is a runtime module where we implement requirements. The second is a deployment module for processing configuration and generating the runtime code.

So, let’s start by creating a Maven multi-module project called quarkus-liquibase-parent that contains two submodules, runtime and deployment:

<modules>
    <module>runtime</module>
    <module>deployment</module>
</modules>

5. Implementing the Runtime Module

In the runtime module, we’ll implement:

  • a configuration class for capturing the Liquibase changelog file
  • a CDI producer for exposing the Liquibase API
  • and a recorder that acts as a proxy for recording invocation calls

5.1. Maven Dependencies and Plugins

The runtime module will depend on the quarkus-core module and eventually the runtime modules of the needed extensions. Here, we need the quarkus-agroal dependency as our extension needs a Datasource. We’ll include the Liquibase library here, too:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-core</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-agroal</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>4.26.0</version>
</dependency>

Also, we may need to add the quarkus-bootstrap-maven-plugin. This plugin automatically generates the Quarkus extension descriptor by calling the extension-descriptor goal.

Or, we can omit this plugin and generate the descriptor manually.

Either way, we can find the extension descriptor is located under META-INF/quarkus-extension.properties:

deployment-artifact=com.baeldung.quarkus.liquibase\:deployment\:1.0-SNAPSHOT

5.2. Exposing the Configuration

To provide the changelog file, we need to implement a configuration class:

@ConfigRoot(name = "liquibase", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public final class LiquibaseConfig {
    @ConfigItem
    public String changeLog;
}

We annotate the class by @ConfigRoot and the properties by @ConfigItem. So, the changeLog field, which is the camel case form of the change-log, will be provided through the quarkus.liquibase.change-log key in the application.properties file, located in a Quarkus application classpath:

quarkus.liquibase.change-log=db/liquibase-changelog-master.xml

We can also note the ConfigRoot.phase value which instructs when to resolve the change-log key. In this case, BUILD_AND_RUN_TIME_FIXED, the key is read at deployment time and available to the application at runtime.

5.3. Exposing the Liquibase API as a CDI Bean

We’ve seen above how to run a Liquibase migration from the main method.

Now, we’ll reproduce the same code but as a CDI bean, and we’ll use a CDI producer for that purpose:

@Produces
public Liquibase produceLiquibase() throws Exception {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(classLoader);
    DatabaseConnection jdbcConnection = new JdbcConnection(dataSource.getConnection());
    Liquibase liquibase = new Liquibase(liquibaseConfig.changeLog, resourceAccessor, jdbcConnection);
    return liquibase;
}

5.4. Recording Bytecode

In this step, we’ll write a recorder class that acts as a proxy for recording bytecode and setting up the runtime logic:

@Recorder
public class LiquibaseRecorder {

    public BeanContainerListener setLiquibaseConfig(LiquibaseConfig liquibaseConfig) {
        return beanContainer -> {
            LiquibaseProducer producer = beanContainer.instance(LiquibaseProducer.class);
            producer.setLiquibaseConfig(liquibaseConfig);
        };
    }

    public void migrate(BeanContainer container) throws LiquibaseException {
        Liquibase liquibase = container.instance(Liquibase.class);
        liquibase.update(new Contexts());
    }

}

Here, we have to record two invocations. setLiquibaseConfig for setting configuration and migrate for executing the migration. Next, we’ll look at how these recorder methods are called by the deployment build step processors which we’ll implement in the deployment module.

Note that when we invoke these recorder methods at build time, instructions are not executed but recorded for later execution at startup time.

6. Implementing the Deployment Module

The central components in a Quarkus extension are the Build Step Processors. They are methods annotated as @BuildStep that generate bytecode through recorders, and they are executed during the build time through the build goal of the quarkus-maven-plugin configured in a Quarkus application.

@BuildSteps are ordered thanks to BuildItems. They consume build items generated by early build steps and produce build items for later build steps.

The generated code by all the ordered build steps found in the application deployment modules is actually the runtime code.

6.1. Maven Dependencies

The deployment module should depend on the corresponding runtime module and eventually on the deployment modules of the needed extensions:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-core-deployment</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-arc-deployment</artifactId>
    <version>${quarkus.version}</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-agroal-deployment</artifactId>
    <version>${quarkus.version}</version>
</dependency>

<dependency>
    <groupId>com.baeldung.quarkus.liquibase</groupId>
    <artifactId>runtime</artifactId>
    <version>${project.version}</version>
</dependency>

The latest stable version of Quarkus extensions is the same for the runtime module.

6.2. Implementing Build Step Processors

Now, let’s implement two build step processors for recording bytecode. The first build step processor is the build() method which will record bytecode for execution in the static init method. We configure this through the STATIC_INIT value:

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void build(BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
  BuildProducer<FeatureBuildItem> featureProducer,
  LiquibaseRecorder recorder,
  BuildProducer<BeanContainerListenerBuildItem> containerListenerProducer,
  DataSourceInitializedBuildItem dataSourceInitializedBuildItem) {

    featureProducer.produce(new FeatureBuildItem("liquibase"));

    AdditionalBeanBuildItem beanBuilItem = AdditionalBeanBuildItem.unremovableOf(LiquibaseProducer.class);
    additionalBeanProducer.produce(beanBuilItem);

    containerListenerProducer.produce(
      new BeanContainerListenerBuildItem(recorder.setLiquibaseConfig(liquibaseConfig)));
}

First, we create a FeatureBuildItem to mark the type or the name of the extension. Then, we create an AdditionalBeanBuildItem so that the LiquibaseProducer bean will be available for the Quarkus container*.*

Finally, we create a BeanContainerListenerBuildItem in order to fire the BeanContainerListener after the Quarkus BeanContainer startup. Here, in the listener, we pass the configuration to the Liquibase bean.

The processMigration(), in turn, will record the invocation for execution in the main method as it’s configured using the RUNTIME_INIT parameter for recording.

@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
void processMigration(LiquibaseRecorder recorder, 
  BeanContainerBuildItem beanContainer) throws LiquibaseException {
    recorder.migrate(beanContainer.getValue());
}

Here, in this processor, we just called the migrate() recorder method, which in turn records the update() Liquibase method for later execution.

7. Testing the Liquibase Extension

To test our extension, we’ll first start by creating a Quarkus application using the quarkus-maven-plugin:

mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create\
-DprojectGroupId=com.baeldung.quarkus.app\
-DprojectArtifactId=quarkus-app

Next, we’ll add our extension as a dependency in addition to the Quarkus JDBC extension corresponding to our underlying database:

<dependency>
    <groupId>com.baeldung.quarkus.liquibase</groupId>
    <artifactId>runtime</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-h2</artifactId>
    <version>1.0.0.CR1</version>
</dependency>

Next, we’ll need to have the quarkus-maven-plugin in our pom file:

<plugin>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-maven-plugin</artifactId>
    <version>${quarkus.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>build</goal>
            </goals>
        </execution>
    </executions>
</plugin>

This is especially useful for running the application using the dev goal or building an executable using the build goal.

And next, we’ll provide data source configuration through the application.properties file located in src/main/resources:

quarkus.datasource.url=jdbc:h2:mem:testdb
quarkus.datasource.driver=org.h2.Driver
quarkus.datasource.username=user
quarkus.datasource.password=password

Next, we’ll provide the changelog configuration for our changelog file:

quarkus.liquibase.change-log=db/liquibase-changelog-master.xml

Finally, we can start the application either in dev mode:

mvn compile quarkus:dev

Or in production mode:

mvn clean package
java -jar target/quarkus-app-1.0-SNAPSHOT-runner.jar

8. Conclusion

In this article, we implemented a Quarkus extension. As an example, we have showcased how to get Liquibase running on top of a Quarkus application.

The full code source is available over on GitHub.