Consuming RESTful services with template interfaces

duration 20 minutes

Learn how to use MicroProfile Rest Client to invoke RESTful microservices over HTTP in a type-safe way.

What you’ll learn

You will learn how to build a MicroProfile Rest Client to access remote RESTful services. You will create a template interface that maps to the remote service that you want to call. MicroProfile Rest Client automatically generates a client instance based on what is defined and annotated in the template interface. Thus, you don’t have to worry about all of the boilerplate code, such as setting up a client class, connecting to the remote server, or invoking the correct URI with the correct parameters.

The application that you will be working with is an inventory service, which fetches and stores the system property information for different hosts. Whenever a request is made to retrieve the system properties of a particular host, the inventory service will create a client to invoke the system service on that host. The system service simulates a remote service in the application.

You will instantiate the client and use it in the inventory service. You can choose from two different approaches, Context and Dependency Injection (CDI) with the help of MicroProfile Config or the RestClientBuilder method. In this guide, you will explore both methods to handle scenarios for providing a valid base URL.

  • When the base URL of the remote service is static and known, define the default base URL in the configuration file. Inject the client with CDI method.

  • When the base URL is not yet known and needs to be determined during the run time, set the base URL as a variable. Build the client with the more verbose RestClientBuilder method.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-microprofile-rest-client.git
cd guide-microprofile-rest-client

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project, which is what you will build.

Try what you’ll build

To try out the application, first navigate to the finish directory, which contains the complete inventory application. Then, run the following Maven goals to build the application and run it inside Open Liberty:

mvn install liberty:start-server

After you start the application, you can access the following microservices:

When you are done checking out the application, stop the Open Liberty server. You must stop your running Open Liberty server before you re-install it next time.

mvn liberty:stop-server

Writing the RESTful client interface

Now, navigate to the start directory to begin.

The MicroProfile Rest Client API is added as a dependency to your pom.xml file. Look for the dependency with the microprofile-rest-client-api artifact ID. This dependency provides the library that is required to implement the MicroProfile Rest Client interface.

The mpRestClient-1.0 feature is also enabled in the src/main/liberty/config/server.xml file. This feature enables your Open Liberty server to use MicroProfile Rest Client to invoke RESTful microservices.

The code for the system service in the src/main/java/io/openliberty/guides/system directory is provided for you. It simulates a remote RESTful service that the inventory service invokes.

Create a RESTful client interface for the system service. Write a template interface that maps the API of the remote system service. The template interface describes the remote service that you want to access. The interface defines the resource to access as a method by mapping its annotations, return type, list of arguments, and exception declarations.

Create the SystemClient class in the src/main/java/io/openliberty/guides/inventory/client/SystemClient.java file:

package io.openliberty.guides.inventory.client;

import java.util.Properties;
import javax.enterprise.context.Dependent;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@Dependent
@RegisterRestClient
@RegisterProvider(UnknownUrlExceptionMapper.class)
@Path("/properties")
public interface SystemClient {

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public Properties getProperties() throws UnknownUrlException, ProcessingException;
}

The MicroProfile Rest Client feature automatically builds and generates a client implementation based on what is defined in the SystemClient interface. Thus, you don’t waste time on setting up the client and connecting with the remote service.

When the getProperties() method is invoked, the SystemClient instance sends a GET request to the <baseUrl>/properties endpoint.

The @RegisterProvider annotation tells the framework to register the provider classes to be used when the framework invokes the interface. You can add as many providers as necessary. In the SystemClient interface, add a response exception mapper as a provider to map the 404 response code with the UnknownUrlException exception.

Handling exceptions through ResponseExceptionMappers

Error handling is an important step to ensure that the application can fail safely. If there is an error response such as 404 NOT FOUND when invoking the remote service, you need to handle it. First, define an exception, and map the exception with the error response code. Then, register the exception mapper in the client interface.

Look at the client interface again, the @RegisterProvider(UnknownUrlExceptionMapper.class) annotation registers the UnknownUrlExceptionMapper response exception mapper. An exception mapper maps various response codes from the remote service to throwable exceptions.

Implement the actual exception class and the mapper class to see how this mechanism works.

Create an UnknownUrlException exception class in the src/main/java/io/openliberty/guides/inventory/client/UnknownUrlException.java file:

package io.openliberty.guides.inventory.client;

public class UnknownUrlException extends Exception {

  private static final long serialVersionUID = 1L;

  public UnknownUrlException() {
    super();
  }

  public UnknownUrlException(String message) {
    super(message);
  }
}

Now, link the UnknownUrlException class with the corresponding response code through a ResponseExceptionMapper mapper class. Create an UnknownUrlExceptionMapper mapper class in the src/main/java/io/openliberty/guides/inventory/client/UnknownUrlExceptionMapper.java file:

package io.openliberty.guides.inventory.client;

import java.util.logging.Logger;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;

@Provider
public class UnknownUrlExceptionMapper
    implements ResponseExceptionMapper<UnknownUrlException> {
  Logger LOG = Logger.getLogger(UnknownUrlExceptionMapper.class.getName());

  @Override
  public boolean handles(int status, MultivaluedMap<String, Object> headers) {
    LOG.info("status = " + status);
    return status == 404;
  }

  @Override
  public UnknownUrlException toThrowable(Response response) {
    return new UnknownUrlException();
  }
}

The handles() method inspects the HTTP response code to determine whether an exception is thrown for the specific response, and the toThrowable() method returns the mapped exception.

Injecting the client with dependency injection

Now, instantiate the SystemClient interface and use it in the inventory service. If you want to connect only with the default host name, you can easily instantiate the SystemClient with CDI annotations. CDI injection simplifies the process of bootstrapping the client.

First, you need to define the base URL of the SystemClient instance. Configure the default base URL with the MicroProfile Config feature. This feature is enabled for you in both server.xml and pom.xml files.

Create a configuration file src/main/webapp/META-INF/microprofile-config.properties. The config property to add the base URL is <fullyQualifiedInterfaceName>/mp-rest/url. In this case, add the config property io.openliberty.guides.inventory.client.SystemClient/mp-rest/url. Configure this property to the default http://localhost:9080/system base URL:

io.openliberty.guides.inventory.client.SystemClient/mp-rest/url=http://localhost:9080/system

This configuration is automatically picked up by the MicroProfile Config API.

Look at the annotations in the SystemClient interface again.

@Dependent
@RegisterRestClient
@RegisterProvider(UnknownUrlExceptionMapper.class)
@Path("/properties")
public interface SystemClient {

The @Dependent annotation tells the service that the scope of the interface depends on the class that it is injected into. By default, interfaces have a @Dependent scope unless another scope is defined on the interface.

The @RegisterRestClient annotation registers the interface as a RESTful client. The runtime creates a CDI managed bean for every interface that is annotated with the @RegisterRestClient annotation.

The @Dependent and @RegisterRestClient annotations imply that the interface is manageable through CDI. You must have them in order to inject the client.

Inject the SystemClient interface into the InventoryManager class, which is another CDI managed bean. Create the InventoryManager class in the src/main/java/io/openliberty/guides/inventory/InventoryManager.java file:

package io.openliberty.guides.inventory;

import java.net.URL;
import java.net.UnknownHostException;
import java.net.MalformedURLException;
import javax.ws.rs.ProcessingException;
import java.util.Properties;
import javax.inject.Inject;
import javax.enterprise.context.ApplicationScoped;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import io.openliberty.guides.inventory.model.InventoryList;
import io.openliberty.guides.inventory.client.SystemClient;
import io.openliberty.guides.inventory.client.UnknownUrlException;
import io.openliberty.guides.inventory.client.UnknownUrlExceptionMapper;

@ApplicationScoped
public class InventoryManager {

  private InventoryList invList = new InventoryList();
  private final String DEFAULT_PORT = System.getProperty("default.http.port");

  @Inject
  @RestClient
  private SystemClient defaultRestClient;

  public Properties get(String hostname) {

    Properties properties = null;
    if (hostname.equals("localhost")) {
      properties = getPropertiesWithDefaultHostName();
    } else {
      properties = getPropertiesWithGivenHostName(hostname);
    }

    if (properties != null) {
      invList.addToInventoryList(hostname, properties);
    }
    return properties;
  }

  public InventoryList list() {
    return invList;
  }

  private Properties getPropertiesWithDefaultHostName() {
    try {
      return defaultRestClient.getProperties();
    } catch (UnknownUrlException e) {
      System.err.println("The given URL is unreachable.");
    } catch (ProcessingException ex) {
      handleProcessingException(ex);
    }
    return null;
  }

  private Properties getPropertiesWithGivenHostName(String hostname) {
    String customURLString = "http://" + hostname + ":" + DEFAULT_PORT + "/system";
    URL customURL = null;
    try {
      customURL = new URL(customURLString);
      SystemClient customRestClient = RestClientBuilder.newBuilder()
                                         .baseUrl(customURL)
                                         .register(UnknownUrlExceptionMapper.class)
                                         .build(SystemClient.class);
      return customRestClient.getProperties();
    } catch (ProcessingException ex) {
      handleProcessingException(ex);
    } catch (UnknownUrlException e) {
      System.err.println("The given URL is unreachable.");
    } catch (MalformedURLException e) {
      System.err.println("The given URL is not formatted correctly.");
    }
    return null;
  }

  private void handleProcessingException(ProcessingException ex) {
    Throwable rootEx = ExceptionUtils.getRootCause(ex);
    if (rootEx != null && rootEx instanceof UnknownHostException) {
      System.err.println("The specified host is unknown.");
    } else {
      throw ex;
    }
  }

}

@Inject and @RestClient annotations inject an instance of the SystemClient called defaultRestClient to the InventoryManager class.

Since the InventoryManager class is @ApplicationScoped, and the SystemClient CDI bean maintains the same scope through the @Dependent annotation, the client is initialized once per application.

If the hostname parameter is localhost, the service runs the getPropertiesWithDefaultHostName() helper function to fetch system properties. The helper function invokes the system service by calling the defaultRestClient.getProperties() method.

Building the client with RestClientBuilder

The inventory service might also connect with a host other than the default localhost host. Then, you cannot configure a base URL that is not yet known. Therefore, set the host name as a variable and build the client by using the RestClientBuilder method. You can customize the base URL from the host name attribute.

Look at the getPropertiesWithGivenHostName() method in the src/main/java/io/openliberty/guides/inventory/InventoryManager.java file:

private Properties getPropertiesWithGivenHostName(String hostname) {
  String customURLString = "http://" + hostname + ":" + DEFAULT_PORT + "/system";
  URL customURL = null;
  try {
    customURL = new URL(customURLString);
    SystemClient customRestClient = RestClientBuilder.newBuilder()
                                       .baseUrl(customURL)
                                       .register(UnknownUrlExceptionMapper.class)
                                       .build(SystemClient.class);
    return customRestClient.getProperties();
  } catch (ProcessingException ex) {
    handleProcessingException(ex);
  } catch (UnknownUrlException e) {
    System.err.println("The given URL is unreachable.");
  } catch (MalformedURLException e) {
    System.err.println("The given URL is not formatted correctly.");
  }
  return null;
}

The host name is provided as a parameter. This method first assembles the base URL that consists of the new host name. Then, the method instantiates a RestClientBuilder builder with the new URL, registers the response exception mapper, and builds the SystemClient instance.

Similarly, call the customRestClient.getProperties() method to invoke the system service.

Building and running the application

To build the application, run the Maven install phase from the command line in the start directory:

mvn install

This command builds the application and creates a .war file in the target directory. It also configures and installs Open Liberty into the target/liberty/wlp directory.

Next, run the Maven liberty:start-server goal:

mvn liberty:start-server

This goal starts an Open Liberty server instance. Your Maven pom.xml is already configured to start the application in this server instance.

When the server is running, select either approach to fetch your system properties:

If you make changes to the code, use the Maven compile goal to rebuild the application and have the running Open Liberty server pick them up automatically. Make sure your application is using loose config for this to work:

mvn compile

To stop the Open Liberty server, run the Maven liberty:stop-server goal:

mvn liberty:stop-server

Testing the application

Create a RestClientTest test class in the src/test/java/it/io/openliberty/guides/client/RestClientTest.java file:

package it.io.openliberty.guides.client;

import static org.junit.Assert.assertEquals;
import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;
import javax.ws.rs.client.WebTarget;
import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class RestClientTest {

  private static String port;

  private Client client;

  private final String INVENTORY_SYSTEMS = "inventory/systems";

  @BeforeClass
  public static void oneTimeSetup() {
    port = System.getProperty("liberty.test.port");
  }

  @Before
  public void setup() {
    client = ClientBuilder.newClient();
    client.register(JsrJsonpProvider.class);
  }

  @After
  public void teardown() {
    client.close();
  }

  @Test
  public void testSuite() {
    this.testDefaultLocalhost();
    this.testRestClientBuilder();
  }

  public void testDefaultLocalhost() {
    String hostname = "localhost";

    String url = "http://localhost:" + port + "/" + INVENTORY_SYSTEMS + "/" + hostname;

    JsonObject obj = fetchProperties(url);

    assertEquals("The system property for the local and remote JVM should match",
                 System.getProperty("os.name"),
                 obj.getString("os.name"));
  }

  public void testRestClientBuilder() {
    String hostname = null;
    try{
      hostname = InetAddress.getLocalHost().getHostAddress();
    } catch (UnknownHostException e) {
      System.err.println("Unknown Host.");
    }

    String url = "http://localhost:" + port + "/" + INVENTORY_SYSTEMS + "/" + hostname;

    JsonObject obj = fetchProperties(url);

    assertEquals("The system property for the local and remote JVM should match",
                 System.getProperty("os.name"),
                 obj.getString("os.name"));
  }

  private JsonObject fetchProperties(String url) {
    WebTarget target = client.target(url);
    Response response = target.request().get();

    assertEquals("Incorrect response code from " + url, 200, response.getStatus());

    JsonObject obj = response.readEntity(JsonObject.class);
    response.close();
    return obj;
  }

}

Each test case tests one of the methods for instantiating a RESTful client.

The testDefaultLocalhost() test fetches and compares system properties from the http://localhost:9080/inventory/systems/localhost URL.

The testRestClientBuilder() test gets your IP address. Then, use your IP address as the host name to fetch your system properties and compare them.

In addition, a few endpoint tests are provided for you to test the basic functionality of the inventory and system services. If a test failure occurs, you might have introduced a bug into the code.

Running the tests

If the server is still running from the previous steps, stop it using the Maven liberty:stop-server goal from command line in the start directory:

mvn liberty:stop-server

Then, verify that the tests pass using the Maven verify goal:

mvn verify

It may take some time before build is complete. If the tests pass, you will see a similar output to the following:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.system.SystemEndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.377 sec - in it.io.openliberty.guides.system.SystemEndpointTest
Running it.io.openliberty.guides.inventory.InventoryEndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.379 sec - in it.io.openliberty.guides.inventory.InventoryEndpointTest
Running it.io.openliberty.guides.client.RestClientTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.121 sec - in it.io.openliberty.guides.client.RestClientTest

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

To see whether the tests detect a failure, change the base URL in the configuration file so that when the inventory service tries to access the invalid URL, an UnknownUrlException is thrown. Rerun the Maven build. You see a test failure occur.

Great work! You’re done!

You just invoked a remote service by using a template interface with MicroProfile Rest Client and Open Liberty.

Feel free to try one of the related guides. They demonstrate more technologies that you can learn and expand on what you built here.

Contribute to this guide

Is something missing or broken? Raise an issue, or send us a pull request.