'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