rook218
rook218

Reputation: 670

The correct way to map a REST response to a Mono<SomeClass> using WebClient/ Spring Boot 2

I have a controller which is asking a service to reach a service endpoint to get a list of account numbers, then reach out to another service for each account number and retrieve additional data about that account. The 'happy path' essentially looks like this:

CONTROLLER

@ApiOperation(value = "getRelatedAccounts")
@GetMapping(value = "/relatedaccounts")
public ResponseEntity<RelatedAccounts> getRelatedAccounts(@RequestParam String accountNumber) {
    return new ResponseEntity<>(myService.getRelatedAccounts(accountNumber), HttpStatus.OK);
}

SERVICE

public RelatedAccounts getRelatedAccounts(String accountNumber) {
    // blah blah, hit the endpoint and get my response of a list of account numbers in relatedAccountsList
    Flux<AccountInformation> accountInfoFlux = Flux.fromIterable(relatedAccountsList)
        .parallel()
        .runOn(Schedulers.elastic())
        .flatMap(this::getAccountInformation)
        .ordered(Comparator.comparing(RelatedAccounts::getCorrectOrder)) // blah blah, convert from ParallelFlux to Flux
}

OTHER SERVICE

public Mono<AccountInformation> getAccountInformation(String accountNumber) {
    WebClient webClient = WebClient.builder()
            .baseUrl("http://myurl.com")
            .build();

    return webClient
            .get()
            .uri(uriBuilder -> uriBuilder
                    .queryParam("accountNumber", accountNumber)
                    .build()
            ).retrieve()
            .bodyToMono(AccountInformation.class) // This doesn't work for us, we get a very complex object as a response and need to parse a few fields.
}

I've spent all day on Google and I don't see anyone else parsing the response they get back, they just magically map it directly onto a perfectly created class. We don't have that option, we need to pluck the accountName from the response body and put it in the AccountInformation object. Does anyone have any idea how to do this?

Upvotes: 2

Views: 9245

Answers (2)

rook218
rook218

Reputation: 670

We ended up doing it a pretty ugly, but extensible and effective way. In our OTHER SERVICE:

public Mono<AccountInformation> getAccountInformation(String accountNumber) {
    WebClient webClient = WebClient.builder()
            .baseUrl("http://myurl.com")
            .build();

    return webClient
            .get()
            .uri(uriBuilder -> uriBuilder
                    .queryParam("accountNumber", accountNumber)
                    .build()
            ).retrieve()
            .bodyToMono(String.class) // just get the response as a JSON string
            .map(jsonString -> {
                ObjectMapper mapper = new ObjectMapper();
                JsonNode root = mapper.readTree(jsonString);
                // parse/ map out all your information here
                return myCustomMappedObject;
            })
}

Upvotes: 0

Joker
Joker

Reputation: 2453

My suggestion is to stay with bodyToMono(AccountInformation.class) and then map into your simple object using Monos map and zip.

  1. Create a class which represents the complex AccountInformation, but only with the information you need (dont include fields of object you dont need).
  2. Use the exact code of your other service (which returns Mono)
  3. You can use .zip to combine your simple object with the AccountInformation object.
  4. As a result you have your simple object with only the data needed from the complex object

Eg.

Other service stays the same:

public Mono<AccountInformation> getAccountInformation(String accountNumber) {
    WebClient webClient = WebClient.builder()
            .baseUrl("http://myurl.com")
            .build();

    return webClient
            .get()
            .uri(uriBuilder -> uriBuilder
                    .queryParam("accountNumber", accountNumber)
                    .build()
            ).retrieve()
            .bodyToMono(AccountInformation.class)
}

Create the missing class (which represents the result of the other service), skipp all fields for infos you dont need

import java.util.List;

public class AccountInformation {
    private String infoYouWant;
    private List<AccountDetails> otherNestedInfoYouWant;
// getters/setters skipped for easier read
}

Combine your simple object with the one from the other service:

        Mono<AccountInformation> accountInformation = someService.getAccountInformation("fillMeWithAccountNumber");
        Mono<SimpleObject> simpleObjectWithAccountInformation = Mono.zip(simpleObject, accountInformation).map(objects -> {
            SimpleObject simpleObject = objects.getT1();
            simpleObject.setAccountNumber(objects.getT2().getInfoYouWant());
            return simpleObject;
        });

Upvotes: 2

Related Questions