Quarkus Extension Dev Service Personnalisé - Partie III

Dans le précédent article, nous avons vu comment lancer automatiquement un conteneur exposant une API et relié à sa propre base de données grâce à une extension Quarkus. Nous allons maintenant voir comment communiquer avec cette api depuis notre application principale.

Pour appeler une API, il nous faut son URL !

Dans le précédent article, nous avions sauvegardé l’URL d’accès à notre API dans un RunningDevService. Nous allons maintenant utiliser cette URL pour permettre d’injecter cette valeur dans notre application principale. Pour ce faire, nous allons créer une nouvelle classe nommée DemoClientApiProcessor :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@BuildSteps(onlyIf = { GlobalDevServicesConfig.Enabled.class })
public class DemoClientApiProcessor {

    @Record(ExecutionTime.RUNTIME_INIT)
    @BuildStep
    public void createSDK(List<DevServicesResultBuildItem> devServicesResultBuildItem, DemoServiceRecorder recorder, BuildProducer<SyntheticBeanBuildItem> syntheticBeanBuildItemBuildProducer) {
        // Retrieve config set Inside DemoExtensionProcessor
        String apiUrl = devServicesResultBuildItem.stream().filter(devService -> devService.getName().equals(DemoServiceContainer.CONTAINER_NAME))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("Can't find Demo-service url"))
                .getConfig().get("url");

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


    @BuildStep
    public void addSDKBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans, BuildProducer<AdditionalIndexedClassesBuildItem> additionalIndexedClasses) {
        additionalBeans.produce(new AdditionalBeanBuildItem(DemoService.class));
        additionalIndexedClasses.produce(new AdditionalIndexedClassesBuildItem(DemoService.class.getName()));
    }
}

La méthode createSDK annotée avec @BuildStep, prend en paramètre une liste de DevServicesResultBuildItem. Quarkus appellera donc cette méthode après l’instanciation de nos conteneurs de base de données et d’API. Les autres paramètres sont un DemoServiceRecorder et un BuildProducerde SyntheticBeanBuildItem. Le fonctionnement des recorders et syntheticBean a déjà été couvert dans un autre article. En résumé, On récupère l’URL de l’API depuis object devServicesResultBuildItem et on construit un DemoConfig contenant cette URL qui sera injectable.

Le recorder est définie comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package info.techland.demo.extension.runtime;

import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;



@Recorder
public class DemoServiceRecorder {

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

Le recorder est défini dans le package runtime de notre extension. Son rôle est d’instancier notre objet DemoConfig avec l’URL passé en paramètre de manière que Quarkus puisse enregistrer le Bytecode en résultant.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package info.techland.demo.extension.runtime;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class DemoConfig {

    private String url;

    // Only for CDI injection
    @SuppressWarnings("unused")
    private DemoConfig() {}

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

    public String getUrl() {
        return url;
    }

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

Notre DemoConfig est juste un Bean contenant l’URL de notre API une fois déployé comme devService.

1
2
3
4
5
@BuildStep
public void addSDKBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans, BuildProducer<AdditionalIndexedClassesBuildItem> additionalIndexedClasses) {
    additionalBeans.produce(new AdditionalBeanBuildItem(DemoService.class));
    additionalIndexedClasses.produce(new AdditionalIndexedClassesBuildItem(DemoService.class.getName()));
}

Cette étape permet de déclarer notre syntheticBean comme injectable dans notre application finale.

Construire un RestClient dans notre application.

Nous avons maintenant une instance de DemoConfig qui est injectable dans n’importe quelle classe de notre application principale. Commençons par créer une interface pour notre RestClient dans notre application principale afin d’appeler notre API Demo.

1
2
3
4
5
6
7
8
9
package info.techland.demo.app;


@Path("/api")
public interface DemoService {

    @GET
    Set<Demo> fetchAll();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Somewhere in our principal application...
 */
@Path("")
public class AppResource {

    private DemoConfig demoConfig;
    private final DemoService demoService;

    public AppResource(DemoConfig demoConfig) {
        this.demoConfig = demoConfig;

        demoService = QuarkusRestClientBuilder.newBuilder()
            .baseUri(URI.create(demoConfig.getUrl()))
            .build(ExtensionsService.class);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Set<Demo> fetchDemo() {
        return this.demoService.fetch();
    }
}

Dans notre application principale, nous injections notre objet DemoConfig contenant l’URL de notre API et nous nous en servons pour configurer un RestClient de manière programmatique. Notre méthode fetchDemo retourne un Set d’object Demo qui est un simple POJO. Notre application principale est maintenant reliée à notre API et le tout de manière automatique. Mais cela peut encore être amélioré. Et si notre RestClient n’était pas créé par notre application principale, mais par notre extension et injectable comme notre DemoConfig ?

Migrer le RestClient dans notre extension

Pour faire cela, c’est assez simple. Dans un premier temps nous allons déplacer notre classe DemoService dans notre extension, dans le package runtime. Ensuite, nous allons créer une classe DemoResource en charge de créer notre RestClient.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package info.techland.demo.extension.runtime;

import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import java.net.URI;
import java.util.List;

@Path("/")
public class DemoResource {

    private DemoService demoService;

    // Only for CDI injection
    @SuppressWarnings("unused")
    private DemoResource() {
    }

    public DemoResource(String url) {
        this.demoService = QuarkusRestClientBuilder.newBuilder()
                .baseUri(URI.create(url))
                .build(DemoService.class);
    }

    @GET
    public Set<Demo> fetchAsync() {
        return demoService.fetchAsync();
    }
}

Le POJO Demo est aussi déplacé dans notre extension afin disponible dans n’importe quelle application utilisant notre extension.

Ensuite dans notre DemoServiceRecorder, nous allons ajouter l’instanciation de notre DemoResource :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Recorder
public class DemoServiceRecorder {

    public RuntimeValue<DemoResource> createSDK(String url) {
        return new RuntimeValue<>(new DemoResource(url));
    }

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

Dernière étape, configurer Quarkus pour injecter notre DemoResource via un SyntheticBean dans notre DemoClientApiProcessor :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@BuildSteps(onlyIf = { GlobalDevServicesConfig.Enabled.class })
public class DemoClientApiProcessor {

    @Record(ExecutionTime.RUNTIME_INIT)
    @BuildStep
    public void createSDK(List<DevServicesResultBuildItem> devServicesResultBuildItem, DemoServiceRecorder recorder, BuildProducer<SyntheticBeanBuildItem> syntheticBeanBuildItemBuildProducer) {
        // Retrieve config set Inside DemoExtensionProcessor
        String apiUrl = devServicesResultBuildItem.stream().filter(devService -> devService.getName().equals(DemoServiceContainer.CONTAINER_NAME))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("Can't find Demo-service url"))
                .getConfig().get("url");

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

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


    @BuildStep
    public void addSDKBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans, BuildProducer<AdditionalIndexedClassesBuildItem> additionalIndexedClasses) {
        additionalBeans.produce(new AdditionalBeanBuildItem(DemoService.class));
        additionalIndexedClasses.produce(new AdditionalIndexedClassesBuildItem(DemoService.class.getName()));
    }

    /**
     * A jandex file is mandatory by the maven-quarkus-plugin to resolve CDI dependency in the package phase.
     * At runtime, we need to remove this beans because we provide our own via syntheticBean
     * @param buildExclusionsBuildItemBuildProducer Producer for beans that will be excluded
     */
    @BuildStep
    public void removeJandexDemoBean(BuildProducer<ExcludedTypeBuildItem> buildExclusionsBuildItemBuildProducer) {
        buildExclusionsBuildItemBuildProducer.produce(new ExcludedTypeBuildItem(DemoConfig.class.getName()));
        buildExclusionsBuildItemBuildProducer.produce(new ExcludedTypeBuildItem(DemoResource.class.getName()));
    }
}

Notre RestClient est maintenant utilisable via injection d’un DemoResource dans notre application. Reprenons le code de notre AppResource qui est maintenant beaucoup plus simple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Path("")
public class AppResource {

    private final DemoResource demoResource;

    public AppResource(DemoResource demoResource) {
        this.demoResource = demoResource;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Set<Demo> fetchDemo() {
        return this.demoResource.fetch();
    }
}

Conclusion

Voilà qui conclue notre série d’articles sur l’utilisation des extensions Quarkus pour développeur des devs services personnalisés. Pour les consommateurs de nos API, l’utilisation est grandement simplifiée. Un simple import Maven suffit. Un SDK est disponible rendant l’appelle à notre API très simple. Et les évolutions de notre service sont maintenant aussi simple que la mise à jour de la dépendance Maven dans le projet. Lors d’un changement dans notre API cela pourra possiblement être réalisé de manière transparente par le SDK et sinon sera détectable à la compilation via un changement de signature de méthode Java.

0%