'Spring Cloud Gateway for composite API calls?
I am starting to build a Microservice API Gateway, and I am considering Spring Cloud to help me with the routing. But some calls to the Gateway API will need multiple requests to different services.
Lets say I have 2 services: Order Details Service and Delivery Service. I want to have a Gateway endpoint GET /orders/{orderId} that makes a call to Order Details service and then Delivery Service and combine the two to return full Order details with delivery. Is this possible with the routing of Spring cloud or should I make these by hand using something like RestTemplate to make the calls?
Solution 1:[1]
There is an enhancement proposal posted on GitHub to have routes support multiple URIs. So far, there aren't any plans to implement this yet, at least, not according to one of the contributors.
Solution 2:[2]
As posted in the Spring Cloud Gateway Github issue mentioned by g00glen00b, until the library develops a Filter for this, I resolved it using the ModifyResponseBodyGatewayFilterFactory
in my own custom Filter.
Just in case it's useful for anyone else, I provide the base implementation here (it may need some rework, but it should be enough to make the point).
Simply put, I have a "base" service retrieving something like this:
[
{
"targetEntryId": "624a448cbc728123b47d08c4",
"sections": [
{
"title": "sadasa",
"description": "asda"
}
],
"id": "624a448c45459c4d757869f1"
},
{
"targetEntryId": "624a44e5bc728123b47d08c5",
"sections": [
{
"title": "asda",
"description": null
}
],
"id": "624a44e645459c4d757869f2"
}
]
And I want to enrich these entries with the actual targetEntry
data (of course, identified by targetEntryId
).
So, I created my Filter based on the ModifyResponseBody one:
/**
* <p>
* Filter to compose a response body with associated data from a second API.
* </p>
*
* @author rozagerardo
*/
@Component
public class ComposeFieldApiGatewayFilterFactory extends
AbstractGatewayFilterFactory<ComposeFieldApiGatewayFilterFactory.Config> {
public ComposeFieldApiGatewayFilterFactory() {
super(Config.class);
}
@Autowired
ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilter;
ParameterizedTypeReference<List<Map<String, Object>>> jsonType =
new ParameterizedTypeReference<List<Map<String, Object>>>() {
};
@Value("${server.port:9080}")
int aPort;
@Override
public GatewayFilter apply(final Config config) {
return modifyResponseBodyFilter.apply((c) -> {
c.setRewriteFunction(List.class, List.class, (filterExchange, input) -> {
List<Map<String, Object>> castedInput = (List<Map<String, Object>>) input;
// extract base field values (usually ids) and join them in a "," separated string
String baseFieldValues = castedInput.stream()
.map(bodyMap -> (String) bodyMap.get(config.getOriginBaseField()))
.collect(Collectors.joining(","));
// Request to a path managed by the Gateway
WebClient client = WebClient.create();
return client.get()
.uri(UriComponentsBuilder.fromUriString("http://localhost").port(aPort)
.path(config.getTargetGatewayPath())
.queryParam(config.getTargetQueryParam(), baseFieldValues).build().toUri())
.exchangeToMono(response -> response.bodyToMono(jsonType)
.map(targetEntries -> {
// create a Map using the base field values as keys fo easy access
Map<String, Map> targetEntriesMap = targetEntries.stream().collect(
Collectors.toMap(pr -> (String) pr.get("id"), pr -> pr));
// compose the origin body using the requested target entries
return castedInput.stream().map(originEntries -> {
originEntries.put(config.getComposeField(),
targetEntriesMap.get(originEntries.get(config.getOriginBaseField())));
return originEntries;
}).collect(Collectors.toList());
})
);
});
});
}
;
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("originBaseField", "targetGatewayPath", "targetQueryParam",
"composeField");
}
/**
* <p>
* Config class to use for AbstractGatewayFilterFactory.
* </p>
*/
public static class Config {
private String originBaseField;
private String targetGatewayPath;
private String targetQueryParam;
private String composeField;
public Config() {
}
// Getters and Setters...
}
}
For completeness, this is the corresponding route setup using my Filter:
spring:
cloud:
gateway:
routes:
# TARGET ENTRIES ROUTES
- id: targetentries_route
uri: ${configs.api.tagetentries.baseURL}
predicates:
- Path=/api/target/entries
- Method=GET
filters:
- RewritePath=/api/target/entries(?<segment>.*), /target-entries-service$\{segment}
# ORIGIN ENTRIES
- id: originentries_route
uri: ${configs.api.originentries.baseURL}
predicates:
- Path=/api/origin/entries**
filters:
- RewritePath=/api/origin/entries(?<segment>.*), /origin-entries-service$\{segment}
- ComposeFieldApi=targetEntryId,/api/target/entries,ids,targetEntry
And with this, my resulting response looks as follows:
[
{
"targetEntryId": "624a448cbc728123b47d08c4",
"sections": [
{
"title": "sadasa",
"description": "asda"
}
],
"id": "624a448c45459c4d757869f1",
"targetEntry": {
"id": "624a448cbc728123b47d08c4",
"targetEntityField": "whatever"
}
},
{
"targetEntryId": "624a44e5bc728123b47d08c5",
"sections": [
{
"title": "asda",
"description": null
}
],
"id": "624a44e645459c4d757869f2",
"targetEntry": {
"id": "624a44e5bc728123b47d08c5",
"targetEntityField": "somethingelse"
}
}
]
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | g00glen00b |
Solution 2 | Gerardo Roza |