Quarkus Extension Synthetic Build Item

In the previous post on the AdditionalBeanBuildItem, we discussed how to reference a bean with the ARC dependency manager. While useful, there are cases where you need the manager to inject a specific instance of a pre-configured bean. Since AdditionalBeanBuildItem only accepts a bean class as a parameter, it cannot be used for this purpose. This is where the SyntheticBeanBuildItem comes into play.

Use Case

Imagine your extension needs to provide a configuration as an injectable bean to communicate with a third-party API. You’ll need to instruct Quarkus on which bean instance to inject and with what values.

SyntheticBeanBuildItem

The name of this build item is not very descriptive, and it took me some time to understand its purpose. In summary, it’s an AdditionalBeanBuildItem that accepts 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,” meaning it needs to have a default constructor and accessors. In our case, we use a private default constructor to ensure the class is not used in unintended ways. The Quarkus Maven plugin checks for injectable beans during the packaging phase. Without this default constructor, our project build would fail with an Unsatisfied dependency error. We define our bean with the @ApplicationScoped scope so that 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 will execute it during the build phase and record the bytecode produced by the recorder. During runtime, Quarkus will provide this bytecode directly instead of recalculating it dynamically, which greatly speeds up startup time. In our example, the recorder returns a RuntimeValue<Config> 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 will discuss in a future post. Once the API URL is retrieved, we construct our SyntheticBeanBuildItem with our recorder. The unremovable method ensures that our bean instance will not be removed by Quarkus during the extension’s compilation, even if the bean is not used. Without this, our bean won’t be accessible in the Maven module importing our extension. Since we used @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 class, you can add a missing qualifier using 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 of the AdditionalBeanBuildItem. It uses a Recorder to record the bytecode produced by our instance during Quarkus’s build phase. This bytecode is provided as-is during the application’s runtime, accelerating its startup.

0%