I'm using a RouterFunction to define endpoints in my Spring Boot application. My service returns a Mono<Object> and I want to return the result of this when the endpoint is called. I also need to authenticate so I pass a UserPrinciple object through.
Router
#Bean
RouterFunction<ServerResponse> router() {
return route()
.GET("/api/endpoint-name", this::getExample)
.build();
}
private Mono<ServerResponse> getExample(ServerRequest request) {
return ServerResponse.ok().body(fromPublisher(getUserPrincipal().map(service::getSomething), Object.class)).log();
}
private Mono<UserPrincipal> getUserPrincipal() {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication())
.map(auth -> auth.getPrincipal())
.map(UserPrincipal.class::cast);
}
Service
public Mono<Object> getSomething(UserPrincipal userPrincipal) {
WebClient webClient = getWebClient(userPrincipal.getJwt());
return webClient.get()
.uri(uriBuilder -> uriBuilder.path("another/server/endpoint").build())
.retrieve()
.bodyToMono(Object.class);
}
The endpoint is returning this:
{
"scanAvailable": true
}
which suggests that I'm passing the Mono into the body of the response instead of passing in the result. However I've used fromPublisher which I thought would resolve this.
I can't find any examples where the service returns a Mono and the route correctly returns the result of the Mono.
How can I correctly pass a Mono/Flux as the body of the response?
im not going to explain the difference between mapand flatMapsince i have already written a quite comprehensive explanation here:
Do you have a test to show differences between the reactor map() and flatMap()?
The problem in the above code is the return of Object. And input parameters of Object into certain functions. The first function is pretty straight forward
Mono<UserPrincipal> = getUserPrincipal();
While the second one gets a bit more hairy:
Mono<Mono<Object> value = getUserPrincipal().map(service::getSomething);
So why are we getting A nested Mono?, well the get something returns a Mono<Object> and the Map return according the the api is Mono<R> where R is what we return from getSomething.
We then stick it into the fromPublisher which will unrap the first Mono ending up trying to serialize the Mono<Object>resulting in the strange response.
{
"scanAvailable": true
}
The answer here is pay more close attention to the type system. The body function takes a Publisher (Mono or Flux) so you don't need the fromPublisher function.
And also changing map to flatMap since the return type from inside a flatMap is a publisher.
ServerResponse.ok()
.body(getUserPrincipal()
.flatMap(service::getSomething), Object.class));
Related
I am fairly new to Java and Spring Boot (coming from TypeScript) and
experimenting with a small restful CRUD Controller using the
reactive Spring Boot API.
There are many tutorials and examples out there but they all lack
proper response statuses, e.g. giving a 404 on DELETE when the
resource doesn't exist.
What I like to achieve is a DELETE handler which
returns "204 No Content" if the resource existed and was deleted successfully
returns "404 Not found" if the resource doesn't exist
A simple "I don't care about HTTP status" DELETE handler
looks like this:
#DeleteMapping("/{id}")
public Mono<Void> deletePet(#PathVariable String id) {
return petRepository.deleteById(id);
}
This always gives status 200, even when there is no Pet for this ID.
I tried to use petRepository.findById(id) and .defaultIfEmpty()
in several ways to catch the 404 case, but without luck. E.g. with
this implementation I am getting always 204:
#DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deletePet(#PathVariable String id) {
return petRepository.findById(id)
.map(pet1 -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT))
.defaultIfEmpty(new ResponseEntity<Void>(HttpStatus.NOT_FOUND))
.flatMap(res -> {
return petRepository.deleteById(id)
.map(v -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT));
});
}
I think I understand why this isn't working, because after the .defaultIfEmpty()
the Mono isn't empty anymore and the .flatMap will have something to work
on (the 404 response) so the deleteById() is executed. This returns an (obviously)
non empty Mono as well, so the status turns into NO_CONTENT again.
But all my (many) attempts to change this failed so I hope anyone has the right
solution for this problem.
Thanks! :)
When findById returns an empty Mono, the code below will not executed either map or flatMap and will only return the value from defaultIfEmpty
#DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deletePet(#PathVariable String id) {
return petRepository.findById(id)
.map(pet1 -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT))
.flatMap(res -> {
return petRepository.deleteById(id)
.map(v -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT));
})
.defaultIfEmpty(new ResponseEntity<Void>(HttpStatus.NOT_FOUND));
}
Also, your understanding as to why this happens in your code snippet is correct.
After some more research I found a solution:
#DeleteMapping("/{id}")
#ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<ResponseEntity<Void>> deletePet(#PathVariable String id) {
return petRepository.findById(id)
.defaultIfEmpty(new Pet())
.flatMap(pet -> {
if (null == pet.id) {
return Mono.just(new ResponseEntity<Void>(HttpStatus.NOT_FOUND));
}
else {
return petRepository.deleteById(id)
.map(v -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT));
}
});
}
Now an empty Pet object is created when findById() gives an empty
result using defaultIfEmpty().
So the flatMap() gets either the Pet for the given ID or an empty
Pet. The latter is recognized by the fact that the id property is null
which is turned into a 404 response. In the other case the Pet is
deleted and 204 is returned. But note, that the .map() there isn't executed because of the empty deleteById() result. It's just necessary to satisfy the generic interface here. The 204 comes from the #ResponseStatus annotation.
So this is a possible solution - but it looks not very elegant to me (creating an empty Pet and having this no-op deleteById().map()).
If there is a better way to do this, please give your answer here.
I am trying to do two API calls, the second API call is dependent on the first API response. The following piece of code gives response for first weblient call.Here I am not getting the response from second API call. On log I could see that the request for the second webclient call is not even started with onSubscribe(). Can you please tell me what mistake am I doing.
#Autowired
Issue issue;
List issueList = new ArrayList<>();
public Mono<Response> getResponse(Request request) {
return webClient.post()
.uri("myURI")
.body(Mono.just(request),Request.class)
.retrieve()
.bodyToMono(Response.class)
.flatMap(resp->{
resp.getIssues().stream()
.forEach(issueTemp -> {
issue = issueTemp;
webClient.get()
.uri("mySecondURI" + issueTemp.getId())
.retrieve()
.bodyToMono(Issue.class)
.flatMap(issueTemp2-> {
issue.setSummary(issueTemp2.getSummary());
return Mono.just(issue);
}).log();
issueList.add(issue);
});
Response responseFinal = new Response();
responseFinal.setIssues(issueList);
return Mono.just(responseFinal);
}).log();
}
UPDATE 2:
I have changed my code to Functions and used Flux instead of stream iterations.What I am facing now is , all the iterations are get filtered out in doSecondCall method. Please refer my comment in doSecondCall method. Due to which the second call is not triggered. If i dont apply the filter, there are requests triggered like "issue/null" which also causes my service to go down.
public Mono<Response> getResponse(Request request) {
return webClient.post()
.uri("myURI")
.body(Mono.just(request),Request.class)
.retrieve()
.bodyToMono(Response.class)
.flatMap(r->
doSecondCall(r).flatMap(issueList->{
r.setIssues(issueList);
return Mono.just(r);
})
);
}
public Mono<Issue> doSecondCall(Response r) {
return Flux.fromIterable(r.getIssues())
.filter(rf->rf.getId()!=null) //everything gets filtered out
.flatMap(issue->getSummary(issue.getId()))
.collectList();
}
public Mono<Issue> getSummary(Response r) {
return webClient.get()
.uri("issue/"+id)
.retrieve()
.bodyToMono(Issue.class).log();
}
[ How does Reactive programming using WebFlux handles dependent external api calls ] #Thomas- Also ,Just found this thread. He basically says unless you block the first call, there is no way to declare the second one. Is that the case?
Why you are not triggering the second calls is because you are breaking the chain as i have mentioned in this answer (with examples).
Stop breaking the chain
// here...
.forEach(issueTemp -> {
issue = issueTemp; // and this is just silly? why?
webClient.get() // Here you are calling the webClient but ignoring the return value, so you are breaking the chain.
.uri("mySecondURI" + issueTemp.getId())
.retrieve()
.bodyToMono(Issue.class)
.flatMap(issueTemp2-> {
issue.setSummary(issueTemp2.getSummary());
return Mono.just(issue); // Return here but you are ignoring this return value
}).log();
issueList.add(issue);
});
You should use more functions to divide up your code. Make it a habit by writing a function and always start with the return statement. You code is very hard to read.
I think you should instead use a FLux instead of iterating a stream.
// something like the following i'm writing by free hand without IDE
// i have no idea what your logic looks like but you should get the point.
Flux.fromIterable(response.getIssues())
.flatMap(issue -> {
return getIssue(issue.getId())
.flatMap(response -> {
return issue.setSummary(reponse.getSummary());
});
}).collectList();
I have the following request handler
fun x(req: ServerRequest) = req.toMono()
.flatMap {
...
val oldest = myRepository.findOldest(...) // this is the object I want to modify
...
val v= anotherMongoReactiveRepository.save(Y(...)) // this saves successfully
myRepository.save(oldest.copy(
remaining = (oldest.remaining - 1)
)) // this is not saved
ok().body(...)
}
and the following mongodb reactive repository
#Repository
interface MyRepository : ReactiveMongoRepository<X, String>, ... {
}
The problem is that after the save() method is executed there is no changed in the object. I managed to fix the problem with save().block() but I don't know why the first save on the other repository works and this one isn't. Why is this block() required?
Nothing happens until someone subscribes to reactive Publisher. That's why it started to work when you used block(). If you need to make a call to DB and use the result in another DB request than use Mono / Flux operators like map(), flatMap(),... to build a pipeline of all the operations you need and after that return resulting Mono / Flux as controller’s response. Spring will subscribe to that Mono / Flux and will return the request. You don't need to block it. And it is not recommended to do it (to use block() method).
Short example how to work with MongoDB reactive repositories in Java:
#GetMapping("/users")
public Mono<User> getPopulation() {
return userRepository.findOldest()
.flatMap(user -> { // process the response from DB
user.setTheOldest(true);
return userRepository.save(user);
})
.map(user -> {...}); // another processing
}
Can anyone tell me in this example of routes everytime I type ex: /api/person/1, etc it all goes to /api/person? No matter which method I choose, it always goes with /api/person.
#Bean
public RouterFunction<ServerResponse> monoRouterFunction(PersonService personService) {
return RouterFunctions
.nest(path("/api/person"),
route(method(GET), personService::findAllPeople)
.andRoute(GET("/{id}"), personService::findOnePerson)
.andRoute(POST("/add"), personService::addPerson)
.andRoute(PUT("/update"), personService::updatePerson)
.andRoute(DELETE("/delete/{id}"), personService::deletePerson));
}
Below code is working fine. I have personally tried in my local. As #Brain told, just add GET("/") for findAllPeople() handler method.
#Bean
public RouterFunction<ServerResponse> monoRouterFunction(PersonService personService)
{
return RouterFunctions
.nest(path("/api/person"),
route(method(GET("/")), personService::findAllPeople)
.andRoute(GET("/{id}"), personService::findOnePerson)
.andRoute(POST("/add"), personService::addPerson)
.andRoute(PUT("/update"), personService::updatePerson)
.andRoute(DELETE("/delete/{id}"), personService::deletePerson));
}
Sample working application: https://github.com/karthikaiselvan/spring-reactive-mongo
Unlike the annotation model, WebFlux.fn is very explicit about routing: ordering and all predicates matter. But the good thing is it's easier to debug and you can set debug points in your predicates to understand why a request is being routed to a handler.
In this case, this RouterFunction could be described as:
If the path starts with "/api/person"
and the method is GET -> then personService::findAllPeople
and the method is POST and path matches "/api/person/{id}" -> then personService::findOnePerson
etc
Because the first match wins, a request like "GET /api/person/42" will match 1), since it starts with "/api/person" and it is a GET request.
If you want to change that, you can either change the order of your routes, or change your predicate for route(GET("/"), personService::findAllPeople).
even we can remove static imports, besides if we have different types of media types we can use the requestPredicate like the below code :
RouterFunction<ServerResponse> json = route()
.nest(accept(APPLICATION_JSON), builder -> builder
.GET("/{id}", personHandler::findOnePerson)
.GET("", personHandler::findAllPeople)).build();
RouterFunction<ServerResponse> html = route()
.nest(accept(TEXT_HTML), builder -> builder
.GET("/{id}", personHandler::renderPerson)
.GET("", personHandler::renderPersons)).build();
return route()
.path("api/person", () -> html.and(json)) // the default would be the first one(here is html)
.build();
you can find more details in my repository: https://github.com/minarashidi/webflux
I have a reactive rest api (webflux), also using the spring WebClient class, to request data from other rest services.
Simplified design:
#PostMapping(value = "/document")
public Mono<Document> save(#RequestBody Mono<Document> document){
//1st Problem: I do not know how to get the documentoOwner ID
//that is inside the Document class from the request body without using .block()
Mono<DocumentOwner> documentOwner = documentOwnerWebClient()
.get().uri("/document-owner/{id}", document.getDocumentOwner().getId())
.accept(MediaType.APPLICATION_STREAM_JSON)
.exchange()
.flatMap(do -> do.bodyToMono(DocumentOwner.class));
//2nd Problem: I need to check (validate) if the documentOwner object is "active", for instance
//documentOwner and document instances below should be the object per se, not the Mono returned from the external API
if (!documentOwner.isActive) throw SomeBusinessException();
document.setDocumentOwner(documentOwner);
//Now I can save the document in some reactive repository,
//and return the one saved with the given ID.
return documentRepository.save(document)
}
In other words: I understand (almost) all of the reactive examples individually, but I am not able to put it all together and build a simple use case (get -> validate -> save -> return) without blocking the objects.
The closer I could get is:
#PostMapping(value = "/document")
public Mono<Document> salvar(#RequestBody Mono<Document> documentRequest){
return documentRequest
.transform(this::getDocumentOwner)
.transform(this::validateDocumentOwner)
.and(documentRequest, this::setDocumentOwner)
.transform(this::saveDocument);
}
Auxiliar methods are:
private Mono<DocumentOwner> getDocumentOwner(Mono<Document> document) {
return document.flatMap(p -> documentOwnerConsumer.getDocumentOwner(p.getDocumentOwnerId()));
}
private Mono<DocumentOwner> validateDocumentOwner(Mono<DocumentOwner> documentOwner) {
return documentOwner.flatMap(do -> {
if (do.getActive()) {
return Mono.error(new BusinessException("Document Owner is Inactive"));
}
return Mono.just(do);
});
}
private DocumentOwnersetDocumentOwner(DocumentOwner documentOwner, Document document) {
document.setDocumentOwner(documentOwner);
return document;
}
private Mono<Document> saveDocument(Mono<Document> documentMono) {
return documentMono.flatMap(documentRepository::save);
}
I am using Netty, SpringBoot, Spring WebFlux and Reactive Mongo Repository. But there are some problems:
1) I am getting the error:
java.lang.IllegalStateException: Only one connection receive subscriber allowed. Maybe because I am using the same documentRequest to transform and to setDocumentOwner. I really don't know.
2) setDocumentOwner method is not being called. So the document object to be saved is not updated. I believe could have a better way to implement this setDocumentOwner().
Thanks
I don't really get the point with the validation aspect of that question.
But it looks like you're trying to compose reactive types together. This is something that reactor handles perfectly with operators.
There are certainly better ways to handler that case, but a quick search in the Mono API makes me think about this:
Mono<Document> document = ...
Mono<DocumentOwner> docOwner = ...
another = Mono.when(document, docOwner)
.map(tuple -> {
Document doc = tuple.getT1();
DocumentOwner owner = tuple.getT2();
if(owner.getActive()) {
return Mono.error(new BusinessException("Document Owner is Inactive"));
}
doc.setDocumentOwner(owner);
return doc;
})
.flatMap(documentRepository::save);