An API Gateway is responsible for exposing an API, making multiple service calls, aggregating the results, and returning them to the client. The Finagle Scala framework makes this natural by representing service calls as futures, which can be composed to represent dependencies. To stay consistent with other examples in this book, we'll build a small example gateway service in Java using the Spring Boot framework:
- Create the project skeleton. Create a new Java project and add the following dependencies and plugins to the Gradle build file. We'll be using Spring Boot and Hystrix in this recipe:
plugins {
id 'org.springframework.boot' version '1.5.9.RELEASE'
}
group 'com.packtpub.microservices'
version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '1.5.9.RELEASE'
compile group: 'com.netflix.hystrix', name: 'hystrix-core', version: '1.0.2'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
Looking at the JSON example in the previous section, it's clear that we are collecting and aggregating some distinct domain concepts. For the purposes of this example, we'll imagine that we have a message service that retrieves information about messages, including likes, comments, and attachments, and a user service. Our gateway service will be making a call to the message service to retrieve the message itself, then calls to the other services to get the associated data, which we'll stitch together in a single response. For the purposes of this recipe, imagine the message service is running on port 4567 and the user service on port 4568. We'll create some stub services to mock out the data for these hypothetical microservices.
- Create a model to represent our Message data:
package com.packtpub.microservices.gateway.models;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = false)
public class Message {
private String id;
private String body;
@JsonProperty("from_user_id")
private String fromUserId;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getFromUserId() {
return fromUserId;
}
public void setFromUserId(String fromUserId) {
this.fromUserId = fromUserId;
}
}
It's important that non-dependent service calls be done in a non-blocking, asynchronous manner. Luckily, Hystrix has an option to execute commands asynchronously, returning Future<T>.
- Create a new package, say, com.packtpub.microservices.gateway.commands with the following classes:
- Create the AttachmentCommand class with the following content:
package com.packtpub.microservices.gateway.commands;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
public class AttachmentCommand extends HystrixCommand<String> {
private String messageId;
public AttachmentCommand(String messageId) {
super(HystrixCommandGroupKey.Factory.asKey("AttachmentCommand"));
this.messageId = messageId;
}
@Override
public String run() {
RestTemplate template = new RestTemplate();
String attachmentsUrl = "http://localhost:4567/message/" + messageId + "/attachments";
ResponseEntity<String> response = template.getForEntity(attachmentsUrl, String.class);
return response.getBody();
}
}
- Create the CommentCommand class with the following content:
package com.packtpub.microservices.commands;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
public class CommentCommand extends HystrixCommand<String> {
private String messageId;
public CommentCommand(String messageId) {
super(HystrixCommandGroupKey.Factory.asKey("CommentGroup"));
this.messageId = messageId;
}
@Override
public String run() {
RestTemplate template = new RestTemplate();
String commentsUrl = "http://localhost:4567/message/" + messageId + "/comments";
ResponseEntity<String> response = template.getForEntity(commentsUrl, String.class);
return response.getBody();
}
}
- Create the LikeCommand class with the following content:
package com.packtpub.microservices.commands;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
public class LikeCommand extends HystrixCommand<String> {
private String messageId;
public LikeCommand(String messageId) {
super(HystrixCommandGroupKey.Factory.asKey("Likegroup"));
this.messageId = messageId;
}
@Override
public String run() {
RestTemplate template = new RestTemplate();
String likesUrl = "http://localhost:4567/message/" + messageId + "/likes";
ResponseEntity<String> response = template.getForEntity(likesUrl, String.class);
return response.getBody();
}
}
- Our MessageClient class is a bit different than the previous examples—instead of returning the JSON string from the service response, it'll return an object representation, in this case, an instance of our Message class:
package com.packtpub.microservices.commands;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.packtpub.microservices.models.Message;
import org.springframework.web.client.RestTemplate;
public class MessageClient extends HystrixCommand<Message> {
private final String id;
public MessageClient(String id) {
super(HystrixCommandGroupKey.Factory.asKey("MessageGroup"));
this.id = id;
}
@Override
public Message run() {
RestTemplate template = new RestTemplate();
String messageServiceUrl = "http://localhost:4567/message/" + id;
Message message = template.getForObject(messageServiceUrl, Message.class);
return message;
}
}
- Create the UserCommand class with the following content:
package com.packtpub.microservices.commands;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
public class UserCommand extends HystrixCommand<String> {
private String id;
public UserCommand(String id) {
super(HystrixCommandGroupKey.Factory.asKey("UserGroup"));
this.id = id;
}
@Override
public String run() {
RestTemplate template = new RestTemplate();
String userServiceUrl = "http://localhost:4568/user/" + id;
ResponseEntity<String> response = template.getForEntity(userServiceUrl, String.class);
return response.getBody();
}
}
- Stitch together the execution of these Hystrix commands in a single controller that exposes our API as the /message_details/:message_id endpoint:
package com.packtpub.microservices;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.packtpub.microservices.commands.*;
import com.packtpub.microservices.models.Message;
import org.springframework.boot.SpringApplication;
import org.springframework.http.MediaType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@SpringBootApplication
@RestController
public class MainController {
@RequestMapping(value = "/message_details/{id}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Map<String, HashMap<String, String>> messageDetails(@PathVariable String id)
throws ExecutionException, InterruptedException, IOException {
Map<String, HashMap<String, String>> result = new HashMap<>();
HashMap<String, String> innerResult = new HashMap<>();
Message message = new MessageClient(id).run();
String messageId = message.getId();
Future<String> user = new UserClient(message.getFromUserId()).queue();
Future<String> attachments = new AttachmentClient(messageId).queue();
Future<String> likes = new LikeClient(messageId).queue();
Future<String> comments = new CommentClient(messageId).queue();
ObjectMapper mapper = new ObjectMapper();
StringWriter writer = new StringWriter();
mapper.writeValue(writer, message);
innerResult.put("message", writer.toString());
innerResult.put("from_user", user.get());
innerResult.put("attachments", attachments.get());
innerResult.put("comments", comments.get());
innerResult.put("likes", likes.get());
result.put("message_details", innerResult);
return result;
}
public static void main(String[] args) {
SpringApplication.run(MainController.class, args);
}
}
- There you have it. Run the service with ./gradlew bootRun and test it by making a request to:
$ curl -H "Content-Type: application/json" http://localhost:8080/message_details/1234