Quarkus and "meta" extension

Quarkus is a great Java framework with a large ecosystem thanks to the extensions.
Basically, if using Maven, your pom.xml contains the list of the extensions you want to use, meaning something like this:

...
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-grpc</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-opentelemetry</artifactId>
</dependency>
...
It can quickly become large, and sometime you can have "conflict" between extensions (for example if you mix in the same dependencies set reactive and non reactive extensions).

Quarkus "meta" extension

To simplify the dependencies for the extensions you want to use (especially if it's basically the same extensions in all your services), you can create your own "meta" extension.
This extension doesn't contain actual code, it "wraps" other extensions in an unique one.
Our "meta" extension is actually a "real" extension, meaning we have the "classic" extension structure with both deployment and runtime modules. Let's start our extension with a pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

	<groupId>net.nanthrax.quarkus</groupId>
    <artifactId>my-meta-extension-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
    	<quarkus.version>3.0.2.Final</quarkus.version>
    </properties>

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

</project>
We see here the two main parts of the Quarkus extension framework:
  • deployment is the buildtime augmentation. It's responsible for all metadata processing (reading annotations, XML descriptors, ...). The output of this augmentation phase is recorded bytecode which is responsible for directly instantiating the relevant runtime services. This means that metadata is only processed once at build time, which both saves on startup time, and also on memory usage as the classes etc that are used for processing are not loaded (or even present) in the rumtime JVM. In our "meta" extension, we do kind of "assembly" of all deployment parts of wrapped extensions.
  • runtime is the actual runtime part of the extension, mostly "injecting" the runtime configuration. Remember that most of the job should be/is done at build time (deployment phase).

Let's start with our extension deployment (the buildtime part). The pom.xml is pretty simple:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
            
    <modelVersion>4.0.0</modelVersion>
                
    <parent>    
        <groupId>net.nanthrax.quarkus</groupId>
        <artifactId>my-meta-extension-parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>               
                            
    <artifactId>my-meta-extension-deployment</artifactId>
                
    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-core-deployment</artifactId>
            <version>${quarkus.version}</version>
        </dependency>

        <!-- Quarkus extensions -->
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-grpc-deployment</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-kubernetes-client-deployment</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-kubernetes-config-deployment</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-logging-json-deployment</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-opentelemetry-deployment</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>io.quarkus</groupId>
                            <artifactId>quarkus-extension-processor</artifactId>
                            <version>${quarkus.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
Our meta-extension deployment lists all extensions it will bring, in our case:
  • quarkus-grpc-deployment
  • quarkus-kubernetes-client-deployment
  • quarkus-kubernetes-config-deployment
  • quarkus-logging-json-deployment
  • quarkus-opentelemetry-deployment
A good point is that, even if the artifacts in your application, if you don't use annotation or bean for one annotation (let's say kubernetes-client), the extension is not actually loaded by Quarkus (it's lazy loaded), meaning that you can include any extension you want, it will be actually loaded only if the application using your "meta" extension use a service from an extension: you can include extensions you don't actually use yet, it has no impact on your application.
The pom.xml is the only thing we need for our "meta" extension deployment module.
Let's create the runtime module now for our "meta" extension.
For the runtime module, we need two parts:
  • the pom.xml lists the runtime artifacts of the extensions provided by our "meta" extension (and also use the quarkus-extension-maven-plugin)
  • a quarkus-extension.yaml resource file describing our "meta" extension

Let's start with the pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.nanthrax.quarkus</groupId>
        <artifactId>my-meta-extension-parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>my-meta-extension</artifactId>

    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-grpc</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-kubernetes-client</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-kubernetes-config</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-logging-json</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-opentelemetry</artifactId>
            <version>${quarkus.version}</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>io.quarkus</groupId>
                <artifactId>quarkus-extension-maven-plugin</artifactId>
                <version>${quarkus.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>io.quarkus</groupId>
                            <artifactId>quarkus-extension-processor</artifactId>
                            <version>${quarkus.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
We can see here the runtime artifacts of the extensions provided by our "meta" extension:
  • quarkus-grpc
  • quarkus-kubernetes-client
  • quarkus-kubernetes-config
  • quarkus-logging-json
  • quarkus-opentelemetry
We use the quarkus-extension-maven-plugin to generate all required resources for our "meta" extension.
Now, we have to create the "meta" extension descriptor src/main/resources/META-INF/quarkus-extension.yaml:

---
artifact: ${project.groupId}:${project.artifactId}:${project.version}
name: "My Meta Extension"
metadata:
  keywords:
    - "my-meta-extension"
  guide: "https://jbonofre.github.com/my-meta-extension"
  categories:
    - "cloud"
  status: "testing"
  config:
    - "net.nanthrax.quarkus."
Our "meta" extension is now ready, we just have to build it with mvn clean install.

Usage and advantages of your "meta" extension

To use our "meta-extension", we just need to define our extension as dependency of our application:

...
	<dependencies>
    	...
        <dependency>
        	<groupId>net.nanthrax.quarkus</groupId>
            <artifactId>my-meta-extension</artifactId>
            <version>1.0..0-SNAPSHOT</version>
        </dependency>
        ...
    </dependencies>    
...
That's it ! We now have all extensions coming from our meta one. It means, in our application, we have all beans and annotations from all extensions defined in our "meta" extension.
This "meta" extension has several benefits:
  • you have an unique dependency bringing all extensions transitively
  • you have all annotations and beans available in your IDE with an unique extension
  • the "meta" extension can be managed and released by a team (a kind of "framework" team), validating the extensions other teams can use and with guarantee of good working all together

I'm using this approach in a framework PoC I'm doing in my company, and it's pretty convenient to "hide" actual extensions to the teams just by using the "meta" one.

Comments

Popular posts from this blog

Getting started with Apache Karaf Minho

Apache Karaf Minho and OpenTelemetry