I have a web service with a couple of endpoints, one of them is a PUT endpoint that receives an XML and adds some information to a Redis server.
My endpoint is mapped in a configuration class:
@Configuration
public class RouterConfig {
@Autowired
private YAMLConfig yamlConfig;
@Bean
public RouterFunction<ServerResponse> getRoutes(final ServiceHandler serviceHandler) {
return RouterFunctions.route()
.GET(yamlConfig.getEnvPrefix() STATUS_ENDPOINT, serviceHandler::getStatus)
.GET(yamlConfig.getEnvPrefix() STOCK_CAP_API PRODUCT_CAPPING_ENDPOINT, serviceHandler::getStockCapForProducts)
.PUT(yamlConfig.getEnvPrefix() STOCK_CAP_API ADD_PRODUCT_CAP_FOR_CUSTOMER, RequestPredicates.contentType(MediaType.APPLICATION_XML), serviceHandler::AddStockCap)
.build();
}
}
The handler mehtod for the PUT endpoint looks like this:
@NonNull
public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
log.info("Adding new stock caps");
serverRequest.bodyToMono(CustomerCap.class)
.flatMap(stockCapService::addCustomerStockCap)
.doOnEach(result -> System.out.println("Cap added - " result));
return ServerResponse.ok().build();
}
The bodyToMono
is mapped to a class that represents the XML payload like the one below, and the attribute Customer inside the class is also mapped to a class in the same fashion.
@XmlRootElement(name="customerCap")
@XmlAccessorType(XmlAccessType.FIELD)
public class CustomerCap {
@XmlElement
private List<Customer> customer;
public List<Customer> getCustomer() {
return customer;
}
public void setCustomer(List<Customer> customer) {
this.customer = customer;
}
}
The implementation for the addCustomerStockCap is
@Override
public Mono<Boolean> addCustomerStockCap(CustomerCap customerCap) {
log.info("Starting adding process");
customerCap.getCustomer().forEach(customer -> {
final var customerUID = customer.getCustomerNumber();
customer.getCap().forEach(cap -> {
redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), CAP_LIMIT, String.valueOf(cap.getCustomerCapLimit()));
redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_FROM, String.valueOf(cap.getCapValidFrom()));
redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_TO, String.valueOf(cap.getCapValidTo()));
setExpirationOnHash(customerUID, cap);
});
});
return Mono.just(Boolean.TRUE);
}
The method to handle the endpoint is being invoked properly and I can see the "Addin new stock caps" message on the log, but then the addCustomerStockCap
is never invoked, with no exceptions or errors are being thrown, the method simply doesn't execute.
Another test that I did was creating a controller instead of a functional endpoint. The controller method ended up like this
@PutMapping(STOCK_CAP_API ADD_PRODUCT_CAP_FOR_CUSTOMER)
@ResponseStatus(HttpStatus.OK)
public void AddStockCap(@RequestBody CustomerCap customerCap) {
log.info("Adding new stock caps");;
stockCapService.addCustomerStockCap(customerCap);
}
When I did this I got some errors on the XML parse that I fixed only adding the annotation @NoArgsConstructor
to the CustomerCap
and other element classes. After this fix, the controller/endpoint worked as expected.
The problem still is that I need to make it work on the functional endpoint, we have several applications and we use functional endpoints as our standard so leaving only this endpoint like this wouldn't make sense.
CodePudding user response:
The issue is that your Mono
is never subscribed and thus nothing is emitted and as a consequence stockCapService::addCustomerStockCap
is never invoked. The following should work:
@NonNull
public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
log.info("Adding new stock caps");
serverRequest.bodyToMono(CustomerCap.class)
.flatMap(stockCapService::addCustomerStockCap)
.doOnEach(result -> System.out.println("Cap added - " result))
.subscribe();
return ServerResponse.ok().build();
}
Or
@NonNull
public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
log.info("Adding new stock caps");
return serverRequest.bodyToMono(CustomerCap.class)
.flatMap(stockCapService::addCustomerStockCap)
.doOnEach(result -> System.out.println("Cap added - " result))
.flatMap(ServerResponse.ok().build());
}
Update 21/11/2021
Try the following. You don't really need addCustomerStockCap
to return a Mono<Boolean>
:
@Override
public Boolean addCustomerStockCap(CustomerCap customerCap) {
log.info("Starting adding process");
customerCap.getCustomer().forEach(customer -> {
final var customerUID = customer.getCustomerNumber();
customer.getCap().forEach(cap -> {
redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), CAP_LIMIT, String.valueOf(cap.getCustomerCapLimit()));
redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_FROM, String.valueOf(cap.getCapValidFrom()));
redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_TO, String.valueOf(cap.getCapValidTo()));
setExpirationOnHash(customerUID, cap);
});
});
return Boolean.TRUE;
}
And now you can somewhat simplify your AddStockCap
(flatMap
turns a map
):
@NonNull
public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
log.info("Adding new stock caps");
return serverRequest.bodyToMono(CustomerCap.class)
.map(stockCapService::addCustomerStockCap)
.flatMap(ServerResponse.ok().build());
}
CodePudding user response:
The only way that worked for me to make sure the method is invoked was to add its result as the response body.
The problematic method ended up like this:
@Override
public Boolean addCustomerStockCap(CustomerCap customerCap) {
log.info("Starting adding process");
customerCap.getCustomer().forEach(customer -> {
final var customerUID = customer.getCustomerNumber();
customer.getCap().forEach(cap -> {
redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), CAP_LIMIT, String.valueOf(cap.getCustomerCapLimit()));
redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_FROM, String.valueOf(cap.getCapValidFrom()));
redisCommands.hset(String.join(SEP, STOCK_CAP, customerUID, cap.getMaterialNumber()), VALID_TO, String.valueOf(cap.getCapValidTo()));
setExpirationOnHash(customerUID, cap);
});
});
return Boolean.TRUE;
}
And the handler like this
@NonNull
public Mono<ServerResponse> AddStockCap(ServerRequest serverRequest) {
log.info("Adding new stock caps");
return ServerResponse.ok().body(serverRequest.bodyToFlux(CustomerCap.class).map(stockCapService::addCustomerStockCap), Boolean.class);
}
Not ideal as the goal was for this endpoint to only return a 200 code with no body, but it's a workaround until a found another way to invoke it.
Also on spring documentation, for XML and JSON payload we should use Flux instead of Mono
The following example extracts the body to a Flux (or a Flow in Kotlin), where Person objects are decoded from someserialized form, such as JSON or XML: