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 :
@BuildSteps(onlyIf={GlobalDevServicesConfig.Enabled.class})publicclassDemoClientApiProcessor{@Record(ExecutionTime.RUNTIME_INIT)@BuildSteppublicvoidcreateSDK(List<DevServicesResultBuildItem>devServicesResultBuildItem,DemoServiceRecorderrecorder,BuildProducer<SyntheticBeanBuildItem>syntheticBeanBuildItemBuildProducer){// Retrieve config set Inside DemoExtensionProcessorStringapiUrl=devServicesResultBuildItem.stream().filter(devService->devService.getName().equals(DemoServiceContainer.CONTAINER_NAME)).findFirst().orElseThrow(()->newIllegalStateException("Can't find Demo-service url")).getConfig().get("url");syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(DemoConfig.class).unremovable().setRuntimeInit().runtimeValue(recorder.createConfig("http://"+apiUrl)).done());}@BuildSteppublicvoidaddSDKBeans(BuildProducer<AdditionalBeanBuildItem>additionalBeans,BuildProducer<AdditionalIndexedClassesBuildItem>additionalIndexedClasses){additionalBeans.produce(newAdditionalBeanBuildItem(DemoService.class));additionalIndexedClasses.produce(newAdditionalIndexedClassesBuildItem(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é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.
packageinfo.techland.demo.extension.runtime;importjakarta.enterprise.context.ApplicationScoped;@ApplicationScopedpublicclassDemoConfig{privateStringurl;// Only for CDI injection@SuppressWarnings("unused")privateDemoConfig(){}publicDemoConfig(Stringurl){this.url=url;}publicStringgetUrl(){returnurl;}publicvoidsetUrl(Stringurl){this.url=url;}}
Notre DemoConfig est juste un Bean contenant l’URL de notre API une fois déployé comme devService.
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.
/**
* Somewhere in our principal application...
*/@Path("")publicclassAppResource{privateDemoConfigdemoConfig;privatefinalDemoServicedemoService;publicAppResource(DemoConfigdemoConfig){this.demoConfig=demoConfig;demoService=QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(demoConfig.getUrl())).build(ExtensionsService.class);}@GET@Produces(MediaType.APPLICATION_JSON)publicSet<Demo>fetchDemo(){returnthis.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.
packageinfo.techland.demo.extension.runtime;importio.quarkus.rest.client.reactive.QuarkusRestClientBuilder;importio.smallrye.mutiny.Uni;importjakarta.ws.rs.GET;importjakarta.ws.rs.Path;importjava.net.URI;importjava.util.List;@Path("/")publicclassDemoResource{privateDemoServicedemoService;// Only for CDI injection@SuppressWarnings("unused")privateDemoResource(){}publicDemoResource(Stringurl){this.demoService=QuarkusRestClientBuilder.newBuilder().baseUri(URI.create(url)).build(DemoService.class);}@GETpublicSet<Demo>fetchAsync(){returndemoService.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 :
@BuildSteps(onlyIf={GlobalDevServicesConfig.Enabled.class})publicclassDemoClientApiProcessor{@Record(ExecutionTime.RUNTIME_INIT)@BuildSteppublicvoidcreateSDK(List<DevServicesResultBuildItem>devServicesResultBuildItem,DemoServiceRecorderrecorder,BuildProducer<SyntheticBeanBuildItem>syntheticBeanBuildItemBuildProducer){// Retrieve config set Inside DemoExtensionProcessorStringapiUrl=devServicesResultBuildItem.stream().filter(devService->devService.getName().equals(DemoServiceContainer.CONTAINER_NAME)).findFirst().orElseThrow(()->newIllegalStateException("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());}@BuildSteppublicvoidaddSDKBeans(BuildProducer<AdditionalBeanBuildItem>additionalBeans,BuildProducer<AdditionalIndexedClassesBuildItem>additionalIndexedClasses){additionalBeans.produce(newAdditionalBeanBuildItem(DemoService.class));additionalIndexedClasses.produce(newAdditionalIndexedClassesBuildItem(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
*/@BuildSteppublicvoidremoveJandexDemoBean(BuildProducer<ExcludedTypeBuildItem>buildExclusionsBuildItemBuildProducer){buildExclusionsBuildItemBuildProducer.produce(newExcludedTypeBuildItem(DemoConfig.class.getName()));buildExclusionsBuildItemBuildProducer.produce(newExcludedTypeBuildItem(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 :
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.