MicroProfile REST Client 1.4; Examination of a MicroProfile REST Client Application

By Rebecca Searls | June 01, 2020

In this article I will create a simple MicroProfile Rest Client that calls a remote service. In the process I will discuss the nuances in creating the REST client endpoint. What is unique about @RegisterRestClient. Why both @RegisterRestClient and @Path are required to be declared on the REST client endpoint and the rules in setting the baseUri on that class.

Requriements

The REST (Remote) Service

I've created a REST service that mocks a DB of cartoons organized into categories and provides several endpoints for data retrieval.

@Path("/")
public class CartoonService {

    @GET
    @Path("/graphicNovels")
    @Produces(MediaType.APPLICATION_JSON)
    public List<String> getGraphicNovels ()
    {
        return graphicNovels;
    }

    @GET
    @Path("/tv")
    @Produces(MediaType.APPLICATION_JSON)
    public List<String> getTv ()
    {
        return tv;
    }

    @GET
    @Path("/get/{category}")
    @Produces(MediaType.APPLICATION_JSON)
    public List<String> getCategory ( @PathParam("category") String category)
    {
        List<String> result = null;

        if (category != null && !category.isEmpty()) {
            result = categoryMap.get(category.toLowerCase());
        }

        if (result == null) {
            result = new ArrayList(Arrays.asList("Unknown category"));
        }
        return result;
    }

    /**
     * Simple check that service is available
     * @return
     */
    @GET
    @Path("/ping")
    public String ping ()
    {
        return "CartoonService is alive  ";
    }


    List<String> graphicNovels = new ArrayList(
            Arrays.asList("Nimona",
                    "Watchmen",
                    "Maus: A Survivors Tale"));

    List<String> tv = new ArrayList(
            Arrays.asList("The Jetsons",
                    "Rocky and Bullwinkle"));

    List<String> comics = new ArrayList(
            Arrays.asList("Beetle Baily",
                    "Dick Tracy",
                    "Krazy Kat"));

    HashMap<String, List<String>> categoryMap = new HashMap();

    public CartoonService() {
        categoryMap.put("graphicnovels", graphicNovels);
        categoryMap.put("tv", tv);
        categoryMap.put("comics", comics);
    }
}

I am choosing to provide an implementation of javax.ws.rs.core.Application rather than configuring the app in a web.xml file. This service will be deployed separately from the microprofile rest client app.

@ApplicationPath("/theService")
public class ServiceActivator extends Application {
    /* class body intentionally left blank */
}

The REST Client Interface

To create a MicroProfile REST Client one needs to create an interface using JAX-RS and MicroProfile annotations which represents the remote service. The interface must duplicate the method signature for each method to be accessed, (i.e. annotations, return type, list of arguments, and exception declarations), with one exception, only one HTTP (verb) method annotation is allowed per method in the interface.

The Microprofile-Rest-Client specification, section Invalid Client Interface Examples, states,

"The client interface method may contain, at most, one HTTP method annotation 
....
If a (interface) method is annotated with more than one HTTP method, 
the implementation must throw a RestClientDefinitionException."

If the remote service method being duplicated has multiple HTTP method annotations, select only one of them to use on the method in the interface. The resource class created to use this interface can be defined in a way to make reference the other HTTP methods.

The interface class must also have assigned to it annotations RegisterRestClient and Path.

@RegisterRestClient(baseUri ="http://localhost:8888/some-root-context")
@Path("/theService")
@Singleton
public interface CartoonServiceIntf {

    @GET
    @Path("/graphicNovels/json")
    @Produces(MediaType.APPLICATION_JSON)
    List<String> getGraphicNovels ();
    
    @GET
    @Path("/get/{category}")
    @Produces(MediaType.APPLICATION_JSON)
    List<String> getCategory ( @PathParam("category") String category);
    
}

Side Note:

The specification also states that RestClientBuilder's baseUrl method can set the baseUri for the interface class, but this is not overriding the RegisterRestClient's baseUri or properties file value. It is creating a proxy object from a regular old POJO, the interface. The setting in RegisterRestClient and the properties file come into play when CDI creates the bean and used when the bean is injected into a class.

Setting "base URL" Order of Precedence

The Microprofile Configuration specification defines this in detail. Here are the general rules.

Examples of valid and invalid baseUri settings

The default value, "" for baseUri is overridden by the properties file setting. The url is a valid uri string and a valid (functioning) address.

@RegisterRestClient
public interface CartoonServiceIntf {   ...
   
microprofile-config.properties
org.jboss.rest.comics.patron.CartoonServiceIntf/mp-rest/url=http://localhost:8080/microprofile-rest-client-service

BaseUri is set to a dummy but parsable uri. The property in the properties file overrides baseUri setting. This is a valid setting.

@RegisterRestClient(baseUri ="http://localhost:8888/SOME-ROOT-CONTEXT")
 
microprofile-config.properties
org.jboss.rest.comics.patron.CartoonServiceIntf/mp-rest/url=http://localhost:8080/microprofile-rest-client-service

BaseUri is set to a dummy parsable uri however the property in the properties file is not a parsable uri. The character, ''\'' is illegal.
Wildfly will throw an exception when the client calls the endpoint.

@RegisterRestClient(baseUri ="http://localhost:8888/some-root-context")
 
microprofile-config.properties
org.jboss.rest.comics.patron.CartoonServiceIntf/mp-rest/url=http://localhost:8080\microprofile-rest-client-service

Both baseUri and mp-rest/url are parsable uris, however the url in the properties file is not a reachable address and it overrides the baseUri value. Wildfly will throw an exception when the client calls the endpoint because the service can not be found at that address.

@RegisterRestClient(baseUri ="http://localhost:8080/microprofile-rest-client-service")
 
microprofile-config.properties
org.jboss.rest.comics.patron.CartoonServiceIntf/mp-rest/url=http://localhost:8888/some-root-context

The default value of baseUri is "". It is not a parsable uri. No url is provided in the properties file to override it. Wildfly will throw an exception.

@RegisterRestClient
 
microprofile-config.properties
# file intentionally empty

Best Practices

  • When the base URL of the remote service is static and known, define the default base URL in the RegisterRestClient annotation or in the properties file.
  • When the base URL is not yet known and can be determined during the run time, set the base URL through the RestClientBuilder.
  • It's debatable if its better to set a dummy parsable base URI in the RegisterRestClient annotation or leave it unset. Both indicate to the knowledgeable user to declare it in the property file.
Examples of Path settings

If the service's settings are

@ApplicationPath("/theService")
public class ServiceActivator extends Application { ....
 
@Path("/guy")
public class CartoonService {  ....

the Path must be

@Path("/theService/guy")
public interface CartoonServiceIntf { ....

If the service's settings are

@ApplicationPath("/")
public class ServiceActivator extends Application { ....
 
@Path("/api")
public class CartoonService {  ....

the Path must be

@Path("/api")
public interface CartoonServiceIntf { ....

If the service's settings are

@ApplicationPath("/")
public class ServiceActivator extends Application { ....
 
@Path("/")
public class CartoonService {  ....

then Path must be

@Path("/")
public interface CartoonServiceIntf { ....

If the service's settings are

@ApplicationPath("")
public class ServiceActivator extends Application { ....
 
@Path("/")
public class CartoonService {  ....

then Path must be

@Path("/")
public interface CartoonServiceIntf { ....

Side Note

One might think it is easier to just added the Path value in the baseUri but this will not work. The under laying code requires these be set separately.

The service will not be found if baseUri and Path are combined and Path is set to the empty string or "/".

@RegisterRestClient(
baseUri ="http://localhost:8080/microprofile-rest-client-service/theService/guy")
@Path("")
 
microprofile-config.properties
# file intentionally empty

The REST Client Resource

Next a JAX-RS Resource is created. It is just a wrapper class for accessing the methods defined in CartoonServiceIntf. Access to the service is provided by injection.

@Path("/thePatron")
@ApplicationScoped
public class CartoonPatronResource {
    @Inject
    @RestClient CartoonServiceIntf service;

    @GET
    @Path("/all")
    @Produces(MediaType.APPLICATION_JSON)
    public List<String> getAll(){
        List<String> result = new ArrayList<>();
        try {
            result.addAll(service.getCategory("GraphicNovels"));
            result.addAll(service.getCategory("TV"));
            result.addAll(service.getCategory("comics"));
        } catch (Exception e) {
            result.add(e.getMessage());
        }
        return result;
    }

    @GET
    @Path("/get/{category}")
    @Produces(MediaType.APPLICATION_JSON)
    public List<String> getCategory (@PathParam("category") String category){

        return service.getCategory(category);
    }

    /**
     * Simple check that service is available
     * @return
     */
    @GET
    @Path("/hello")
    public String getHello() {
        if (service == null) {
            return "Hello back.  the service is null";
        }
        return "Hello from CartoonPatronResource  ";
    }
}

The Microprofile-Rest-Client specification, section MicroProfile Rest Client CDI Support, states

... for each interface annotated with RegisterRestClient. The bean 
created will include a qualifier @RestClient to differentiate the use 
as an API call against any other beans registered of the same type. Based on
the rules of how CDI resolves bean, you are only required to use the qualifier 
if you have multiple beans of the same type. Any injection point or programmatic 
look up that uses the qualifier RestClient is expected to be resolved by the 
MicroProfile Rest Client runtime.

In this scenario we have only one bean of type, CartoonServiceIntf, so the RestClient annotation is not required but it seems best practice would be to always declare it in order to provide clarity to the reader and to avoid future code maintenance issues.

Just as with the remote service I am choosing to provide an implementation of javax.ws.rs.core.Application for this web app. This client app will be deployed separately from the service app.

@ApplicationPath("/")
public class PatronActivator extends Application {
    /* class body intentionally left blank */
}

Build and Deploy

mvn clean package
cp ./service/target/microprofile-rest-client-service.war ${WILDFLY_HOME}/standalone/deployments/.
cp ./client/target/microprofile-rest-client-client.war ${WILDFLY_HOME}/standalone/deployments/.
${WILDFLY_HOME}/bin/standalone.sh

Test the Service

Using cURL check that the remote service is accessible.

curl -v http://localhost:8080/microprofile-rest-client-service/theService/ping

CartoonService is alive  
curl -v http://localhost:8080/microprofile-rest-client-service/theService/get/comics

["Beetle Baily","Dick Tracy","Krazy Kat"]

Test the Client

Using cURL check that the client is working.

curl -v http://localhost:8080/microprofile-rest-client-client/thePatron/hello

Hello from CartoonPatronResource
curl -v http://localhost:8080/microprofile-rest-client-client/thePatron/all

["Nimona","Watchmen","Maus: A Survivors Tale","The Jetsons","Rocky and Bullwinkle","Beetle Baily","Dick Tracy","Krazy Kat"]