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, which maps to the remote service that you want to call, to use the CompletionStage return type. 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 that you’ll be working with is a job manager that maintains an inventory of available systems. It consists of four microservices, gateway, job, system, and inventory. The job microservice allows you to dispatch jobs that will be run by the system microservice. A job is a sleep operation that is used to represent a slow task that lasts for a duration of 5 to 10 seconds. When a job completes, the system microservice reports the sleep time that results from the job. In addition to running jobs, the system microservice also registers itself at startup with the inventory microservice, which keeps track of all instances of the system microservice. Finally, the gateway microservice is a backend for frontend service. It communicates with the backend job and inventory microservices on the caller’s behalf.

Reactive Inventory System

The microservice that you will modify is the gateway service. It acts as a gateway to communicate with the backend microservices. Whenever a request is made to the gateway service to retrieve the jobs, the gateway service communicates with the job service on that host to get the completed jobs.

The implementations of the application and its services are provided for you in the start/src directory. The application also uses the Apache Kafka and ZooKeeper services to distribute the job results and system status.

If you want to learn more about MicroProfile Rest Client, you can read the Consuming RESTful services with template interfaces guide.

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.

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

Navigate to the start directory to begin.

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

Replace the JobClient interface.
gateway/src/main/java/io/openliberty/guides/gateway/client/JobClient.java

JobClient.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2019 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.guides.gateway.client;
14
15import java.util.concurrent.CompletionStage;
16
17import javax.ws.rs.GET;
18import javax.ws.rs.POST;
19import javax.ws.rs.Path;
20import javax.ws.rs.PathParam;
21import javax.ws.rs.Produces;
22import javax.ws.rs.core.MediaType;
23
24import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
25
26import io.openliberty.guides.models.Job;
27import io.openliberty.guides.models.JobResult;
28import io.openliberty.guides.models.Jobs;
29
30@RegisterRestClient(baseUri = "http://job-service:9080")
31@Path("/jobs")
32public interface JobClient {
33
34    // tag::getJobs[]
35    @GET
36    @Produces(MediaType.APPLICATION_JSON)
37    public CompletionStage<Jobs> getJobs();
38    // end::getJobs[]
39
40    // tag::getJob[]
41    @GET
42    @Path("{jobId}")
43    @Produces(MediaType.APPLICATION_JSON)
44    public CompletionStage<JobResult> getJob(@PathParam("jobId") String jobId);
45    // end::getJob[]
46
47    // tag::createJob[]
48    @POST
49    @Produces(MediaType.APPLICATION_JSON)
50    public CompletionStage<Job> createJob();
51    // end::createJob[]
52
53}

The changes involve changing the return types of the getJobs, getJob, and createJob methods to return CompletionStage<T> types. These changes make the methods asynchronous. Since the methods now have return type of CompletionStage<T>, you aren’t able to directly manipulate the inner types. As you will see in the next section, you will be able to indirectly use the inner object by chaining callbacks.

Updating a REST resource to asynchronously handle HTTP requests

JAX-RS resources can also have asynchronous methods. So instead of returning a JobsList model type, you can return a CompletionStage<JobsList> type. Completion stages can be chained together by using the thenApplyAsync() method.

Replace the GatewayJobResource class.
gateway/src/main/java/io/openliberty/guides/gateway/GatewayJobResource.java

GatewayJobResource.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2019 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 v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.guides.gateway;
14
15import java.util.concurrent.CompletionStage;
16
17import javax.inject.Inject;
18import javax.ws.rs.GET;
19import javax.ws.rs.POST;
20import javax.ws.rs.Path;
21import javax.ws.rs.PathParam;
22import javax.ws.rs.Produces;
23import javax.ws.rs.core.MediaType;
24
25import org.eclipse.microprofile.rest.client.inject.RestClient;
26
27import io.openliberty.guides.gateway.client.JobClient;
28import io.openliberty.guides.models.JobList;
29import io.openliberty.guides.models.Job;
30import io.openliberty.guides.models.JobResult;
31
32@Path("/jobs")
33public class GatewayJobResource {
34
35    @Inject
36    @RestClient
37    private JobClient jobClient;
38
39    @GET
40    @Produces(MediaType.APPLICATION_JSON)
41    public CompletionStage<JobList> getJobs() {
42        return jobClient
43            .getJobs()
44            // tag::thenApplyAsync[]
45            .thenApplyAsync((jobs) -> {
46                return new JobList(jobs.getResults());
47            })
48            // end::thenApplyAsync[]
49            // tag::exceptionally[]
50            .exceptionally((ex) -> {
51                // Respond with empty list on error
52                return new JobList();
53            });
54            // end::exceptionally[]
55    }
56
57    @GET
58    @Path("{jobId}")
59    @Produces(MediaType.APPLICATION_JSON)
60    public CompletionStage<JobResult> getJob(@PathParam("jobId") String jobId) {
61        return jobClient.getJob(jobId);
62    }
63
64    @POST
65    @Produces(MediaType.APPLICATION_JSON)
66    public CompletionStage<Job> createJob() {
67        return jobClient.createJob();
68    }
69}

Similar to the synchronous approach, if completed jobs are successfully obtained from the job microservice, the resource responds with an HTTP status of 200 and the body will contain a list of jobs. Finally, return the CompletionStage<JobsList> result you built by using the thenApplyAsync() method.

The CompletionStage interface represents a unit of computation. After that computation completes, it can either be finished or chained with more completion stages by using the thenApplyAsync() method to perform more computations. Exceptions can be handled in a callback provided to the exceptionally() method, which behaves similar to a catch block. When you return a CompletionStage type in the resource, it doesn’t necessarily mean that the computation completed and the response was built. JAX-RS will respond to the caller after the CompletionStage completes.

Building and running the application

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

Install Docker by following the instructions on the official Docker documentation. Start your Docker environment.

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

mvn clean install

Run the following commands to build and containerize the application:

docker build -t system:1.0-SNAPSHOT system/.
docker build -t inventory:1.0-SNAPSHOT inventory/.
docker build -t job:1.0-SNAPSHOT job/.
docker build -t gateway:1.0-SNAPSHOT gateway/.

Next, use the provided script to start the application in Docker containers. The script creates a network for the containers to communicate with each other. It also creates containers for Kafka, Zookeeper, and all of the microservices in the project.

./scripts/start-app
.\scripts\start-app.bat

The services take some time to become available. You can access the application by making requests to the gateway job endpoints:

DescriptionEndpointSample Output

Get completed jobs

GET http://localhost:8080/api/jobs

{"count":0,"results":[]}

Create a job

POST http://localhost:8080/api/jobs

{"jobId":"661891cb-ad36-4ef4-9bb3-641f973f2964"}

Get a specific job

GET http://localhost:8080/api/jobs/{jobId}

{"jobId":"661891cb-ad36-4ef4-9bb3-641f973f2964","result":5}

To create a job, you can use curl -X POST http://localhost:8080/api/jobs command if available on the system. The Postman application can also be used. The request take some time for the job results to return.

The completed jobs JSON output with a created job looks like {"averageResult":5.0,"count":1,"results":[{"jobId":"661891cb-ad36-4ef4-9bb3-641f973f2964","result":5}]}. The averageResult attribute is the average sleep time of all the jobs. The count attribute is the number of jobs, and the results attribute contains the list of the jobs. The JSON output for each job has a job ID and a sleep time as the result for the job.

If no jobs are created, the JSON output will be {"count":0,"results":[]}. The count attribute is 0 and the results attribute is empty.

Switching to an asynchronous programming model freed up the thread that was handling your request to /api/jobs. While the request is processed, the thread can handle other work.

Testing the gateway application

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

Create the GatewayJobEndpointIT class.
gateway/src/test/java/it/io/openliberty/guides/gateway/GatewayJobEndpointIT.java

The following descriptions explain what the test cases verify:

  • The testCreateJob() test case verifies that the gateway service communicates with a backend service to create a job.

  • The testGetJobs() test case verifies that the gateway service communicates with an upstream service to get all jobs and transforms them to a JobList model type.

GatewayJobEndpointIT.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2019 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 v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - Initial implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13package it.io.openliberty.guides.gateway;
 14
 15import static org.junit.Assert.assertEquals;
 16
 17import javax.json.JsonObject;
 18import javax.net.ssl.HostnameVerifier;
 19import javax.net.ssl.SSLSession;
 20import javax.ws.rs.client.Client;
 21import javax.ws.rs.client.ClientBuilder;
 22import javax.ws.rs.core.Response;
 23
 24import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
 25import org.junit.After;
 26import org.junit.Before;
 27import org.junit.Rule;
 28import org.junit.Test;
 29import org.mockserver.client.MockServerClient;
 30import org.mockserver.junit.MockServerRule;
 31import org.mockserver.model.HttpRequest;
 32import org.mockserver.model.HttpResponse;
 33
 34public class GatewayJobEndpointIT {
 35
 36    private final String BASE_URL = "http://localhost:9080/api/jobs";
 37
 38    private Client client;
 39    private Response response;
 40
 41    @Rule
 42    public MockServerRule mockServerRule = new MockServerRule(this, 9082);
 43
 44    private MockServerClient mockServerClient = mockServerRule.getClient();
 45
 46    @Before
 47    public void setup() throws InterruptedException {
 48        response = null;
 49        client = ClientBuilder.newBuilder()
 50                    .hostnameVerifier(new HostnameVerifier() {
 51                        public boolean verify(String hostname, SSLSession session) {
 52                            return true;
 53                        }
 54                    })
 55                    .register(JsrJsonpProvider.class)
 56                    .build();
 57
 58        mockServerClient
 59                    .when(HttpRequest.request()
 60                        .withMethod("GET")
 61                        .withPath("/jobs"))
 62                    .respond(HttpResponse.response()
 63                        .withStatusCode(200)
 64                        .withBody("{ \"results\": [ { \"jobId\": \"my-job-1\", \"result\": 7 }, { \"jobId\": \"my-job-2\", \"result\": 5 } ] } ")
 65                        .withHeader("Content-Type", "application/json"));
 66
 67        mockServerClient
 68                    .when(HttpRequest.request()
 69                        .withMethod("POST")
 70                        .withPath("/jobs"))
 71                    .respond(HttpResponse.response()
 72                        .withStatusCode(200)
 73                        .withBody("{ \"jobId\": \"my-job-id\" }")
 74                        .withHeader("Content-Type", "application/json"));
 75    }
 76
 77    @After
 78    public void teardown() {
 79        client.close();
 80    }
 81
 82    // tag::testCreateJob[]
 83    @Test
 84    public void testCreateJob() throws InterruptedException {
 85        this.response = client
 86            .target(BASE_URL)
 87            .request()
 88            .post(null);
 89
 90        assertEquals(200, response.getStatus());
 91
 92        JsonObject obj = response.readEntity(JsonObject.class);
 93        String jobId = obj.getString("jobId");
 94        assertEquals("my-job-id", jobId);
 95    }
 96    // end::testCreateJob[]
 97
 98    // tag::testGetJobs[]
 99    @Test
100    public void testGetJobs() {
101        this.response = client
102            .target(BASE_URL)
103            .request()
104            .get();
105
106        assertEquals(200, response.getStatus());
107
108        JsonObject obj = response.readEntity(JsonObject.class);
109        assertEquals(2, obj.getInt("count"));
110        assertEquals(6.0, obj.getJsonNumber("averageResult").doubleValue(), 0.01);
111        assertEquals(2, obj.getJsonArray("results").size());
112    }
113    // end::testGetJobs[]
114
115}

Running the tests

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

mvn verify

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

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.gateway.GatewayInventoryEndpointIT
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.88 s - in it.io.openliberty.guides.gateway.GatewayInventoryEndpointIT
Running it.io.openliberty.guides.gateway.GatewayJobEndpointIT
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.455 s - in it.io.openliberty.guides.gateway.GatewayJobEndpointIT

Results:

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

Tearing down the environment

Navigate back to the start directory.

Finally, use the following script to stop the application:

./scripts/stop-app
.\scripts\stop-app.bat

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

Copied to clipboard
Copy code block
Copy file contents

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