Chapter 6. Consuming microservices

This chapter covers

  • How to consume a microservice
  • Your choices when consuming a microservice

Consuming a microservice can mean many things to many people. Clients of a microservice could be scripts, web pages, other microservices, or pretty much anything that can make HTTP requests. If we covered them all, this chapter would be a whole book by itself!

Developing a microservice is interesting, but it doesn’t get you far until you introduce many microservices interacting with each other. To enable two services to interact with each other, you need a method by which one service can call another.

This chapter provides examples focused on one microservice consuming another with Java-based libraries, but the methods shown can be equally applied to any generic Java client consuming a microservice.

With Enterprise Java, two services would interact with a direct service call, as shown in figure 6.1.

Figure 6.1. Enterprise Java business service calls

The service call could be accomplished by the following:

  • @EJB injection when using EJBs
  • @Inject with CDI
  • Retrieving an instance of a service via a static method or variable
  • Spring dependency injection, either XML or annotation based

All these options require that your two services reside in the same JVM and runtime, as shown in figure 6.1.

In the figure one microservice is calling another microservice. In the diagram, they’re within the same microservices environment, but they don’t have to be. Revisiting figure 1.4, figure 6.2 highlights the focus of this chapter, solving the means by which two microservices in separate runtimes are able to communicate.

Figure 6.2. Consuming microservices

In our particular case, you’ll be taking the new Cayambe administration microservice from chapter 2 and developing clients for it with different libraries. Figure 6.3 illustrates where the microservice client fits; you use a short-term way of retrieving category data until such time as the final solution is in place.

Figure 6.3. Cayambe administration microservice client

You’ll start with a look at consuming a microservice by using low-level libraries that deal directly with HTTP requests. Because they deal with HTTP requests, they can be used with microservices that don’t expose RESTful endpoints. Then you’ll learn about client libraries that are specifically designed to simplify the code required to call RESTful endpoints. They offer a higher level of abstraction over HTTP requests, which simplifies the client code significantly, as you’ll see in our examples. The code for this section can be found in the /chapter6 directory of the book’s example code. For each client library, a service will be implemented that calls the CategoryResource RESTful endpoint, which you created in chapter 2, and then returns the received data as the response to the caller.

Tip

You can set the port that the CategoryResource starts on to prevent port clashes with the other microservices. You set the swarm.port.offset property in the Maven plugin to 1.

Each of these services needs an object to represent the category JSON that you’ll receive from the administration service. To facilitate that, each client library Maven module will have its own Category object, which will be used when deserializing the response from the administration service.

Listing 6.1. Category model class
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")                                                        1
public class Category {

    protected Integer id;

    protected String name;

    protected String header;

    protected Boolean visible;

    protected String imagePath;

    protected Category parent;

    private Collection<Category> children = new HashSet<>();               2

    protected LocalDateTime created = LocalDateTime.now();

    protected LocalDateTime updated;

    protected Integer version;

    ...
}

  • 1 Defines the key as the ID for Category, which is used for deserializing the collection of children received as JSON
  • 2 Initializes the collection of children to ensure that you always have a valid collection, even if it’s empty

The getter and setter methods are omitted for brevity, but the full source of Category is available in the book’s source code.

In addition, each of these services needs access to ExecutorService to submit work for processing on a new thread. You want to use one provided by Java EE so the services all retrieve one the same way:

private ManagedExecutorService executorService() throws Exception {
    InitialContext ctx = new InitialContext();
    return (ManagedExecutorService)
      ctx.lookup("java:jboss/ee/concurrency/executor/default");
}

This does a simple JNDI lookup of the service by name and returns the instance you can use for submitting work.

Note

The ExecutorService is defined for you by WildFly. You don’t need to do anything to make it available in order to retrieve it from JNDI.

Your services could just as easily have created a new Thread directly to perform any required work, but then your new thread would be outside the Java EE thread pool management. Is this a problem? Not always, but you could have problems if the thread pool size of the runtime is almost as large as the available JVM threads. In that case, you could exhaust all available JVM threads when creating threads outside the Java EE thread pool. As a general rule, it’s best not to create threads directly, but instead use the ExecutorService.

Because you want to show how synchronous and asynchronous usage scenarios result in different client code for consuming a microservice, each resource that uses a client library will contain two endpoints:

  • /sync—Synchronously processes a request from the caller
  • /async—Asynchronously processes a request from the caller

Traditionally, services were developed to communicate synchronously with any other resources required to complete a response. With increasing demands from the enterprise to deliver greater performance and scalability, we’ve moved toward greater asynchronous behavior in our services. In this chapter and the remainder of the book, you’ll learn about both synchronous and asynchronous usage patterns. Enhancing the benefits of microservices also requires some level of asynchronous behavior; otherwise, you minimize the benefits of their distributed nature. If you take that route, you may as well stick with a monolith!

Note

Each of your microservices defines a field called categoryUrl, which is hardcoded to http://localhost:8081/admin/categorytree. This isn’t what you’d go into production doing, but it serves our purpose to simplify the examples. In a later chapter, you’ll see how service discovery can be used for connecting to other services.

6.1. Consuming a microservice with a Java client library

In this section, you’ll see examples of consuming a microservice that uses lower-level libraries to deal with HTTP requests directly. Though that results in more verbose and extra handling of data, it does provide the greatest flexibility as to how a call can be made. For instance, using these libraries may be a better choice if a microservice needs to communicate with many types of HTTP resources, because it doesn’t make sense to add another library just for RESTful endpoint interaction.

6.1.1. java.net

The classes in the java.net package have been part of the JDK from the beginning. Though they’ve been enhanced and updated over the years, they focus on low-level HTTP interactions. They’re in no way designed for RESTful endpoint consumption, so some level of cumbersome code is required.

Let’s take a look at our first method for the DisplayResource.

Listing 6.2. DisplayResource with java.net
@GET
@Path("/sync")
@Produces(MediaType.APPLICATION_JSON)
public Category getCategoryTreeSync() throws Exception {
    HttpURLConnection connection = null;

    try {
        URL url = new URL(this.categoryUrl);                                1
        connection = (HttpURLConnection) url.openConnection();

        connection.setRequestMethod("GET");                                 2
        connection.setRequestProperty("Accept", MediaType.APPLICATION_JSON);3

        if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {    4
            throw new RuntimeException("Request Failed: HTTP Error code: " +
connection.getResponseCode());
        }

        return new ObjectMapper()                                           5
                .registerModule(new JavaTimeModule())                       6
                .readValue(connection.getInputStream(), Category.class);    7

    } finally {
        assert connection != null;
        connection.disconnect()                                             8
    }
}

  • 1 Create a URL pointing to your CategoryResource.
  • 2 Set HTTP GET as the request method for your connection.
  • 3 Set “application/json” as the media type that you’ll accept in a response.
  • 4 Check for a non-OK response code.
  • 5 Create a new ObjectMapper to perform the JSON deserialization.
  • 6 Register the JavaTimeModule to handle conversion of JSON to LocalDateTime instances.
  • 7 Pass the InputStream you received in the response to the ObjectMapper for deserialization, into an instance of Category.
  • 8 Close the connection to CategoryResource.

Though you’re dealing with a simple RESTful endpoint, the client code to communicate with it certainly isn’t, and this is only synchronous!

The next listing shows how the preceding code changes if you want to handle the client request to your microservice asynchronously.

Listing 6.3. DisplayResource with java.net asynchronously
@GET
@Path("/async")
@Produces(MediaType.APPLICATION_JSON)
public void getCategoryTreeAsync(
    @Suspended final AsyncResponse asyncResponse)                 1
    throws Exception {

    executorService().execute(() -> {                             2
        HttpURLConnection connection = null;

        try {
            // The code to open the connection, check the status code,
            // and process the response is identical to the synchronous
            // example and has been removed.

            asyncResponse.resume(category);                       3
        } catch (IOException e) {
            asyncResponse.resume(e);                              4
        } finally {
            assert connection != null;
            connection.disconnect();
        }
    });
}

  • 1 Request processing will be handled asynchronously.
  • 2 Pass the lambda expression to an executor for processing.
  • 3 Resume the AsyncResponse with the deserialized Category instance.
  • 4 Resume the AsyncResponse with an exception.

The listing introduced concepts you haven’t seen before—namely, @Suspended and AsyncResponse. These two pieces are the core of how JAX-RS handles client requests asynchronously. @Suspended informs the JAX-RS runtime that the HTTP request from the client should be suspended until a response is ready. AsyncResponse indicates how the developer informs the runtime that a response is ready or has failed to complete.

What does that look like? Take a look at figure 6.4.

Here’s what is happening at each step within figure 6.4:

  1. An HTTP request arrives from a browser or another client.
  2. getCategoryTreeAsync() triggers code to be executed in a separate thread. On completion of getCategoryTreeAsync(), the client request is suspended and the HTTP request thread that was handling it is made available to handle additional requests.
  3. An HTTP request is made to an external microservice.
    Figure 6.4. JAX-RS AsyncResponse handling

  4. An HTTP response is received from the external microservice.
  5. The response data is passed to asyncResponse.resume().
  6. The client request is reactivated in the HTTP request thread and a response is constructed.
  7. The response is returned to the browser, or to whatever client made the request.
Warning

Using @Suspended in a RESTful endpoint doesn’t prevent the client that’s calling the endpoint from blocking. It benefits only the server side of the request by allowing greater request throughput. Without the use of @Suspended, a JAX-RS resource can handle only as many requests as there are available threads, because each request blocks the thread until the method completes.

Now that you have your services built, you can start them.

Change into the /chapter6/admin directory of the book’s example code and run this:

mvn thorntail:run

CategoryResource will be started and available at http://localhost:8081/admin/categorytree in a browser.

Now you start your DisplayResource. Change into the /chapter6/java-net directory and run this:

mvn thorntail:run

It’s now possible to access the microservices by accessing them in a browser: http://localhost:8080/sync and http://localhost:8080/async. Either of the preceding URLs opened in the browser will show the tree of categories that are currently present within the administration microservice.

6.1.2. Apache HttpClient

With Apache HttpClient, you get an abstraction over classes you used with java.net, minimizing the code required for interacting with the underlying HTTP connection. The code in DisplayResource isn’t vastly different from your previous code, but it does improve the code’s readability.

For instance, let’s look at your first method for DisplayResource.

Listing 6.4. DisplayResource with HttpClient
@GET
@Path("/sync")
@Produces(MediaType.APPLICATION_JSON)
public Category getCategoryTreeSync() throws Exception {
    try (CloseableHttpClient httpclient = HttpClients.createDefault()) {  1

        HttpGet get = new HttpGet(this.categoryUrl);                      2
        get.addHeader("Accept", MediaType.APPLICATION_JSON);              3

        return httpclient.execute(get, response -> {                      4
            int status = response.getStatusLine().getStatusCode();
            if (status >= 200 && status < 300) {                          5
                return new ObjectMapper()                                 6
                        .registerModule(new JavaTimeModule())
                        .readValue(response.getEntity().getContent(),
Category.class);
            } else {
                throw new ClientProtocolException("Unexpected response
status: " + status);
            }
        });
    }
}

  • 1 Create an HTTP client inside the try-with-resources statement.
  • 2 Create an HttpGet instance with the CategoryResource URL endpoint.
  • 3 Specify that you’ll accept JSON responses.
  • 4 Execute HttpGet, passing a handler for the Response.
  • 5 Verify that the response code is OK.
  • 6 Extract HttpEntity from Response. Convert the entity to a Category instance using an ObjectMapper.

Even with this short example, you can see how much simpler your client code is when making an HTTP request. Now let’s see how much simpler your code becomes when you use @Suspended.

Listing 6.5. DisplayResource with HttpClient and @Suspended
@GET
@Path("/async")
@Produces(MediaType.APPLICATION_JSON)
public void getCategoryTreeAsync(@Suspended final AsyncResponse
asyncResponse) throws Exception {
    executorService().execute(() -> {                                       1
        try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
            HttpGet get = new HttpGet(this.categoryUrl);

            // The code to initiate the HTTP GET request and convert the
HttpEntity
            // is identical to the synchronous example and has been removed.

            asyncResponse.resume(category);                                 2
        } catch (IOException e) {
            asyncResponse.resume(e);
        }
    });
}

  • 1 Execute your calling code in a separate thread.
  • 2 Resume the AsyncResponse with the received category.

Once again, this approach is similar to our synchronous example, but you use @Suspended and AsyncResponse to indicate to JAX-RS that you want the HTTP request to be suspended while you make your call to an external microservice.

If you already have your CategoryResource microservice running at http://localhost:8081, you can now start your new microservice by using Apache HttpClient.

Warning

You need to stop any previously running microservices before you can run this one, because they use the same port.

Change into the /chapter6/apache-httpclient directory and run this:

mvn thorntail:run

It’s now possible to access the microservices by accessing them in a browser: http://localhost:8080/sync and http://localhost:8080/async. As with your previous microservice, you’ll see a tree of categories that are currently present within the administration microservice.

In this section, you looked at client libraries that focus on using URLs and HTTP request methods directly. They’re great for interacting with HTTP resources, but they’re verbose when dealing with RESTful endpoints. Can you find client libraries that simplify your client code even further?

6.2. Consuming a microservice with a JAX-RS client library

This section introduces client libraries that bring your abstraction level even higher than HTTP. Both libraries provide APIs that are designed specifically for use in communicating with JAX-RS endpoints.

6.2.1. JAX-RS client

JAX-RS has been defined over the years as part of the JSR 311 and JSR 339 specifications of Java EE. As part of these specifications, JAX-RS has a client API that provides a developer with a cleaner means of calling RESTful endpoints from a JAX-RS resource.

So what are the benefits of using the JAX-RS client library? It allows you to forget about the low-level HTTP connection you need for connecting to a RESTful microservice, and focus on the required metadata such as the following:

  • HTTP method
  • Parameters to be passed
  • MediaType format of parameters and return type
  • Required cookies
  • Any other piece of metadata required to consume a RESTful microservice

When using the JAX-RS client library, you need to register a provider to handle the deserialization of JSON into LocalDateTime instances when processing a response. For that, you need the following listing, which you’ll use in our subsequent examples.

Listing 6.6. ClientJacksonProvider
public class ClientJacksonProvider implements ContextResolver<ObjectMapper> { 1

    private final ObjectMapper mapper = new ObjectMapper()                    2
            .registerModule(new JavaTimeModule());                            3

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return mapper;                                                        4
    }
}

  • 1 Provide ContextResolver for ObjectMapper instances.
  • 2 Create a new ObjectMapper instance.
  • 3 Register the JavaTimeModule for handling LocalDateTime conversion.
  • 4 Return the instance of ObjectMapper that you created when it’s requested.

Once again, you start with your synchronous example endpoint.

Listing 6.7. DisplayResource with JAX-RS client
@GET
@Path("/sync")
@Produces(MediaType.APPLICATION_JSON)
public Category getCategoryTreeSync() {
    Client client = ClientBuilder.newClient();         1

    return client
            .register(ClientJacksonProvider.class)     2
            .target(this.categoryUrl)                  3
            .request(MediaType.APPLICATION_JSON)       4
            .get(Category.class);                      5
}

  • 1 Create a JAX-RS client.
  • 2 Register the provider you defined in listing 6.6.
  • 3 Set the target of the client to be the CategoryResource URL.
  • 4 Specify that your response should return JSON.
  • 5 Make an HTTP GET request and convert the response body to Category.

When comparing the preceding listing to either of the pure Java client libraries, you have a significantly simplified and more coherent piece of code for calling an external microservice.

Is that important? In terms of the functionality required to execute a request and process the response, not at all. But that isn’t anywhere near as critical as how easily a developer can understand existing or develop new code. I’ll leave it up to you to judge, but I know I’d prefer to see the preceding example than anything else we’ve seen so far.

Can the JAX-RS client library likewise improve the readability of your code for asynchronous use? See the next listing.

Listing 6.8. DisplayResource with JAX-RS client and @Suspended
@GET
@Path("/async")
@Produces(MediaType.APPLICATION_JSON)
public void getCategoryTreeAsync(@Suspended final AsyncResponse
asyncResponse) throws Exception {
    executorService().execute(() -> {
        Client client = ClientBuilder.newClient();

        try {
            Category category = client.target(this.categoryUrl)
                    .register(ClientJacksonProvider.class)
                    .request(MediaType.APPLICATION_JSON)
                    .get(Category.class);

            asyncResponse.resume(category);
        } catch (Exception e) {
            asyncResponse.resume(Response                          1
                                         .serverError()
                                         .entity(e.getMessage())
                                         .build());
        }
    });
}

  • 1 Return a response you construct, including the exception message, instead of just passing the exception along.

As with all asynchronous usage, you specify @Suspended and AsyncResponse. You also use ManagedExecutorService to provide a new thread for processing your call, and you set the response with asyncResponse.resume().

You also could’ve used the asynchronous functionality of the JAX-RS client library itself.

Listing 6.9. DisplayResource with JAX-RS client and InvocationCallback
@GET
@Path("/asyncAlt")
@Produces(MediaType.APPLICATION_JSON)
public void getCategoryTreeAsyncAlt(@Suspended final AsyncResponse
asyncResponse) {
    Client client = ClientBuilder.newClient();
    WebTarget target = client.target(this.categoryUrl)
            .register(ClientJacksonProvider.class);
    target.request(MediaType.APPLICATION_JSON)
            .async()                                         1
            .get(new InvocationCallback<Category>() {        2
                @Override
                public void completed(Category result) {
                    asyncResponse.resume(result);
                }

                @Override
                public void failed(Throwable throwable) {
                    throwable.printStackTrace();
                    asyncResponse.resume(Response
                         .serverError()
                         .entity(throwable.getMessage())
                         .build());
                }
            });
}

  • 1 Indicate you want the call to be asynchronous.
  • 2 Pass InvocationCallback with methods for completed and failed handling.

This second asynchronous version alters which pieces of your code execute in a new thread, but it doesn’t alter the end result. In getCategoryTreeAsync(), you pass all your RESTful endpoint code into a new thread so that the HTTP request thread can be unblocked almost as quickly as it was processed. getCategoryTreeAsyncAlt() differs by executing only the HTTP request to your external microservice in a new thread. All the setup code required to make your HTTP request occurs in the same thread as the client request.

As getCategoryTreeAsyncAlt() uses the HTTP request thread opened for the client the longest, it reduces the throughput of the RESTful endpoints by causing each client to block on a thread for longer than necessary. Though the impact may be minimal, given a large enough number of requests, the impact exists.

So why show an inferior method that negatively affects throughput? First, as a way to show that there can be many means to achieve a similar goal. Second, many microservices may not have a large enough number of concurrent client requests for such a performance impact to be noticeable and cause problems. In such a case, a developer may prefer callbacks over any alternative—because when an option doesn’t impact performance, that’s a reasonable choice to make.

In switching to using the JAX-RS client library, you’ve simplified your calling code and made it clearer to understand. That certainly makes it more pleasurable to develop with than the lower-level libraries, but it does come at a cost in terms of how flexibly it can be used.

What kind of flexibility is lost? For most use cases, the JAX-RS client library wouldn’t cause any impact, but calling a microservice that uses a binary protocol would be more difficult. Depending on the protocol, it may require developing custom handlers and providers or incorporating additional third-party libraries that provide such features.

Change into the /chapter6/jaxrs-client directory and run this:

mvn thorntail:run

It’s now possible to access the microservices by accessing them in a browser: http://localhost:8080/sync and http://localhost:8080/async. As with our previous examples, you’ll see a tree of categories that are currently present within the administration microservice.

6.2.2. RESTEasy client

RESTEasy is an implementation of the JAX-RS specification that’s made available within WildFly as well as separately. Though many parts of its client library are identical to those provided by the JAX-RS client API, RESTEasy provides a particularly interesting feature that’s worthwhile.

With the JAX-RS client library, you specify what RESTful endpoint you want to call by chaining methods together to build up a picture of the endpoints, URL path, parameters, return type, media types, and so forth. There’s nothing wrong with that, but it’s not overly natural for developers who are more familiar with creating RESTful endpoints with JAX-RS.

With RESTEasy, you can re-create the RESTful endpoint that you want to communicate with as an interface and have a proxy of that interface generated for you. This process allows you to use an interface of the external microservice as if it were present within your own codebase.

For your external CategoryResource microservice, you’d create an interface like the following.

Listing 6.10. CategoryService
@Path("/admin/categorytree")
public interface CategoryService {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    Category getCategoryTree();
}

There’s nothing special about the code here. It looks like any other JAX-RS endpoint class, except that it’s an interface and there’s no method implementation. Another benefit is needing to define only the methods on the interface that your microservice requires. For instance, if an external microservice has five endpoints and your micro-service needs to use only one, your interface defining that external microservice requires only a single method. You don’t need to define the entire external microservice.

Is there an advantage to this? Definitely! It allows you to have a focused definition of an external microservice that you need to consume. If methods are updated on that microservice that you don’t consume, there’s no need to update your interface because you don’t use those endpoints.

Note

Taking this approach, it’d be possible to share the same interface between service and client. The service would provide an implementation of the interface for the actual endpoint code.

Warning

Such an approach, though possible, isn’t recommended practice for microservices because it becomes a separate library that both microservices depend on, introducing release timing and sequencing issues. This is a dangerous road to head down and will result in only continual pain for an enterprise. It’s therefore preferable to replicate the methods that you need to call.

Now that you’ve defined an interface that maps onto your external microservice, how can it be used?

Listing 6.11. DisplayResource with RESTEasy
@GET
@Path("/sync")
@Produces(MediaType.APPLICATION_JSON)
public Category getCategoryTreeSync() {
    ResteasyClient client = new ResteasyClientBuilder().build();           1
    ResteasyWebTarget target = client.target(this.categoryUrl)             2
            .register(ClientJacksonProvider.class);

    CategoryService categoryService = target.proxy(CategoryService.class); 3
    return categoryService.getCategoryTree();                              4
}

  • 1 Create the client with RESTEasy.
  • 2 Set the target URL base path for your request.
  • 3 Generate a proxy implementation of your CategoryService.
  • 4 Call CategoryResource via your proxy.

With this approach, you shift setting all the request parameters such as URL path, media types, and return types into your CategoryService interface. Now your client code interacting with the proxy behaves just like a local method call. You’ve gained a further simplification in your code by separating common request parameter values into a single place. This is particularly important when a microservice may require calling the same external microservice in different RESTful endpoints, because you don’t want to repeat information that isn’t going to change wherever it might be called from.

Let’s see some asynchronous examples with your proxy interface.

Listing 6.12. DisplayResource with RESTEasy and @Suspended
@GET
@Path("/async")
@Produces(MediaType.APPLICATION_JSON)
public void getCategoryTreeAsync(@Suspended final AsyncResponse
asyncResponse) throws Exception {
    executorService().execute(() -> {
        ResteasyClient client = new ResteasyClientBuilder().build();

        try {
            ResteasyWebTarget target = client.target(this.categoryUrl)
                    .register(ClientJacksonProvider.class);

            CategoryService categoryService =
     target.proxy(CategoryService.class);
            Category category = categoryService.getCategoryTree();
            asyncResponse.resume(category);
        } catch (Exception e) {
            asyncResponse.resume(Response
                                         .serverError()
                                         .entity(e.getMessage())
                                         .build());
        }
    });
}

The only changes you need between synchronous and asynchronous RESTful endpoints are the JAX-RS asynchronous requirements of @Suspended and @AsyncResponse, submitting the client code for processing in a separate thread, and setting either success or failure on asyncResponse.resume().

The one drawback with the proxy approach you’ve been using with the RESTEasy client library is that it doesn’t support invoking a callback when executing the call to your external microservice. As such, your getCategoryTreeAsyncAlt() with RESTEasy would be identical to when you used the JAX-RS client library.

Change into the /chapter6/resteasy-client directory and run this:

mvn thorntail:run

It’s now possible to access the microservices at http://localhost:8080/sync and http://localhost:8080/async. Each URL will return a tree of categories that are currently present within the administration microservice, as the result.

Now we’ve covered a couple of client libraries that provide a higher level of abstraction for interacting with RESTful endpoints. The examples show the benefits to the client code in reducing complexity and improving readability.

Summary

  • Java-based client libraries, such as java.net and Apache HttpClient, provide low-level access to networking in Java but create more verbose code than necessary.
  • JAX-RS-based client libraries provide an abstraction that makes consuming microservices easier.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.104.124