In the previous article, we discussed how to automatically launch a container that exposes an API connected to its own database using a Quarkus extension. Now, we will explore how to communicate with this API from our main application.
To Call an API, We Need Its URL!
In the previous article, we saved the access URL for our API in a RunningDevService. Now, we’ll use this URL to inject this value into our main application. To do this, we will create a new class named 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()));}}
The createSDK method, annotated with @BuildStep, takes a list of DevServicesResultBuildItem as a parameter. Quarkus will call this method after instantiating our database and API containers. The other parameters include a DemoServiceRecorder and a BuildProducer of SyntheticBeanBuildItem. Recorders and synthetic beans were already covered in a previous article. In summary, we retrieve the API URL from the devServicesResultBuildItem object and create a DemoConfig containing this URL, which will then be injectable.
The recorder is defined in the runtime package of our extension. Its role is to instantiate our DemoConfig object with the URL passed as a parameter so that Quarkus can record the resulting bytecode.
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;}}
Our DemoConfig is just a bean containing the URL of our API once deployed as a devService.
This step declares our syntheticBean as injectable in our final application.
Building a RestClient in Our Application
Now we have an instance of DemoConfig that is injectable into any class of our main application. Let’s start by creating an interface for our RestClient in our main application to call our Demo API.
/**
* 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();}}
In our main application, we inject our DemoConfig object, which contains the URL of our API, and use it to configure a RestClient programmatically. Our fetchDemo method returns a Set of Demo objects, which is a simple POJO. Our main application is now automatically linked to our API. But this can be improved further. What if our RestClient wasn’t created by our main application, but by our extension and made injectable, like our DemoConfig?
Migrating the RestClient to Our Extension
Do this, it’s quite simple. First, we will move our DemoService class to our extension, within the runtime package. Next, we will create a DemoResource class responsible for creating our 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();}}
The Demo POJO is also moved to our extension so that it’s available in any application using our extension.
Next, in our DemoServiceRecorder, we will add the instantiation of our 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()));}}
Our RestClient is now usable via injection of a DemoResource in our application. Let’s update the code in our AppResource, which is now much simpler:
This concludes our series on using Quarkus extensions to develop custom dev services. For API consumers, usage is greatly simplified. A simple Maven import is all that’s required, providing a readily available SDK that simplifies calling our API. Future updates to our service are now as easy as updating the Maven dependency in the project. When there is a change in our API, it may be handled transparently by the SDK; otherwise, it will be detectable at compile time through a change in the Java method signature.