springspring-webfluxspring-webclientspring-reactive

Reactive Spring Boot: return evaluates before map finshed


I'm using reactive WebClient to build an API that communicates with 2 other APIs. API2 needs to get Information from API1, and then my service combines and returns both information. Resource:

@GetMapping("monoMedication/{medID}")
    public  Mono<Object> getMonoMedication(@PathVariable String medID) throws SSLException {
           Mono<Login> Login =createWebClient()
                .post()
                .uri("URI_LOGIN_API1" )
                .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromObject(body))
                .retrieve()
                .bodyToMono(Login.class);

        return Login.map(login-> {
                       Mono<String> medicationBundles = null;
            try {
                 medicationBundles = createWebClient()
                        .post()
                        .uri("URI_API1_GET_DATA")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromObject("Information"))
                        .header("Authorization", login.getSessionId())
                        .retrieve()
                       .bodyToMono(String.class);

            } catch (SSLException e) {
                e.printStackTrace();
            }

            return  medicationBundles.map(bundles_string -> {
                try {
                    List<Object> bundle_list = mapper.readValue(bundles_string, new TypeReference<List<Object>>(){});
                    bundle_list.forEach(bundle-> processBundle(bundle,medicationList));

                    return medicationList;
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                }
                    return null;
                })
        })
    } 

The Function:

List<String> medicationList = new ArrayList<>();
    private void processBundle(Object bundle, List<String> medicationlist) {
       //do something to get id from bundle
        String ID = bundle.getID();
// if i add something to medicationList.add(ID) here, it is in the required return of my API
        Mono<String> Medication = 
            webClientBuilder.build()
            .get()
            .uri("URI_API2_GET_DATA"+ID)
            .retrieve()
            .bodyToMono(String.class);

         Medication.map(medication_ID -> {
        //do something to get information from medication_ID
            String info = medication_ID.getInfo();

            //this Information comes after the required return
            return medicationList.add(info+ID);
        }).subscribe();
    }

My Problem is, that the return comes before the required last map is completed. I somehow missing something. I tried different approaches with e.g. then(), thenMany(), thenReturn() in different positions. Is there a way to do this? If there is a perhaps already a finished simple example, that would also help!


Solution

  • It's hard to follow along in your code because you are mixing and matching reactive programming with imperativ programming in a non best practice way.

    You code doesn't compile and you have several strange things like medID never being used and variables never declared like body. So i have only taken your code "as is", i have not produced a fully working example only a guide.

    You should pick to either go the reactive route, or the imperative route. Some parts of my answer will be opinionated, if anyone later will complain, so thats the disclaimer.

    First off, you are on each request creating several WebClients this is in my opinion considered bad practice. Creating a WebClient is sort of an expensive unneeded operation since you can reuse them, so you should declare your web clients during startup and @Autowire them in.

    @Configuration
    public class WebClientConfig {
    
    
        @Bean
        @Qualifier("WebClient1")
        public WebClient createWebClient1() {
            return WebClient.create( ... );
        }
    
        @Bean
        @Qualifier("WebClient2")
        public WebClient createWebClient2() {
            return WebClient.create( ... );
        }
    
        @Bean
        @Qualifier("WebClient3")
        public WebClient createWebClient3() {
            return WebClient.create( ... );
        }
    }
    

    And then use them by autowire them into your class.

    After cleaning up your code and dividing it up into functions, with proper returns i hope this gives you some idea how i would sort of structure it. Your problem is that you are not returning properly from your functions, and you are not chaining on the returns. As soon as you need to use subscribe you usually know you have done something wrong.

    @RestController
    public class FooBar {
    
        private WebClient webClient1;
        private WebClient webClient2;
        private WebClient webClient3;
    
        @Autowire
        public Foobar(@Qualifier("WebClient1") WebClient webclient1, @Qualifier("WebClient2") WebClient webclient2, @Qualifier("WebClient3") WebClient webclient3) {
            this.webClient1 = webClient1;
            this.webClient2 = webClient2;
            this.webClient3 = webClient3;
        }
    
        @GetMapping("monoMedication/{medID}")
        public Mono<List<MedicationData>> getMonoMedication(@PathVariable String medID) {
            return doLogin()
                .flatMap(login -> {
                    return getMedicationBundles(login.getSessionId());
                }).flatMap(medicationBundles -> {
                    return getMedicationData(medicationBundle.getId());
                }).collectList();
        }
    
        private Mono<Login> doLogin() {
             return webClient1
                    .post()
                    .uri("URI_LOGIN_API1")
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .bodyValue(body)
                    .retrieve()
                    .onStatus(HttpStatus::is4xxClientError, response -> ...)
                    .onStatus(HttpStatus::is5xxServerError, response -> ...)
                    .bodyToMono(Login.class);
        }
    
        private Flux<MedicationBundle> getMedicationBundles(String sessionId) {
            return webClient2
                    .post()
                    .uri("URI_API1_GET_DATA")
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .bodyValue("Information")
                    .header("Authorization", sessionId)
                    .retrieve()
                    .onStatus(HttpStatus::is4xxClientError, response -> ...)
                    .onStatus(HttpStatus::is5xxServerError, response -> ...)
                    .bodyToFlux(MedicationBundle.class);
        }
    
        private Mono<String> getMedicationData(String medicationId) {
            return webClient3.get()
                .uri(uriBuilder - > uriBuilder
                        .path("/URI_API2_GET_DATA/{medicationId}")
                        .build(medicationId))
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, response -> ...)
                .onStatus(HttpStatus::is5xxServerError, response -> ...)
                .bodyToMono(MedicationData.class);
        }
    }
    
    

    I wrote this by free hand without any IDE its more to show you how you should structure your code, hope this will show you some guidelines.

    Some do's and donts in reactive programming:

    Some good reads and videos:

    Getting started Reactor

    Webflux official documentation (i suggest going through the official reactor documentation first, these docs can be hard to understand unless you know reactor quite good)

    Good video from spring one, this one addresses pretty much everything i have written above. Do's and don't in reactive programming

    As said before this is a bit opinion based, but hopefully this will give you some guidelines.