Quarkus Extension Synthetic Build Item

Dans le précédent post sur le buildItem AdditionalBeanBuildItem, nous avons vu comment référencer un bean au gestionnaire de dépendance ARC. Cela est très pratique, mais dans certains cas, nous voulons pouvoir que le gestionnaire injecte une instance spécifique d’un bean déjà configuré. L’AdditionalBeanBuildItem ne prenant qu’une classe de bean en paramètre, il ne peut pas être utilisé pour cela. C’est ici qu’intervient le SyntheticBeanBuildItem

Cas d’usage.

Imaginons que votre extension souhaite fournir une configuration sous forme d’un bean injectable pour aller communiquer avec une API tierce. Il va falloir dire à Quarkus quelle instance de bean injecter et avec quelles valeurs.

SyntheticBeanBuildItem

Le nom de ce buildItem n’est pas très parlant et j’ai mis pas mal de temps à comprendre à quoi il pouvait bien servir. En résumé, il s’agit d’un AdditionalBeanBuildItem prenant une instance de classe au lieu d’une classe en paramètre. Cela le rend très puissant. Pour produire une instance de classe, il va tout d’abord falloir fournir cette instance sous la forme d’un Recorder.

Recorder

Commençons par créer notre Bean de configuration d’API dans le module runtime de notre 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;
    }
}

Notre bean doit être “CDI compatible”. Pour cela, il doit comporter un constructeur par défaut ainsi que des accesseurs. Dans notre cas, nous utilisons un constructeur par défaut privé pour que la classe ne puisse pas être utilisée d’une manière non prévue. Le plugin maven quarkus vérifie la présence des Bean injectable lors de la phase de package. Sans ce constructeur par défaut le build de notre projet plante avec une erreur Unsatisfied dependency. Nous définissons notre bean en scope @ApplicationScoped pour que la même instance soit toujours injectée.

Créons maintenant le recorder pour notre classe Config :

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

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

Lorsque qu’un recorder est déclaré, Quarkus va exécuter le recorder lors de la phase de build et enregistrer le bytecode produit par ce recorder. Lors de la phase de runtime, quarkus fournira se bytecode directement au lieu d’avoir à le recalculer dynamiquement. Cela permet d’accélérer grandement le temps de démarrage. Dans notre exemple, le recorder retourne un object RuntimeValue<Config> contenant l’instance de notre configuration. Ce RuntimeValue sera consommer par notre 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());
    }

Pour configurer notre syntheticBuildItem nous utilisons une méthode annotée avec @Record(ExecutionTime.RUNTIME_INIT). Ici, le ExecutionTime.STATIC_INIT ne fonctionnera pas, car notre recorder ne sera pas encore disponible. Ensuite, nous récupérons notre URL d’API via un DevServicesResultBuildItem dont nous parlerons dans un prochain article. Une fois l’url de l’API récupérée, nous construisons notre SyntheticBeanBuildItem avec notre recorder. La méthode unremovable s’assure que notre instance de bean ne sera pas supprimer par Quarkus lors de la compilation de notre extension même si ce bean n’est pas utilisé. Sans cela, notre bean ne sera pas accessible dans le module maven important notre extension. Comme nous avons utilisé @Record(ExecutionTime.RUNTIME_INIT), nous informons Quarkus que notre instance de bean sera disponible lors de la phase RuntimeInit grâce à la méthode setRuntimeInit. Enfin, nous passons l’instance de notre bean de config à la méthode runtimeValue pour finir d’initialiser notre SyntheticBeanBuildItem.

Si vous n’avez pas le contrôle sur la classe du bean produit, vous pouvez ajouter un qualifier manquant via la méthode .addQualifier(ApplicationScoped.class)

Injection du Bean

Dans un projet important notre extension :

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

Nous pouvons maintenant injecter notre Bean de configuration pour l’utiliser :

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

    private final Config config;

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

## Conclusion

Un SyntheticBeanBuildItem est le pendant dynamique de l’AdditionalBeanBuildItem. Il utilise un Recorder pour enregistrer le bytecode produit par notre instance lors de la phase de Build de quarkus. Ce bytecode sera fournis tel quel lors de l’exécution de l’application afin d’accélérer son démarrage.

0%