Microprofile Dynamic Rest Client

In Microprofile, using the @RegisterRestClient annotation requires that you define either a baseURI as part of the annotation, or defined in the application properties file. However such definition is static and if you need to change it during runtime, you wont be able to do it as part of the annotation.

You could create a custom ConfigSource and dynamically load it. However this will not apply the changes to the currently injected @RestClient service, it will apply it the next time the @RestClient service is injected again.

Think of it as delayed loading of configuration properties. Probably that might not be what you need.

public class InMemoryConfigSource implements ConfigSource, Serializable {

...Your implementation here.
}

Config customConfig =               ConfigProviderResolver.instance().getBuilder().forClassLoader(Thread.currentThread().getContextClassLoader())                    .withSources(InMemoryConfigSource.config("my.prop", "5678"))
                        .addDefaultSources()
                        .addDiscoveredSources()
                        .addDiscoveredConverters()
                        .build();

ConfigProviderResolver.instance().registerConfig(customConfig, Thread.currentThread().getContextClassLoader());

There is another option that Eclipse Microprofile provides, which is RestClientBuilder. This allows you to set a baseURI during runtime and returning an instance of your @RestClientService. We will cover the latter.


We will create a Rest Client service using the annotation @RegisterRestClient and we will create an application.properties file under /test/resources. This service will point to the default url http://localhost:8080/v1 when tests are initially run.

@RegisterRestClient
public interface TvMazeService {

    @GET
    @Path("/singlesearch/shows")
    public TvMazeShow searchShows(@QueryParam("q") String query);
}

In application.properties under /test/resources, we add the following property:

com.example.quarkus.consumer.services.TvMazeService/mp-rest/url:http://localhost:8080/v1

What this does is state which API the TvMazeService is actually consuming. In the static case, it is http://localhost:8080/v1

We will create another Service which will use our RestClient, and also decide on which URL to use by dynamically changing the baseURL.

package com.example.quarkus.services;


import com.example.quarkus.consumer.services.TvMazeService;
import com.example.quarkus.model.TvMazeShow;

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;

import java.net.MalformedURLException;
import java.net.URL;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

@Path("/v1/library")
public class ShowsLibraryService {

    @RestClient
    TvMazeService tvMazeService;

    @Inject
    private Logger log;

    /**
     *  Re-route the injected TvMazeService to a different baseURL when provided.
     * @param showName
     * @param baseURL
     * @return
     * @throws MalformedURLException
     */
    @GET
    public TvMazeShow getSimplifiedShow(@QueryParam("showName") String showName,
                                        @HeaderParam("baseURL") String baseURL) throws MalformedURLException {

        if(baseURL != null){
            tvMazeService =  RestClientBuilder.newBuilder()
                    .baseUrl(new URL(baseURL))
                    .build(TvMazeService.class);

            log.info("Request directed to custom baseURL  ----> " + baseURL);
        }

        return tvMazeService.searchShows(showName);
    }

}


In this example, we specify the custom baseURL as a @HeaderParam but that does not mean you cant cant create your own method of determining which baseURL to use.

We then use the RestClientBuilder.newBuilder() and provide the TvMazeService.class (Our RestClient service) and specify a new URL. Now all requests will be routed to http://localhost:8080/v2 when we specify the @HeaderParam.

For the Integration Test, we use MockServer to stub the responses for /v1 and /v2 and to validate dynamic routing.

package com.example.quarkus;

import com.example.quarkus.services.ShowsLibraryService;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;

import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.net.MalformedURLException;

import javax.inject.Inject;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.filter.log.RequestLoggingFilter;
import io.restassured.filter.log.ResponseLoggingFilter;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
import org.hamcrest.core.IsEqual;


@QuarkusTest
class ShowsLibraryServiceIT {

    private static WireMockServer wireMockServer;

    @BeforeAll
    public static void setupBeforeAll() {
        RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter());

        wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().port(8080));
        wireMockServer.checkForUnmatchedRequests();
        wireMockServer.start();

        stubFor(get(urlPathMatching("/v1/singlesearch/shows"))
                .withQueryParam("q", equalTo("girls"))
                .willReturn(aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withBody(
                                " {\"id\":\"139\",\"url\":\"https://www.tvmaze.com/shows/139/girls\"," +
                                        "\"name\":\"V1ResponseGirls\",\"type\":\"Scripted\"," +
                                        "\"language\":\"English\",\"genres\":[\"Drama\",\"Romance\"],\"runtime\":30,\"averageRunTime\":null,\"status\":\"Ended\"}"
                        )));

        stubFor(get(urlPathMatching("/v2/singlesearch/shows"))
                .withQueryParam("q", equalTo("girls"))
                .willReturn(aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withBody(
                                " {\"id\":\"139\",\"url\":\"https://www.tvmaze.com/shows/139/girls\"," +
                                        "\"name\":\"V2ResponseGirls\",\"type\":\"Scripted\"," +
                                        "\"language\":\"English\",\"genres\":[\"Drama\",\"Romance\"],\"runtime\":30,\"averageRunTime\":null,\"status\":\"Ended\"}"
                        )));
    }

    @AfterAll
    public static void stop() {
        if (wireMockServer != null) {
            wireMockServer.stop();
        }
    }

    @Test
    public void validateDynamicURL_towardsV1_Configuration() throws MalformedURLException {

       RestAssured.given()
                .queryParam("showName", "girls")
                .when().get("/v1/library")
                .then()
                .statusCode(200)
                .body("name", IsEqual.equalTo("V1ResponseGirls"));
    }


    @Test
    public void validateDynamicURL_towardsV2_Configuration() throws MalformedURLException {

        RestAssured.given()
                .queryParam("showName", "girls")
                .header("baseURL", "http://localhost:8080/v2")
                .when().get("/v1/library")
                .then()
                .statusCode(200)
                .body("name", IsEqual.equalTo("V2ResponseGirls"));
    }
}

The code can be found here.

Leave a comment