In order to model dependent asynchronous service calls, we'll take advantage of two features in Java 8. Streams are useful for processing data, so we'll use them in our example to extract usernames from a list of followings and map a function to each element. Java 8's CompletableFuture can be composed, which allows us to naturally express dependencies between futures.
In this recipe, we'll create a simple client application that calls the social service for a list of users that the current user follows. For each user returned, the application will get user details from the users service. We'll build this example as a command-line application for easy demonstration purposes, but it could just as well be another microservice, or a web or mobile client.
Before we start building our application, we'll outline the responses from our hypothetical social service and users service services. We can mimic these services by just hosting the appropriate JSON response on a local web server. We'll use ports 8000 and 8001 for the social service and users service, respectively. The social service has an endpoint, /followings/:username, that returns a JSON object with a list of followings for the specified username. The JSON response will look like the following snippet:
{
"username": "paulosman",
"followings": [
"johnsmith",
"janesmith",
"petersmith"
]
}
The users service has an endpoint called /users/:username, which will return a JSON representation of the user's details, including the username, full name, and avatar URL:
{
"username": "paulosman",
"full_name": "Paul Osman",
"avatar_url": "http://foo.com/pic.jpg"
}
Now that we have our services and we've outlined the responses we expect from each, let's go ahead and build our client application by performing the following steps:
- Create a new Java/Gradle application called UserDetailsClient with the following build.gradle file:
group 'com.packtpub.microservices'
version '1.0-SNAPSHOT'
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath group: 'org.springframework.boot', name: 'spring-boot-gradle
-plugin', version: '1.5.9.RELEASE'
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
compile group: 'org.springframework.boot',
name: 'spring-boot-starter-web'
}
- Create a package called com.packtpub.microservices.ch04.user.models and a new class called UserDetails. We'll use this class to model our response from the users service:
package com.packtpub.microservices.ch04.user.models;
import com.fasterxml.jackson.annotation.JsonProperty;
public class UserDetails {
private String username;
@JsonProperty("display_name")
private String displayName;
@JsonProperty("avatar_url")
private String avatarUrl;
public UserDetails() {}
public UserDetails(String username, String displayName,
String avatarUrl) {
this.username = username;
this.displayName = displayName;
this.avatarUrl = avatarUrl;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String toString() {
return String.format("[UserDetails: %s, %s, %s]", username,
displayName, avatarUrl);
}
}
- Create another class in the com.packtpub.microservices.ch04.user.models package called Followings. This will be used to model the response from the social service:
package com.packtpub.microservices.ch04.user.models;
import java.util.List;
public class Followings {
private String username;
private List<String> followings;
public Followings() {}
public Followings(String username, List<String> followings) {
this.username = username;
this.followings = followings;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public List<String> getFollowings() {
return followings;
}
public void setFollowings(List<String> followings) {
this.followings = followings;
}
public String toString() {
return String.format("[Followings for username: %s - %s]",
username, followings);
}
}
- Create a service representation for calling our social service. Predictably enough, we'll call it SocialService and put it in the com.packtpub.microservices.ch04.user.services package:
package com.packtpub.microservices.ch04.user.services;
import com.packtpub.microservices.models.Followings;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.CompletableFuture;
@Service
public class SocialService {
private final RestTemplate restTemplate;
public SocialService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
@Async
public CompletableFuture<Followings>
getFollowings(String username) {
String url = String.format("http://localhost:8000/followings/
%s", username);
Followings followings = restTemplate.getForObject(url,
Followings.class);
return CompletableFuture.completedFuture(followings);
}
}
- Create a service representation for our users service. Appropriately, we'll call the class UserService in the same package:
package com.packtpub.microservices.services;
import com.packtpub.microservices.models.Followings;
import com.packtpub.microservices.models.UserDetails;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.CompletableFuture;
@Service
public class UserService {
private final RestTemplate restTemplate;
public UserService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
@Async
public CompletableFuture<UserDetails>
getUserDetails(String username) {
String url = String.format("http://localhost:8001/users/
%s", username);
UserDetails userDetails = restTemplate.getForObject(url,
UserDetails.class);
return CompletableFuture.completedFuture(userDetails);
}
}
- We now have classes to model the responses from our services, and service objects to represent the services we're going to invoke. It's time to tie it all together by creating our main class, which will call these two services in a dependent manner, using the composability of futures to model the dependency. Create a new class called UserDetailsClient, as follows:
package com.packtpub.microservices.ch04.user;
import com.packtpub.microservices.models.Followings;
import com.packtpub.microservices.models.UserDetails;
import com.packtpub.microservices.services.SocialService;
import com.packtpub.microservices.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
@SpringBootApplication
public class UserDetailsClient implements CommandLineRunner {
public UserDetailsClient() {}
@Autowired
private SocialService socialService;
@Autowired
private UserService userService;
public CompletableFuture<List<UserDetails>>
getFollowingDetails(String username) {
return socialService.getFollowings(username).thenApply(f ->
f.getFollowings().stream().map(u ->userService.
getUserDetails(u)).map(CompletableFuture::join).
collect(Collectors.toList()));
}
public static void main(String[] args) {
SpringApplication.run(UserDetailsClient.class, args);
}
@Override
public void run(String... args) throws Exception {
Future<List<UserDetails>> users = getFollowingDetails
("paulosman");
System.out.println(users.get());
System.out.println("Heyo");
System.exit(0);
}
}
The magic really happens in the following method:
CompletableFuture<List<UserDetails>> getFollowingDetails(String username)
{
return socialService.getFollowings(username).thenApply(
f -> f.getFollowings().stream().map(u ->
userService.getUserDetails(u)).map(
CompletableFuture::join).collect(Collectors.toList()));
}
Recall that the getFollowings method in SocialService returns CompletableFuture<Followings>. CompletableFuture has a method, called thenApply, that takes the eventual result of the future (Followings) and applies it to be passed in the Lambda. In this case, we're taking Followings and using the Java 8 Stream API to call map on the list of usernames returned by the social service. The map applies each username to a function that calls getUserDetails on UserService. The CompletableFuture::join method is used to turn List<Future<T>> into Future<List<T>>, which is a common operation when performing these kinds of dependent service invocations. Finally, we collect the results and return them as a list.