Why can't i subscribe multiple times to a publisher? - spring

i wrote two methods,first one calls the publisher (productDtoMono) only once and the other one calls it twice,
the first one returns bad request (empty body) when called from controller
while the second works perfectly and returns http ok
i did my homework and i see that i can't consume the publisher more than one time
why is that ?
public Mono<ProductDto> firstupdateProduct(Mono<ProductDto> productDtoMono) {
return productDtoMono.flatMap(productDto ->
productRepository.findById(productDto.getId())
.flatMap(p -> productDtoMono
.map(EntityDtoUtil::toEntity)))
.flatMap(productRepository::save)
.map(EntityDtoUtil::toDto);
}
public Mono<ProductDto> secondupdateProduct(String id, Mono<ProductDto> productDtoMono) {
return productRepository.findById(id)
.flatMap(product -> productDtoMono
.map(EntityDtoUtil::toEntity))
.flatMap(productRepository::save)
.map(EntityDtoUtil::toDto);
}
my controller method :
#PutMapping
public Mono<ResponseEntity<ProductDto>> updateProduct(#RequestBody Mono<ProductDto> productDtoMono){
System.out.println("in controller "+productDtoMono);
return productService.updateProduct(productDtoMono)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}

I did more research. Mono, Flux are cold publishers by default, so when the source of their creation comes from a request, they can only be consumed once. When we try to consume it a second time, it tries to generate the object from a request, hence the bad request get thrown. I had to cache the body while passing it to service like this:
#PutMapping
public Mono<ResponseEntity<ProductDto>> updateProduct(#RequestBody Mono<ProductDto> productDtoMono){
System.out.println("in controller "+productDtoMono);
return productService.updateProduct(productDtoMono.cache())
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
and changed updateProduct like this
public Mono<ProductDto> updateProduct(Mono<ProductDto> productDtoMono) {
return productDtoMono.map(ProductDto::getId)
.flatMap(productRepository::findById)
.flatMap(product -> productDtoMono.map(EntityDtoUtil::toEntity))
.flatMap(productRepository::save)
.map(EntityDtoUtil::toDto);
}

Related

route request to a different controller

I have a controller that has read function and handles urls like this
-> /{id}/EnumElement
class FirstController {
Object read(#PathVariable UUID id, #PathVariable EnumeratedEntity value,
HttpServletRequest request)
}
I want to add the second controller that would handle only a single request
class SecondController {
-> /{id}/metadata
Object meta(#PathVariable UUID id, HttpServletRequest request)
}
I also have a controller advice that supposed to handle EnumeratedEntity values
#RestControllerAdvice
public class DefaultControllerAdvice {
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(EnumeratedEntity.class, new PropertyEditorSupport() {
#Override
public void setAsText(final String text) {
try {
setValue(EnumeratedEntity.valueOf(text));
} catch (IllegalArgumentException e) {
throw new UnknownResourceException("The requested resource is not supported", e);
}
}
});
}
The problem is that each time I request /metadata the advicer is trying to get the value of metadata (that does not exist) and throws an error. Also it seems like it takes first controller as a priority or something.
Is there a way to route /metadata request to a second controller and ignore advicer altogether?
After some search I decided to go with this solution for the first controller
#GetMapping(/{id}/{enumElement:^(?!metadata).*})
Object read(#PathVariable UUID id, #PathVariable EnumeratedEntity value,
HttpServletRequest request)

Spring Webflux - lazily initialized in-memory cache

can some one help me with a correct pattern for stateful service in SpringWebflux? I have a REST service which communicates with an external API and needs to fetch auth token from that API during the first call and cache it to reuse in all next calls. Currently I'm having a code which works, but concurrent calls cause multiple token requests. Is there a way to handle concurrency?
#Service
#RequiredArgsConstructor
public class ExternalTokenRepository {
private final WebClient webClient;
private Object cachedToken = null;
public Mono<Object> getToken() {
if (cachedToken != null) {
return Mono.just(cachedToken);
} else {
return webClient.post()
//...
.exchangeToMono(response -> {
//...
return response.bodyToMono(Object.class)
})
.doOnNext(token -> cachedToken = token)
}
}
}
UPDATED: Token I receive have some expiration and I need to refresh it after some time. Refresh request should be call only once too.
You can initialize Mono in the constructor and use cache operator:
#Service
public class ExternalTokenRepository {
private final Mono<Object> cachedToken;
public ExternalTokenRepository(WebClient webClient) {
this.cachedToken = webClient.post()
//...
.exchangeToMono(response -> {
//...
return response.bodyToMono(Object.class);
})
.cache(); // this is the important part
}
public Mono<Object> getToken() {
return cachedToken;
}
}
UPDATE: cache operator also supports TTL based on the return value: https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html#cache-java.util.function.Function-java.util.function.Function-java.util.function.Supplier-

How use thenEmpty and then thenReturn in Spring WebFlux?

I'm new in Spring WebFlux and I have a problem. I want to return something like Mono<String> as follow:
#PostMapping("/test")
public Mono<String> test(){
return Mono.just("Test String")
.thenEmpty(it -> {
// Do something I need
System.out.println("Print somethings");
})
.thenReturn("Return String");
}
I wish the method to return Return String, But it return nothing. What's the Problem?
thenEmpty will be invoked and return Mono<Void> as part of pipeline order. since Mono<Void> return, rest of the operator in pipeline is not working. if you want to use thenReturn then use below code.
#PostMapping("/test")
public Mono<String> test(){
return Mono.just("Test String")
.doOnNext(s -> {
// do what you want
})
.thenReturn("Return String");
}
you can try something like this:
#PostMapping("/test") public Mono<String> test(){
return Mono.just("Test String")
.doOnNext(s -> {
// do what you want
})
.map(s -> {
return "done";
});
}
You can use other doOn* methods, depending on what you need. For example doOnSuccess or doOnError.
Then you can use map if you need to manipulate your data (but keeping return type).

Return response messages in spring boot

I am working with spring boot with a h2 database. I would like to return a 201 message when the register is inserted succesfully and a 400 when is duplicated. I am using ResponseEntity to achieve this, fot example , the next is my create method from the Service:
#Override
public ResponseEntity<Object> createEvent(EventDTO eventDTO) {
if (eventRepository.findOne(eventDTO.getId()) != null) {
//THis is a test, I am looking for the correct message
return new ResponseEntity(HttpStatus.IM_USED);
}
Actor actor = actorService.createActor(eventDTO.getActor());
Repo repo = repoService.createRepo(eventDTO.getRepo());
Event event = new Event(eventDTO.getId(), eventDTO.getType(), actor, repo, createdAt(eventDTO));
eventRepository.save(event);
return new ResponseEntity(HttpStatus.CREATED);
}
This is my controller:
#PostMapping(value = "/events")
public ResponseEntity addEvent(#RequestBody EventDTO body) {
return eventService.createEvent(body);
}
But I'm not getting any message in the browser, I am doing different tests with postman and when I consult for all the events, the result is correct, but each time that I make a post I dont get any message in the browser, I am not pretty sure what is the cause of this issue. Any ideas?
The ideal way to send Response to the client is to create DTO/DAO with ResponseEntity in Controller
Controller.java
#PostMapping("/test")
public ResponseEntity<Object> testApi(#RequestBody User user)
{
System.out.println("User: "+user.toString());
return assetService.testApi(user);
}
Service.java
public ResponseEntity testApi(User user) {
if(user.getId()==1)
return new ResponseEntity("Created",HttpStatus.CREATED);
else
return new ResponseEntity("Used",HttpStatus.IM_USED);
// for BAD_REQUEST(400) return new ResponseEntity("Bad Request",HttpStatus.BAD_REQUEST);
}
Tested using Postman
Status 201 Created
Status 226 IM Used
Okay, I really don't feel good that service sending the ResponseEntity but not Controller.You could use #ResponseStatus and ExceptionHandler classes for these cases, like below.
Create a class in exception package
GlobalExceptionHandler.java
#ControllerAdvice
public class GlobalExceptionHandler {
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(DataIntegrityViolationException.class) // NOTE : You could create a custom exception class to handle duplications
public void handleConflict() {
}
}
Controller.java
#PostMapping(value = "/events")
#ResponseStatus(HttpStatus.CREATED) // You don't have to return any object this will take care of the status
public void addEvent(#RequestBody EventDTO body) {
eventService.createEvent(body);
}
Now changing the service would look like,
Service.java
#Override
public void createEvent(EventDTO eventDTO) { // No need to return
if (eventRepository.findOne(eventDTO.getId()) != null) {
throw new DataIntegrityViolationException("Already exists"); // you have to throw the same exception which you have marked in Handler class
}
Actor actor = actorService.createActor(eventDTO.getActor());
Repo repo = repoService.createRepo(eventDTO.getRepo());
Event event = new Event(eventDTO.getId(), eventDTO.getType(), actor, repo, createdAt(eventDTO));
eventRepository.save(event);
}

Spring 3.0 RESTful Controller Fails on Redirect

I am setting up a simple RESTful controller for a Todo resource with an XML representation. It all works great - until I try to redirect. For example, when I POST a new Todo and attempt to redirect to its new URL (for example /todos/5, I get the following error:
Error 500 Unable to locate object to be marshalled in model: {}
I do know the POST worked because I can manually go to the new URL (/todos/5) and see the newly created resource. Its only when trying to redirect that I get the failure. I know in my example I could just return the newly created Todo object, but I have other cases where a redirect makes sense. The error looks like a marshaling problem, but like I said, it only rears itself when I add redirects to my RESTful methods, and does not occur if manually hitting the URL I am redirecting to.
A snippet of the code:
#Controller
#RequestMapping("/todos")
public class TodoController {
#RequestMapping(value="/{id}", method=GET)
public Todo getTodo(#PathVariable long id) {
return todoRepository.findById(id);
}
#RequestMapping(method=POST)
public String newTodo(#RequestBody Todo todo) {
todoRepository.save(todo); // generates and sets the ID on the todo object
return "redirect:/todos/" + todo.getId();
}
... more methods ...
public void setTodoRepository(TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
private TodoRepository todoRepository;
}
Can you spot what I am missing? I am suspecting it may have something to do with returning a redirect string - perhaps instead of it triggering a redirect it is actually being passed to the XML marshaling view used by my view resolver (not shown - but typical of all the online examples), and JAXB (the configured OXM tool) doesn't know what to do with it. Just a guess...
Thanks in advance.
This happend because redirect: prefix is handled by InternalResourceViewResolver (actually, by UrlBasedViewResolver). So, if you don't have InternalResourceViewResolver or your request doesn't get into it during view resolution process, redirect is not handled.
To solve it, you can either return a RedirectView from your controller method, or add a custom view resolver for handling redirects:
public class RedirectViewResolver implements ViewResolver, Ordered {
private int order = Integer.MIN_VALUE;
public View resolveViewName(String viewName, Locale arg1) throws Exception {
if (viewName.startsWith(UrlBasedViewResolver.REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(UrlBasedViewResolver.REDIRECT_URL_PREFIX.length());
return new RedirectView(redirectUrl, true);
}
return null;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
}

Resources