Consuming RESTful services asynchronously with template interfaces

duration 15 minutes

Prerequisites:

Learn how to use MicroProfile Rest Client to invoke RESTful microservices asynchronously over HTTP.

What you’ll learn

You will learn how to build a MicroProfile Rest Client to access remote RESTful services using asynchronous method calls. You’ll update the template interface for a MicroProfile Rest Client to use the CompletionStage return type. The template interface maps to the remote service that you want to call. A CompletionStage interface allows you to work with the result of your remote service call asynchronously.

What is asynchronous programming?

Imagine asynchronous programming as a restaurant. After you’re seated, a waiter takes your order. Then, you must wait a few minutes for your food to be prepared. While your food is being prepared, your waiter may take more orders or serve other tables. After your food is ready, your waiter brings out the food to your table. However, in a synchronous model, the waiter must wait for your food to be prepared before serving any other customers. This method blocks other customers from placing orders or receiving their food.

You can perform lengthy operations, such as input/output (I/O), without blocking with asynchronous methods. The I/O operation can occur in the background and a callback notifies the caller to continue its computation when the original request is complete. As a result, the original thread frees up so it can handle other work rather than wait for the I/O to complete. Revisiting the restaurant analogy, food is prepared asynchronously in the kitchen and your waiter is freed up to attend to other tables.

In the context of REST clients, HTTP request calls can be time consuming. The network might be slow, or maybe the upstream service is overwhelmed and can’t respond quickly. These lengthy operations can block the execution of your thread when it’s in use and prevent other work from being completed.

The application in this guide consists of three microservices, system, inventory, and query. Every 15 seconds the system microservice calculates and publishes an event that contains its average system load. The inventory microservice subscribes to that information so that it can keep an updated list of all the systems and their current system loads.

Reactive Inventory System

The microservice that you will modify is the query service. It communicates with the inventory service to determine which system has the highest system load and which system has the lowest system load.

The system and inventory microservices use MicroProfile Reactive Messaging to send and receive the system load events. If you want to learn more about reactive messaging, see the Creating Reactive Java Microservices guide.

Additional prerequisites

Before you begin, Docker needs to be installed. For installation instructions, refer to the official Docker documentation. You will build and run the microservices in Docker containers. An installation of Apache Kafka is provided in another Docker container.

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-async.git
cd guide-microprofile-rest-client-async

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

The finish directory contains the finished project that you will build.

Before you begin, make sure you have all the necessary prerequisites.

Updating the template interface of a REST client to use asynchronous methods

Navigate to the start directory to begin.

The query service uses a MicroProfile Rest Client to access the inventory service. You will update the methods in the template interface for this client to be asynchronous.

Replace the InventoryClient interface.
query/src/main/java/io/openliberty/guides/query/client/InventoryClient.java

InventoryClient.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2020, 2024 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.guides.query.client;
13
14import java.util.List;
15import java.util.Properties;
16import java.util.concurrent.CompletionStage;
17
18import jakarta.ws.rs.GET;
19import jakarta.ws.rs.Path;
20import jakarta.ws.rs.PathParam;
21import jakarta.ws.rs.Produces;
22import jakarta.ws.rs.core.MediaType;
23
24import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
25
26@Path("/inventory")
27@RegisterRestClient(configKey = "InventoryClient", baseUri = "http://localhost:9085")
28public interface InventoryClient extends AutoCloseable {
29
30    @GET
31    @Path("/systems")
32    @Produces(MediaType.APPLICATION_JSON)
33    List<String> getSystems();
34
35    // tag::getSystem[]
36    @GET
37    @Path("/systems/{hostname}")
38    @Produces(MediaType.APPLICATION_JSON)
39    CompletionStage<Properties> getSystem(
40        @PathParam("hostname") String hostname);
41    // end::getSystem[]
42
43}

The changes involve the getSystem method. Change the return type to CompletionStage<Properties> to make the method asynchronous. The method now has the return type of CompletionStage<Properties> so you aren’t able to directly manipulate the Properties inner type. As you will see in the next section, you’re able to indirectly use the Properties by chaining callbacks.

Updating a REST resource to asynchronously handle HTTP requests

To reduce the processing time, you will update the /query/systemLoad endpoint to asynchronously send the requests. Multiple client requests will be sent synchronously in a loop. The asynchronous calls do not block the program so the endpoint needs to ensure that all calls are completed and all returned data is processed before proceeding.

Replace the QueryResource class.
query/src/main/java/io/openliberty/guides/query/QueryResource.java

QueryResource.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2020, 2024 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.guides.query;
 13
 14import java.math.BigDecimal;
 15import java.util.List;
 16import java.util.Map;
 17import java.util.Properties;
 18import java.util.concurrent.ConcurrentHashMap;
 19import java.util.concurrent.CountDownLatch;
 20import java.util.concurrent.TimeUnit;
 21
 22import jakarta.enterprise.context.ApplicationScoped;
 23import jakarta.inject.Inject;
 24import jakarta.ws.rs.GET;
 25import jakarta.ws.rs.Path;
 26import jakarta.ws.rs.Produces;
 27import jakarta.ws.rs.core.MediaType;
 28
 29import org.eclipse.microprofile.rest.client.inject.RestClient;
 30
 31import io.openliberty.guides.query.client.InventoryClient;
 32
 33@ApplicationScoped
 34@Path("/query")
 35public class QueryResource {
 36
 37    @Inject
 38    @RestClient
 39    private InventoryClient inventoryClient;
 40
 41    // tag::systemLoad[]
 42    @GET
 43    @Path("/systemLoad")
 44    @Produces(MediaType.APPLICATION_JSON)
 45    public Map<String, Properties> systemLoad() {
 46        // tag::getSystems[]
 47        List<String> systems = inventoryClient.getSystems();
 48        // end::getSystems[]
 49        // tag::countdown1[]
 50        CountDownLatch remainingSystems = new CountDownLatch(systems.size());
 51        // end::countdown1[]
 52        final Holder systemLoads = new Holder();
 53
 54        for (String system : systems) {
 55            // tag::getSystem[]
 56            inventoryClient.getSystem(system)
 57            // end::getSystem[]
 58                           // tag::thenAcceptAsync[]
 59                           .thenAcceptAsync(p -> {
 60                                if (p != null) {
 61                                    systemLoads.updateValues(p);
 62                                }
 63                                // tag::countdown2[]
 64                                remainingSystems.countDown();
 65                                // end::countdown2[]
 66                           })
 67                           // end::thenAcceptAsync[]
 68                           // tag::exceptionally[]
 69                           .exceptionally(ex -> {
 70                                ex.printStackTrace();
 71                                // tag::countdown3[]
 72                                remainingSystems.countDown();
 73                                // end::countdown3[]
 74                                return null;
 75                           });
 76                           // end::exceptionally[]
 77        }
 78
 79        // Wait for all remaining systems to be checked
 80        try {
 81            // tag::await[]
 82            remainingSystems.await(30, TimeUnit.SECONDS);
 83            // end::await[]
 84        } catch (InterruptedException e) {
 85            e.printStackTrace();
 86        }
 87
 88        return systemLoads.getValues();
 89    }
 90    // end::systemLoad[]
 91
 92    // tag::holder[]
 93    private class Holder {
 94        // tag::volatile[]
 95        private volatile Map<String, Properties> values;
 96        // end::volatile[]
 97
 98        Holder() {
 99            // tag::concurrentHashMap[]
100            this.values = new ConcurrentHashMap<String, Properties>();
101            // end::concurrentHashMap[]
102            init();
103        }
104
105        public Map<String, Properties> getValues() {
106            return this.values;
107        }
108
109        public void updateValues(Properties p) {
110            final BigDecimal load = (BigDecimal) p.get("systemLoad");
111
112            this.values.computeIfPresent("lowest", (key, curr_val) -> {
113                BigDecimal lowest = (BigDecimal) curr_val.get("systemLoad");
114                return load.compareTo(lowest) < 0 ? p : curr_val;
115            });
116            this.values.computeIfPresent("highest", (key, curr_val) -> {
117                BigDecimal highest = (BigDecimal) curr_val.get("systemLoad");
118                return load.compareTo(highest) > 0 ? p : curr_val;
119            });
120        }
121
122        private void init() {
123            // Initialize highest and lowest values
124            this.values.put("highest", new Properties());
125            this.values.put("lowest", new Properties());
126            this.values.get("highest").put("hostname", "temp_max");
127            this.values.get("lowest").put("hostname", "temp_min");
128            this.values.get("highest").put(
129                "systemLoad", new BigDecimal(Double.MIN_VALUE));
130            this.values.get("lowest").put(
131                "systemLoad", new BigDecimal(Double.MAX_VALUE));
132        }
133    }
134    // end::holder[]
135}

First, the systemLoad endpoint first gets all the hostnames by calling getSystems(). In the getSystem() method, multiple requests are sent asynchronously to the inventory service for each hostname. When the requests return, the thenAcceptAsync() method processes the returned data with the CompletionStage<Properties> interface.

The CompletionStage<Properties> interface represents a unit of computation. After a computation is complete, it can either be finished or it can be chained with more CompletionStage<Properties> interfaces using the thenAcceptAsync() method. Exceptions are handled in a callback that is provided to the exceptionally() method, which behaves like a catch block. When you return a CompletionStage<Properties> type in the resource, it doesn’t necessarily mean that the computation completed and the response was built. JAX-RS responds to the caller after the computation completes.

In the systemLoad() method a CountDownLatch object is used to track asynchronous requests. The countDown() method is called whenever a request is complete. When the CountDownLatch is at zero, it indicates that all asynchronous requests are complete. By using the await() method of the CountDownLatch, the program waits for all the asynchronous requests to be complete. When all asynchronous requests are complete, the program resumes execution with all required data processed.

A Holder class is used to wrap a variable called values that has the volatile keyword. The values variable is instantiated as a ConcurrentHashMap object. Together, the volatile keyword and ConcurrentHashMap type allow the Holder class to store system information and safely access it asynchronously from multiple threads.

Building and running the application

You will build and run the system, inventory, and query microservices in Docker containers. You can learn more about containerizing microservices with Docker in the Containerizing microservices guide.

Start your Docker environment. Dockerfiles are provided for you to use.

To build the application, run the Maven install and package goals from the command-line session in the start directory:

mvn -pl models install
mvn package

Run the following commands to containerize the microservices:

docker build -t system:1.0-SNAPSHOT system/.
docker build -t inventory:1.0-SNAPSHOT inventory/.
docker build -t query:1.0-SNAPSHOT query/.

Next, use the provided startContainers script to start the application in Docker containers. The script creates containers for Kafka and all of the microservices in the project, in addition to a network for the containers to communicate with each other. The script also creates three instances of the system microservice.

.\scripts\startContainers.bat
./scripts/startContainers.sh

The services take some time to become available. Visit the http://localhost:9085/health URL to confirm that the inventory microservice is up and running.

You can access the application by making requests to the query/systemLoad endpoint by going to the http://localhost:9080/query/systemLoad URL.

When the service is ready, you see an output similar to the following example which was formatted for readability.

{
    "highest": {
        "hostname" : "8841bd7d6fcd",
        "systemLoad" : 6.96
    },
    "lowest": {
        "hostname" : "37140ec44c9b",
        "systemLoad" : 6.4
    }
}

Switching to an asynchronous programming model freed up the thread that handles requests to the inventory service. While requests process, the thread can handle other work or requests. In the /query/systemLoad endpoint, multiple systems are read and compared at once.

When you are done checking out the application, run the following script to stop the application:

.\scripts\stopContainers.bat
./scripts/stopContainers.sh

Testing the query microservice

You will create an endpoint test to test the basic functionality of the query microservice. If a test failure occurs, then you might have introduced a bug into the code.

Create the QueryServiceIT class.
query/src/test/java/it/io/openliberty/guides/query/QueryServiceIT.java

The testLoads() test case verifies that the query service can calculate the highest and lowest system loads.

QueryServiceIT.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2020, 2024 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package it.io.openliberty.guides.query;
 13
 14import java.nio.file.Paths;
 15import java.time.Duration;
 16import java.util.Map;
 17import java.util.Properties;
 18
 19import org.slf4j.Logger;
 20import org.slf4j.LoggerFactory;
 21import org.junit.jupiter.api.Test;
 22import org.junit.jupiter.api.AfterAll;
 23import org.junit.jupiter.api.BeforeAll;
 24import org.junit.jupiter.api.BeforeEach;
 25import org.mockserver.client.MockServerClient;
 26import org.mockserver.model.HttpRequest;
 27import org.mockserver.model.HttpResponse;
 28
 29import org.testcontainers.containers.Network;
 30import org.testcontainers.containers.KafkaContainer;
 31import org.testcontainers.containers.GenericContainer;
 32import org.testcontainers.containers.MockServerContainer;
 33import org.testcontainers.containers.output.Slf4jLogConsumer;
 34import org.testcontainers.containers.wait.strategy.Wait;
 35import org.testcontainers.images.builder.ImageFromDockerfile;
 36import org.testcontainers.utility.DockerImageName;
 37import org.jboss.resteasy.client.jaxrs.ResteasyClient;
 38import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
 39import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
 40import static org.junit.jupiter.api.Assertions.assertEquals;
 41
 42import jakarta.ws.rs.client.ClientBuilder;
 43import jakarta.ws.rs.core.UriBuilder;
 44
 45public class QueryServiceIT {
 46
 47    private static Logger logger = LoggerFactory.getLogger(QueryServiceIT.class);
 48
 49    public static QueryResourceClient client;
 50
 51    private static Network network = Network.newNetwork();
 52
 53    private static String testHost1 =
 54        "{"
 55            + "\"hostname\" : \"testHost1\","
 56            + "\"systemLoad\" : 1.23"
 57        + "}";
 58    private static String testHost2 =
 59        "{"
 60            + "\"hostname\" : \"testHost2\","
 61            + "\"systemLoad\" : 3.21"
 62        + "}";
 63    private static String testHost3 =
 64        "{"
 65            + "\"hostname\" : \"testHost3\","
 66            + "\"systemLoad\" : 2.13"
 67        + "}";
 68
 69    private static ImageFromDockerfile queryImage =
 70        new ImageFromDockerfile("query:1.0-SNAPSHOT")
 71            .withDockerfile(Paths.get("./Dockerfile"));
 72
 73    public static final DockerImageName MOCKSERVER_IMAGE = DockerImageName
 74        .parse("mockserver/mockserver")
 75        .withTag("mockserver-"
 76                 + MockServerClient.class.getPackage().getImplementationVersion());
 77
 78    public static MockServerContainer mockServer =
 79        new MockServerContainer(MOCKSERVER_IMAGE)
 80            .withNetworkAliases("mock-server")
 81            .withNetwork(network);
 82
 83    public static MockServerClient mockClient;
 84
 85    private static KafkaContainer kafkaContainer =
 86        new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest"))
 87            .withListener(() -> "kafka:19092")
 88            .withNetwork(network);
 89
 90    private static GenericContainer<?> queryContainer =
 91        new GenericContainer(queryImage)
 92            .withNetwork(network)
 93            .withExposedPorts(9080)
 94            .waitingFor(Wait.forHttp("/health/ready"))
 95            .withStartupTimeout(Duration.ofMinutes(3))
 96            .withLogConsumer(new Slf4jLogConsumer(logger))
 97            .dependsOn(kafkaContainer);
 98
 99    private static QueryResourceClient createRestClient(String urlPath) {
100        ClientBuilder builder = ResteasyClientBuilder.newBuilder();
101        ResteasyClient client = (ResteasyClient) builder.build();
102        ResteasyWebTarget target = client.target(UriBuilder.fromPath(urlPath));
103        return target.proxy(QueryResourceClient.class);
104    }
105
106    @BeforeAll
107    public static void startContainers() {
108        mockServer.start();
109        mockClient = new MockServerClient(
110            mockServer.getHost(),
111            mockServer.getServerPort());
112
113        kafkaContainer.start();
114
115        queryContainer.withEnv(
116            "InventoryClient/mp-rest/uri",
117            "http://mock-server:" + MockServerContainer.PORT);
118        queryContainer.start();
119
120        client = createRestClient("http://"
121            + queryContainer.getHost()
122            + ":" + queryContainer.getFirstMappedPort());
123    }
124    @BeforeEach
125    public void setup() throws InterruptedException {
126        mockClient.when(HttpRequest.request()
127                        .withMethod("GET")
128                        .withPath("/inventory/systems"))
129                    .respond(HttpResponse.response()
130                        .withStatusCode(200)
131                        .withBody("[\"testHost1\","
132                                + "\"testHost2\","
133                                + "\"testHost3\"]")
134                        .withHeader("Content-Type", "application/json"));
135
136        mockClient.when(HttpRequest.request()
137                        .withMethod("GET")
138                        .withPath("/inventory/systems/testHost1"))
139                    .respond(HttpResponse.response()
140                        .withStatusCode(200)
141                        .withBody(testHost1)
142                        .withHeader("Content-Type", "application/json"));
143
144        mockClient.when(HttpRequest.request()
145                        .withMethod("GET")
146                        .withPath("/inventory/systems/testHost2"))
147                    .respond(HttpResponse.response()
148                        .withStatusCode(200)
149                        .withBody(testHost2)
150                        .withHeader("Content-Type", "application/json"));
151
152        mockClient.when(HttpRequest.request()
153                        .withMethod("GET")
154                        .withPath("/inventory/systems/testHost3"))
155                    .respond(HttpResponse.response()
156                        .withStatusCode(200)
157                        .withBody(testHost3)
158                        .withHeader("Content-Type", "application/json"));
159    }
160
161    @AfterAll
162    public static void stopContainers() {
163        queryContainer.stop();
164        kafkaContainer.stop();
165        mockServer.stop();
166        network.close();
167    }
168
169    // tag::testLoads[]
170    @Test
171    public void testLoads() {
172        Map<String, Properties> response = client.systemLoad();
173
174        assertEquals(
175            "testHost2",
176            response.get("highest").get("hostname"),
177            "Returned highest system load incorrect"
178        );
179        assertEquals(
180            "testHost1",
181            response.get("lowest").get("hostname"),
182            "Returned lowest system load incorrect"
183        );
184    }
185    // end::testLoads[]
186}

Running the tests

Navigate to the query directory, then verify that the tests pass by using the Maven verify goal:

mvn verify
export TESTCONTAINERS_RYUK_DISABLED=true
mvn verify

For more information about disabling Ryuk, see the Testcontainers custom configuration document.

When the tests succeed, you see output similar to the following example:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.query.QueryServiceIT
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 32.123 s - in it.io.openliberty.guides.query.QueryServiceIT

Results:

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

Great work! You’re done!

You have just modified an application to make asynchronous HTTP requests using Open Liberty and MicroProfile Rest Client.

Guide Attribution

Consuming RESTful services asynchronously with template interfaces by Open Liberty is licensed under CC BY-ND 4.0

Copy file contents
Copied to clipboard

Prerequisites:

Nice work! Where to next?

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.

Star