Quarkus Extension Additional Application Archive Marker Build Item

In the previous post about the AdditionalBeanBuildItem, we saw how to reference a bean in the ARC dependency manager. This is very convenient, but in some cases, we want the manager to inject a specific instance of an already configured bean. Since AdditionalBeanBuildItem only takes a bean class as a parameter, it can’t be used for this. This is where SyntheticBeanBuildItem comes in.

Use Case

Imagine your extension wants to provide a configuration as an injectable bean to communicate with a third-party API. You need to tell Quarkus which bean instance to inject and with what values.

SyntheticBeanBuildItem

The name of this build item isn’t very descriptive, and it took me a while to understand its purpose. In summary, it’s an AdditionalBeanBuildItem that takes an instance of a class instead of a class as a parameter, making it very powerful. To produce a class instance, you first need to provide this instance in the form of a Recorder.

Recorder

Let’s start by creating our API configuration bean in the runtime module of our extension:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@ApplicationScoped
public class Config {

    private String url;

    // Only for CDI injection during mvn package phase
    @SuppressWarnings("unused")
    private Config() {}

    public Config(String url) {
        this.url = url;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }
}

Our bean must be “CDI compatible”. This requires a default constructor and accessors. In our case, we use a private default constructor to prevent unintended use. The Quarkus Maven plugin checks for injectable beans during the package phase. Without this default constructor, our project build fails with an Unsatisfied dependency error. We define our bean in the @ApplicationScoped scope to ensure the same instance is always injected.

Now let’s create the recorder for our Config class:

1
2
3
4
5
6
7
@Recorder
public class ConfigRecorder {

    public RuntimeValue<Config> createConfig(String url) {
        return new RuntimeValue<>(new Config(url));
    }
}

When a recorder is declared, Quarkus executes it during the build phase and records the produced bytecode. At runtime, Quarkus provides this bytecode directly instead of recalculating it dynamically, greatly speeding up startup time. In our example, the recorder returns a RuntimeValue object containing our configuration instance. This RuntimeValue will be consumed by our SyntheticBeanBuildItem.

BuildStep

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
public void createConfig(List<DevServicesResultBuildItem> devServicesResultBuildItem, ConfigRecorder recorder, BuildProducer<SyntheticBeanBuildItem> syntheticBeanBuildItemBuildProducer) {
    // Retrieve config set Inside ApiContainer
    String apiUrl = devServicesResultBuildItem.stream().filter(devService -> devService.getName().equals(ApiContainer.CONTAINER_NAME))
            .findFirst()
            .orElseThrow(() -> new IllegalStateException("Can't find api url"))
            .getConfig().get("url");

    syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(ConfigRecorder.class)
            .unremovable()
            .setRuntimeInit()
            .runtimeValue(recorder.createConfig("http://"+apiUrl))
            .done());
}

To configure our syntheticBuildItem, we use a method annotated with @Record(ExecutionTime.RUNTIME_INIT). Here, ExecutionTime.STATIC_INIT won’t work because our recorder won’t be available yet. Next, we retrieve our API URL via a DevServicesResultBuildItem, which we’ll discuss in a future article. Once the API URL is obtained, we build our SyntheticBeanBuildItem with our recorder. The unremovable method ensures our bean instance isn’t removed by Quarkus during the extension compilation even if the bean isn’t used. Without this, our bean wouldn’t be accessible in the Maven module importing our extension. By using @Record(ExecutionTime.RUNTIME_INIT), we inform Quarkus that our bean instance will be available during the RuntimeInit phase using the setRuntimeInit method. Finally, we pass our config bean instance to the runtimeValue method to complete the initialization of our SyntheticBeanBuildItem.

If you don’t have control over the produced bean’s class, you can add a missing qualifier via the .addQualifier(ApplicationScoped.class) method.

Bean Injection

In a project importing our extension:

1
2
3
4
5
<dependency>
    <groupId>org.example</groupId>
    <artifactId>example-extension</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

We can now inject our configuration bean to use it:

1
2
3
4
5
6
7
8
public class MyClassExample {

    private final Config config;

    public MyClassExample(Config config) {
        this.config = config;
    }
}

Conclusion

A SyntheticBeanBuildItem is the dynamic counterpart to the AdditionalBeanBuildItem. It uses a Recorder to record the bytecode produced by our instance during the Quarkus build phase. This bytecode is provided as-is at runtime to speed up application startup.

0%