Spring Cloud Gateway for composite API calls?
Asked Answered
S

2

6

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?

Sayyid answered 8/4, 2019 at 13:16 Comment(0)
B
2

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.

Baccarat answered 8/4, 2019 at 13:32 Comment(4)
So, what you are saying is, it is not yet possible to do what i want and I should stick with standard controllers for these composite endpoints? Would using rest template be best practice?(all microservices are rest)Sayyid
I just found about ProxyExchange, which can serve this purpose instead of relying on RestTemplate. cloud.spring.io/spring-cloud-gateway/multi/…Sayyid
@Sayyid Great! That's what they also recommended on using within the issue I linked within my answer.Baccarat
@Sayyid could you please advise how you proceeded for this?Sugarplum
P
3

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"
    }
  }
]
Processional answered 15/4, 2022 at 19:41 Comment(0)
B
2

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.

Baccarat answered 8/4, 2019 at 13:32 Comment(4)
So, what you are saying is, it is not yet possible to do what i want and I should stick with standard controllers for these composite endpoints? Would using rest template be best practice?(all microservices are rest)Sayyid
I just found about ProxyExchange, which can serve this purpose instead of relying on RestTemplate. cloud.spring.io/spring-cloud-gateway/multi/…Sayyid
@Sayyid Great! That's what they also recommended on using within the issue I linked within my answer.Baccarat
@Sayyid could you please advise how you proceeded for this?Sugarplum

© 2022 - 2024 — McMap. All rights reserved.