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!
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:
Avoid using try/catch blocks, in reactive programming you usually either return a Mono.empty()
to ignore an error or you return a Mono.error()
containing an exception.
Use flatMap
to process things asynchronously, map
to process synchronous, there are several other operators like concatMap
and flatMapSequential
that preserve order, explaining these are a seperate answer itself.
avoid void
methods if you can, always try to use pure functions (avoid manipulating lists in void functions, you do that with pointers in C++ not in Java), you can return Mono<Void>
from a reactive function by chaining on the .then()
operator.
Always take advantage of the type system if possible, try not to serialize into Object
if possible create an object representation of the data and serialize into it.
Almost never subscribe in your application unless you are the consumer of the data. The one that initiates the call is usually the subscriber
, your application is usually the producer
, and the calling client (web, or other service) is the subscriber
. Multiple subscribers
is usually a code smell.
Always try to return and chain on the returns. All functions i have written above return something and a chain on the returns, this is what is called the construction belt analogy
. I personally always start every function with writing the return statement on the first row, then i start writing my reactive stuff. Never leave a Mono
or Flux
without returning and chaining on to it, because a Mono
or Flux
will never get run if no one subscribes to it.
structure your code into functions, writing a function is free :)
Some good reads and videos:
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.