In Spring Reactive Java how can I write an updateById() method using the Router and Handler?
For example, the Router has this code:
RouterFunctions.route(RequestPredicates.PUT("/employees/{id}").and(RequestPredicates.accept(MediaType.APPLICATION_JSON))
.and(RequestPredicates.contentType(MediaType.APPLICATION_JSON)),
employeeHandler::updateEmployeeById);
My question is how to write the employeeHandler::updateEmployeeById() keeping the ID as same but changing the other members of the Employee object?
public Mono<ServerResponse> updateEmployeeById(ServerRequest serverRequest) {
Mono<Employee> employeeMono = serverRequest.bodyToMono(Employee.class);
<And now what??>
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(employeeMono, Employee.class);
}
The Employee class looks like this:
#Document
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Employee {
#Id
int id;
double salary;
}
Thanks for any help.
First of all, you have to add ReactiveMongoRepository in your classpath. You can also read about it here.
#Repository
public interface EmployeeRepository extends ReactiveMongoRepository<Employee, Integer> {
Mono<Employee> findById(Integer id);
}
Then your updateEmployeeById method can have the following structure:
public Mono<ServerResponse> updateEmployeeById(ServerRequest serverRequest) {
return serverRequest
.bodyToMono(Employee.class)
.doOnSubscribe(e -> log.info("update employee request received"))
.flatMap(employee -> {
Integer id = Integer.parseInt(serverRequest.pathVariable("id"));
return employeeRepository
.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException("employee with " + id + " has not been found")))
// what you need to do is to update already found entity with
// new values. Usually map() function is used for that purpose
// because map is about 'transformation' what is setting new
// values in our case
.map(foundEmployee -> {
foundEmployee.setSalary(employee.getSalary());
return foundEmployee;
});
})
.flatMap(employeeRepository::save)
.doOnError(error -> log.error("error while updating employee", error))
.doOnSuccess(e -> log.info("employee [{}] has been updated", e.getId()))
.flatMap(employee -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(employee), Employee.class));
}
UPDATE:
Based on Prana's answer, I have updated the code above merging our solutions in one. Logging with a help of Slf4j was added. And switchIfEmpty() functions for the case when the entity was not found.
I would also suggest your reading about global exception handling which will make your API even better. A part of it I can provide here:
/**
* Returns routing function.
*
* #param errorAttributes errorAttributes
* #return routing function
*/
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
private HttpStatus getStatus(Throwable error) {
HttpStatus status;
if (error instanceof NotFoundException) {
status = NOT_FOUND;
} else if (error instanceof ValidationException) {
status = BAD_REQUEST;
} else {
status = INTERNAL_SERVER_ERROR;
}
return status;
}
/**
* Custom global error handler.
*
* #param request request
* #return response
*/
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> errorPropertiesMap = getErrorAttributes(request, false);
Throwable error = getError(request);
HttpStatus errorStatus = getStatus(error);
return ServerResponse
.status(errorStatus)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
A slightly different version of the above worked without any exceptions:
public Mono<ServerResponse> updateEmployeeById(ServerRequest serverRequest) {
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
Mono<Employee> employeeMono = serverRequest.bodyToMono(Employee.class);
Integer employeeId = Integer.parseInt(serverRequest.pathVariable("id"));
employeeMono = employeeMono.flatMap(employee -> employeeRepository.findById(employeeId)
.map(foundEmployee -> {
foundEmployee.setSalary(employee.getSalary());
return foundEmployee;
})
.flatMap(employeeRepository::save));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(employeeMono, Employee.class).switchIfEmpty(notFound);
}
Thanks to Stepan Tsybulski.
Related
I'm currently updating from Spring boot 2.2.x to 2.6.x + legacy code, it's a big jump so there were multiple changes. I'm now running into a problem with load balancing through an api-gateway. I'll apologize in advance for the wall of code to come. I will put the point of failure at the bottom.
When I send in an API request, I get the following error:
more than one 'primary' bean found among candidates: [zookeeperDiscoveryClientServiceInstanceListSupplier, serviceInstanceListSupplier, retryAwareDiscoveryClientServiceInstanceListSupplier]
it seems that the zookeeperDiscovery and retryAware suppliers are loaded through the default serviceInsatnceListSupplier, which has #Primary over it. I thought would take precedence over the other ones. I assume I must be doing something wrong due changes in the newer version, here are the relevant code in question:
#Configuration
#LoadBalancerClients(defaultConfiguration = ClientConfiguration.class)
public class WebClientConfiguration {
#Bean
#Qualifier("microserviceWebClient")
#ConditionalOnMissingBean(name = "microserviceWebClient")
public WebClient microserviceWebClient(#Qualifier("microserviceWebClientBuilder") WebClient.Builder builder) {
return builder.build();
}
#Bean
#Qualifier("microserviceWebClientBuilder")
#ConditionalOnMissingBean(name = "microserviceWebClientBuilder")
#LoadBalanced
public WebClient.Builder microserviceWebClientBuilder() {
return WebClient.builder();
}
#Bean
#Primary
public ReactorLoadBalancerExchangeFilterFunction reactorLoadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
//the transformer is currently null, there wasn't a transformer before the upgrade
return new CustomExchangeFilterFunction(loadBalancerFactory, transformer);
}
}
There are also some Feign Client related configs here which I will omit, since it's not (or shouldn't be) playing a role in this problem:
public class ClientConfiguration {
/**
* The property key within the feign clients configuration context for the feign client name.
*/
public static final String FEIGN_CLIENT_NAME_PROPERTY = "feign.client.name";
public ClientConfiguration() {
}
//Creates a new BiPredicate for shouldClose. This will be used to determine if HTTP Connections should be automatically closed or not.
#Bean
#ConditionalOnMissingBean
public BiPredicate<Response, Type> shouldClose() {
return (Response response, Type type) -> {
if(type instanceof Class) {
Class<?> currentClass = (Class<?>) type;
return (null == AnnotationUtils.getAnnotation(currentClass, EnableResponseStream.class));
}
return true;
};
}
//Creates a Custom Decoder
#Bean
public Decoder createCustomDecoder(
ObjectFactory<HttpMessageConverters> converters, BiPredicate<Response, Type> shouldClose
) {
return new CustomDecoder(converters, shouldClose);
}
#Bean
#Qualifier("loadBalancerName")
public String loadBalancerName(PropertyResolver propertyResolver) {
String name = propertyResolver.getProperty(FEIGN_CLIENT_NAME_PROPERTY);
if(StringUtils.hasText(name)) {
// we are in a feign context
return name;
}
// we are in a LoadBalancerClientFactory context
name = propertyResolver.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
Assert.notNull(name, "Could not find a load balancer name within the configuration context!");
return name;
}
#Bean
public ReactorServiceInstanceLoadBalancer reactorServiceInstanceLoadBalancer(
BeanFactory beanFactory, #Qualifier("loadBalancerName") String loadBalancerName
) {
return new CustomRoundRobinLoadBalancer(
beanFactory.getBeanProvider(ServiceInstanceListSupplier.class),
loadBalancerName
);
}
#Bean
#Primary
public ServiceInstanceListSupplier serviceInstanceListSupplier(
#Qualifier(
"filter"
) Predicate<ServiceInstance> filter, DiscoveryClient discoveryClient, Environment environment, #Qualifier(
"loadBalancerName"
) String loadBalancerName
) {
// add service name to environment if necessary
if(environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME) == null) {
StandardEnvironment wrapped = new StandardEnvironment();
if(environment instanceof ConfigurableEnvironment) {
((ConfigurableEnvironment) environment).getPropertySources()
.forEach(s -> wrapped.getPropertySources().addLast(s));
}
Map<String, Object> additionalProperties = new HashMap<>();
additionalProperties.put(LoadBalancerClientFactory.PROPERTY_NAME, loadBalancerName);
wrapped.getPropertySources().addLast(new MapPropertySource(loadBalancerName, additionalProperties));
environment = wrapped;
}
return new FilteringInstanceListSupplier(filter, discoveryClient, environment);
}
}
There was a change in the ExchangeFilter constructor, but as far as I can tell, it accepts that empty transformer,I don't know if it's supposed to:
public class CustomExchangeFilterFunction extends ReactorLoadBalancerExchangeFilterFunction {
private static final ThreadLocal<ClientRequest> REQUEST_HOLDER = new ThreadLocal<>();
//I think it's wrong but I don't know what to do here
private static List<LoadBalancerClientRequestTransformer> transformersList;
private final Factory<ServiceInstance> loadBalancerFactory;
public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory) {
this(loadBalancerFactory);
///according to docs, but I don't know where and if I need to use this
#Bean
public LoadBalancerClientRequestTransformer transformer() {
return new LoadBalancerClientRequestTransformer() {
#Override
public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
return ClientRequest.from(request)
.header(instance.getInstanceId())
.build();
}
};
}
public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory, List<LoadBalancerClientRequestTransformer> transformersList) {
super(loadBalancerFactory, transformersList); //the changed constructor
this.loadBalancerFactory = loadBalancerFactory;;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
// put the current request into the thread context - ugly, but couldn't find a better way to access the request within
// the choose method without reimplementing nearly everything
REQUEST_HOLDER.set(request);
try {
return super.filter(request, next);
} finally {
REQUEST_HOLDER.remove();
}
}
//used to be an override, but the function has changed
//code execution doesn't even get this far yet
protected Mono<Response<ServiceInstance>> choose(String serviceId) {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerFactory.getInstance(serviceId);
if(loadBalancer == null) {
return Mono.just(new EmptyResponse());
}
ClientRequest request = REQUEST_HOLDER.get();
// this might be null, if the underlying implementation changed and this method is no longer executed in the same
// thread
// as the filter method
Assert.notNull(request, "request must not be null, underlying implementation seems to have changed");
return choose(loadBalancer, filter);
}
protected Mono<Response<ServiceInstance>> choose(
ReactiveLoadBalancer<ServiceInstance> loadBalancer,
Predicate<ServiceInstance> filter
) {
return Mono.from(loadBalancer.choose(new DefaultRequest<>(filter)));
}
}
There were pretty big changes in the CustomExchangeFilterFunction, but the current execution doesn't even get there. It fails here, in .getIfAvailable(...):
public class CustomRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final int DEFAULT_SEED_POSITION = 1000;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
private final int seedPosition;
private final AtomicInteger position;
private final Map<String, AtomicInteger> positionsForVersions = new HashMap<>();
public CustomRoundRobinLoadBalancer (
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId
) {
this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(DEFAULT_SEED_POSITION));
}
public CustomRoundRobinLoadBalancer (
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId,
int seedPosition
) {
Assert.notNull(serviceInstanceListSupplierProvider, "serviceInstanceListSupplierProvider must not be null");
Assert.notNull(serviceId, "serviceId must not be null");
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.serviceId = serviceId;
this.seedPosition = seedPosition;
this.position = new AtomicInteger(seedPosition);
}
#Override
// we have no choice but to use the raw type Request here, because this method overrides another one with this signature
public Mono<Response<ServiceInstance>> choose(#SuppressWarnings("rawtypes") Request request) {
//fails here!
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get().next().map((List<ServiceInstance> instances) -> getInstanceResponse(instances, request));
}
}
Edit: after some deeper stacktracing, it seems that it does go into the CustomFilterFunction and invokes the constructor with super(loadBalancerFactory, transformer)
I found the problem or a workaround. I was using #LoadBalancerClients because I thought it would just set the same config for all clients that way (even if I technically only have one atm). I changed it to ##LoadBalancerClient and it suddenly worked. I don't quite understand why this made a difference but it did!
I am new to Reactive programming and I got stuck writing a custom Insert query.
So far I have a FriendshipRepository.java class.
public interface FriendshipRepository extends R2dbcRepository<Friendship, String> {
#Query(value = "INSERT INTO public.friendship(requester_id, addressee_id) values (:requesterid::uuid, :addresseeid::uuid)")
public Mono<Void> insertFriendRequest(
#Param("requesterid") String requesterId,
#Param("addresseeid") String addresseeId
);
}
And a FriendshipController.java class.
#RestController
public class FriendsController {
private final FriendshipRepository friendshipRepository;
public FriendsController(FriendshipRepository friendshipRepository) {
this.friendshipRepository = friendshipRepository;
}
#PostMapping(value = "/request", produces = "application/json")
public Mono<ResponseEntity<RequestResponse>> sendFriendRequest(#RequestBody FriendRequest friendRequest, #AuthenticationPrincipal Mono<User> principal) throws Exception {
String id = principal.map(User::getId).toFuture().get();
return friendshipRepository.insertFriendRequest(id, friendRequest.getUserId()).log()
.then(Mono.just("NEXT"))
.map(e -> {
return ResponseEntity.status(HttpStatus.CREATED).body(new RequestResponse("Success", ResponseCode.SUCCESS));
}).onErrorResume(e -> {
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).body(new RequestResponse("Friend request was unsuccessful ", ResponseCode.REFUSED)));
});
}
}
This is a working example.
But I dont understand why I have to call .then(Mono.just("NEXT")) and create a new Mono to be able to return a custom ResponseEntity<RequestResponse>>. I also tried merge the the whole process. I meen by this at the begining when I get the Id from the ReactiveSpringSecutiryContext that is a blocking line of code and If I know it correctly that is a bad approach in Reactive programming.
I tried this approach but in this case, I can only retrun the Id of the user.
Mono<String> userId = principal.map(User::getId);
return userId.doOnNext(id -> {
friendshipRepository.insertFriendRequest(id, friendRequest.getUserId()).log()
.map(e -> {
return ResponseEntity.status(HttpStatus.CREATED).body(new RequestResponse("SIKER", ResponseCode.SUCCESS));
})
.onErrorResume(e -> {
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).body(new RequestResponse("Friend request was unsuccessful ", ResponseCode.REFUSED)));
}).subscribe();
// .doOnSuccess(e -> ServerResponse.noContent().build((new RequestResponse("SIKER", ResponseCode.SUCCESS)), Void.class));
});
How could I rewrite this endpoint? Or does my whole approach inaproptirate?
Thank you in advance for your help.
Probably not the nicest solution, but better then it was.
My query:
public interface FriendshipRepository extends R2dbcRepository<Friendship, String> {
#Query(value = "INSERT INTO public.friendship(requester_id, addressee_id) values (:requesterid::uuid, :addresseeid::uuid) RETURNING id")
public Mono<String> insertFriendRequest(
#Param("requesterid") String requesterId,
#Param("addresseeid") String addresseeId
);
}
My api:
#PostMapping(value = "/request", produces = "application/json")
public Mono<ResponseEntity<RequestResponse>> sendFriendRequest(#RequestBody FriendRequest friendRequest, #AuthenticationPrincipal Mono<User> principal) throws Exception {
String id = principal.map(User::getId).toFuture().get();
return friendshipRepository.insertFriendRequest(id, friendRequest.getUserId()).log()
.map(e -> {
return ResponseEntity.status(HttpStatus.CREATED).body(new RequestResponse("Success", ResponseCode.SUCCESS));
}).onErrorResume(e -> {
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).body(new RequestResponse("Friend request was unsuccessful ", ResponseCode.REFUSED)));
});
}
I am playing with Spring's WebClient. The primary implementation of the REST endpoints (in DemoPOJORouter and DemoPOJOHandler) seem to work. Also, the http.Get endpoint in DemoClientRouter and DemoClientHandler seems to work.
But, the http.Post for the DemoClient implementation "does nothing". It returns success (200), but nothing gets added to the dummy repo. I have a feeling that I need to do something in DemoClient to cause the http.Post endpoint in DemoPOJOHandler to actually execute (i.e., I believe neither the statements in DemoPOJOService.add() nor DemoPOJORepo.add() are being executed).
Based on prior pratfalls in WebFlux/reactive/functional efforts, I have a feeling that I'm not successfully subscribing, and so the statements never are invoked. But, I'm having difficulty identifying the "why".
Test code follows...
DemoClient router...
#Configuration
public class DemoClientRouter {
#Bean
public RouterFunction<ServerResponse> clientRoutes(DemoClientHandler requestHandler) {
return nest(path("/v2"),
nest(accept(APPLICATION_JSON),
RouterFunctions.route(RequestPredicates.GET("/DemoClient/{id}"), requestHandler::getById)
.andRoute(RequestPredicates.POST("/DemoClient"), requestHandler::add)));
}
}
DemoClient handler...
#Component
public class DemoClientHandler {
public static final String PATH_VAR_ID = "id";
#Autowired
DemoClient demoClient;
public Mono<ServerResponse> getById(ServerRequest request) {
Mono<DemoPOJO> monoDemoPOJO;
int id;
// short-circuit if bad request or invalid value for id
id = getIdFromServerRequest(request);
if (id < 1) {
return ServerResponse.badRequest().build();
}
// non-blocking mechanism for either returning the Mono<DemoPOJO>
// or an empty response if Mono<Void> was returned by repo.getById()
return demoClient.getById(id).flatMap(demoPOJO -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(demoPOJO), DemoPOJO.class))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> add(ServerRequest request) {
return request.bodyToMono(DemoPOJO.class).doOnSuccess( demoPOJO -> demoClient.add(demoPOJO))
.then(ServerResponse.ok().build())
.onErrorResume(e -> simpleErrorReporter(e))
.switchIfEmpty(ServerResponse.badRequest().build());
}
private int getIdFromServerRequest(ServerRequest request) {
Map<String, String> pathVariables = request.pathVariables();
int id = -1;
// short-circuit if bad request
// should never happen, but if this method is ever called directly (vice via DemoPOJORouter)
if ((pathVariables == null)
|| (!pathVariables.containsKey(PATH_VAR_ID))) {
return id;
}
try {
id = Integer.parseInt(pathVariables.get(PATH_VAR_ID));
} catch (NumberFormatException e) {
// swallow the error, return value <0 to signal error
id = -1;
}
return id;
}
private Mono<ServerResponse> simpleErrorReporter(Throwable e) {
return ServerResponse.badRequest()
.contentType(MediaType.TEXT_PLAIN)
.syncBody(e.getMessage());
}
}
DemoClient impl...
#Component
public class DemoClient {
private final WebClient client;
public DemoClient() {
client = WebClient.create();
}
public Mono<DemoPOJO> getById(int id) {
return client.get().uri("http://localhost:8080/v2/DemoPOJO/" + id)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(DemoPOJO.class));
}
public Mono<Boolean> add(DemoPOJO demoPOJO) {
return client.post().uri("http://localhost:8080/v2/DemoPOJO")
.syncBody(demoPOJO)
.exchange()
.flatMap(response -> response.bodyToMono(Boolean.class));
}
}
And, the DemoPOJO stuff, starting with DemoPOJORouter...
#Configuration
public class DemoPOJORouter {
#Bean
public RouterFunction<ServerResponse> demoPOJORoute(DemoPOJOHandler requestHandler) {
return nest(path("/v2"),
nest(accept(APPLICATION_JSON),
RouterFunctions.route(RequestPredicates.GET("/DemoPOJO/{id}"), requestHandler::getById)
.andRoute(RequestPredicates.POST("/DemoPOJO"), requestHandler::add)));
}
}
DemoPOJOHandler...
#Component
public class DemoPOJOHandler {
public static final String PATH_VAR_ID = "id";
#Autowired
private DemoPOJOService service;
public Mono<ServerResponse> getById(ServerRequest request) {
Mono<DemoPOJO> monoDemoPOJO;
int id;
// short-circuit if bad request or invalid value for id
id = getIdFromServerRequest(request);
if (id < 1) {
return ServerResponse.badRequest().build();
}
// non-blocking mechanism for either returning the Mono<DemoPOJO>
// or an empty response if Mono<Void> was returned by repo.getById()
return service.getById(id).flatMap(demoPOJO -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(demoPOJO), DemoPOJO.class))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> add(ServerRequest request) {
return request.bodyToMono(DemoPOJO.class).doOnSuccess( demoPOJO -> service.add(demoPOJO))
.then(ServerResponse.ok().build())
.onErrorResume(e -> simpleErrorReporter(e))
.switchIfEmpty(ServerResponse.badRequest().build());
}
private int getIdFromServerRequest(ServerRequest request) {
Map<String, String> pathVariables = request.pathVariables();
int id = -1;
// short-circuit if bad request
// should never happen, but if this method is ever called directly (vice via DemoPOJORouter)
if ((pathVariables == null)
|| (!pathVariables.containsKey(PATH_VAR_ID))) {
return id;
}
try {
id = Integer.parseInt(pathVariables.get(PATH_VAR_ID));
} catch (NumberFormatException e) {
// swallow the exception, return illegal value to signal error
id = -1;
}
return id;
}
private Mono<ServerResponse> simpleErrorReporter(Throwable e) {
return ServerResponse.badRequest()
.contentType(MediaType.TEXT_PLAIN)
.syncBody(e.getMessage());
}
}
DemoPOJOService...
#Component
public class DemoPOJOService {
#Autowired
private DemoPOJORepo demoPOJORepo;
public Mono<DemoPOJO> getById(int id) {
DemoPOJO demoPOJO = demoPOJORepo.getById(id);
return (demoPOJO == null) ? Mono.empty()
: Mono.just(demoPOJO);
}
public Mono<Boolean> add(DemoPOJO demoPOJO) {
return Mono.just(demoPOJORepo.add(demoPOJO));
}
}
DemoPOJORepo...
#Component
public class DemoPOJORepo {
private static final int NUM_OBJS = 5;
private static DemoPOJORepo demoRepo = null;
private Map<Integer, DemoPOJO> demoPOJOMap;
private DemoPOJORepo() {
initMap();
}
public static DemoPOJORepo getInstance() {
if (demoRepo == null) {
demoRepo = new DemoPOJORepo();
}
return demoRepo;
}
public DemoPOJO getById(int id) {
return demoPOJOMap.get(id);
}
public boolean add(DemoPOJO demoPOJO) throws InvalidParameterException {
// short-circuit on null pointer or duplicate id
if (demoPOJO == null) {
throw new InvalidParameterException("Add failed, null object detected...");
} else if (demoPOJOMap.containsKey(demoPOJO.getId())) {
throw new InvalidParameterException("Add failed, duplicate id detected...");
}
demoPOJOMap.put(demoPOJO.getId(), demoPOJO);
// if the return statement is reached, then the new demoPOJO was added
return true;
}
}
Finally, DemoPOJO...
public class DemoPOJO {
public static final String DEF_NAME = "DEFAULT NAME";
public static final int DEF_VALUE = 99;
private int id;
private String name;
private int value;
public DemoPOJO(int id) {
this(id, DEF_NAME, DEF_VALUE);
}
public DemoPOJO(#JsonProperty("id") int id, #JsonProperty("name") String name, #JsonProperty("value") int value) {
this.id = id;
this.name = name;
this.value = value;
}
/*
* setters and getters go here
*/
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(id);
builder.append(" :: ");
builder.append(name);
builder.append(" :: ");
builder.append(value);
return builder.toString();
}
}
Here is probably your problem.
DemoPOJOHandler.class
request.bodyToMono(DemoPOJO.class).doOnSuccess(demoPOJO -> service.add(demoPOJO))
DemoPOJOService.class
public Mono<Boolean> add(DemoPOJO demoPOJO) {
return Mono.just(demoPOJORepo.add(demoPOJO));
}
doOnSuccess returns Void, but you are calling a method that wraps the "action" in a returning Mono. So the demoPOJORepo#add function will never be triggered because you have broken the event chain here. The easiest fix is to just remove the wrapping Mono and return void.
public void add(DemoPOJO demoPOJO) {
demoPOJORepo.add(demoPOJO);
}
This took me way to long to find so here are some pointers when asking a question.
The names of your classes are too like each other, it was hard to follow the codeflow.
DemoPOJOService service your names are so alike so when i saw service was it the DemoPOJOService or the DemoClientService? clear names please.
There is nothing called http.POST when you wrote that i had no idea what you where talking about.
you had problems with the POST part but you posted everything, even the working GET parts, please only post code you suspect is relevant and are part of the problem.
Explain the question more clearly, what you have done, how you do it, what your application structure is and so fourth
Your endpoint urls say nothing "/DemoClient"?
How this question could have been asked to be more clear:
I have two endpoints in two routers in the same spring reactive
application.
When I do a POST request to the "/add" endpoint, this endpoint in turn
makes an a POST call using a WebClient to the same application just on
another endpoint called "/addToMap".
When this first call returns, it returns me a 200 OK status but when i
check the map (that the second endpoint is supposed to add the posted
data to) nothing gets added.
So please, next time asking a question, be clear, very clear, a lot clearer than you think. make sure your code is clear too with good variable and class names and clear url names. If you have messy names on your own computer its fine but when posting here be polite and clean up the code .It takes 5 minutes to add good names to classes and parameters so that we understand your code quicker.
take the time to read the "how to ask a good question" please.
How to ask a good question
In Spring controller approach, We could do REST request argument validation using #Valid with Something like this
#PostMapping(REGISTER)
public ResponseEntity<SomeData> registerSomeData(#RequestBody #Valid final SomeData someData) {
...................
}
public class SomeData {
#Size(min = 2, max = 20)
private String firstname;
#Digits(fraction = 0, integer = 10)
private Integer customerID;
#NotNull
private Customer customer;
}
If the request doesn't match these contraints, then Spring framework would throw Bad Request Exception(400).
With Spring5 router functions, I don't understand how we can do this, because we can't give #Valid in router functions.
It's mildly irritating that this useful functionality does not seem to have been carried over into the functional world but it's really not too hard to implement the validation step yourself. Here's how.
Create a bean to perform the validation:
#Component
public class RequestValidator {
#Inject
Validator validator;
public <T> Mono<T> validate(T obj) {
if (obj == null) {
return Mono.error(new IllegalArgumentException());
}
Set<ConstraintViolation<T>> violations = this.validator.validate(obj);
if (violations == null || violations.isEmpty()) {
return Mono.just(obj);
}
return Mono.error(new ConstraintViolationException(violations));
}
}
Now in your handler function include a step that performs the validation. In this example the FindRequest class is a JSON domain model class that contains validation annotations such as #NotEmpty and #NotNull etc. Adapt how you construct the ServerResponse based on this fictitious example that calls a reactive data repository.
#Component
public class MyHandler {
#Inject
RequestValidator validator;
public Mono<ServerResponse> findAllPeople(ServerRequest request) {
return request.bodyToMono(FindRequest.class)
.flatMap(this.validator::validate)
.flatMap(fr -> ServerResponse
.ok()
.body(this.repo.findAllByName(fr.getName()), Person.class));
}
}
The same approach can be used to extend the functionality to handle Flux as well as Mono.
You can't use annotation-based validation with (functional) Spring Webflux. See this answer.
If you absolutely need annotation-based validation, you should know that you can keep using the traditional Spring MVC with Spring 5 (or non-functional Webflux).
I created GeneralValidator class which works javax.validation.Validator
#Component
#RequiredArgsConstructor
public class GeneralValidator {
private final Validator validator;
private <T> void validate(T obj) {
if (obj == null) {
throw new IllegalArgumentException();
}
Set<ConstraintViolation<T>> violations = this.validator.validate(obj);
if (violations != null && !violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
/**
* #param obj object we will validate
* #param next Publisher we will call if don't have any validation hits
*/
public <T> Mono<ServerResponse> validateAndNext(T obj, Mono<ServerResponse> next) {
try {
validate(obj);
return next;
} catch (IllegalArgumentException ex) {
return ServerResponse.badRequest()
.body(new ErrorResponse("Request body is empty or unable to deserialize"), ErrorResponse.class);
} catch (ConstraintViolationException ex) {
return ServerResponse.badRequest()
.body(new ValidationErrorResponse(
"Request body failed validation",
ex.getConstraintViolations()
.stream()
.map(v -> "Field '%s' has value %s but %s"
.formatted(v.getPropertyPath(), v.getInvalidValue(),v.getMessage()))
.collect(Collectors.toList())
), ValidationErrorResponse.class);
}
}
}
How to use it:
...
.POST("/", req -> req.bodyToMono(RequestObject.class)
.flatMap(r -> validator.validateAndNext(r,routeFunction.execute(r)))
)
...
routeFunction.execute:
public #NotNull Mono<ServerResponse> execute(RequestObject request) {
//handling body
}
I have following spring controller code and want to return not found status if user is not found in database, how to do it?
#Controller
public class UserController {
#RequestMapping(value = "/user?${id}", method = RequestMethod.GET)
public #ResponseBody User getUser(#PathVariable Long id) {
....
}
}
JDK8 approach:
#RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
public ResponseEntity<User> getUser(#PathVariable Long id) {
return Optional
.ofNullable( userRepository.findOne(id) )
.map( user -> ResponseEntity.ok().body(user) ) //200 OK
.orElseGet( () -> ResponseEntity.notFound().build() ); //404 Not found
}
Change your handler method to have a return type of ResponseEntity. You can then return appropriately
#RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
public ResponseEntity<User> getUser(#PathVariable Long id) {
User user = ...;
if (user != null) {
return new ResponseEntity<User>(user, HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
Spring will use the same HttpMessageConverter objects to convert the User object as it does with #ResponseBody, except now you have more control over the status code and headers you want to return in the response.
With the latest update you can just use
return ResponseEntity.of(Optional<user>);
The rest is handled by below code
/**
* A shortcut for creating a {#code ResponseEntity} with the given body
* and the {#linkplain HttpStatus#OK OK} status, or an empty body and a
* {#linkplain HttpStatus#NOT_FOUND NOT FOUND} status in case of a
* {#linkplain Optional#empty()} parameter.
* #return the created {#code ResponseEntity}
* #since 5.1
*/
public static <T> ResponseEntity<T> of(Optional<T> body) {
Assert.notNull(body, "Body must not be null");
return body.map(ResponseEntity::ok).orElse(notFound().build());
}
public static ResponseEntity of(Optional body)
A shortcut for creating a ResponseEntity with the given body and the OK status, or an empty body and a NOT FOUND status in case of an Optional.empty() parameter.
#GetMapping(value = "/user/{id}")
public ResponseEntity<User> getUser(#PathVariable final Long id) {
return ResponseEntity.of(userRepository.findOne(id)));
}
public Optional<User> findOne(final Long id) {
MapSqlParameterSource paramSource = new MapSqlParameterSource().addValue("id", id);
try {
return Optional.of(namedParameterJdbcTemplate.queryForObject(SELECT_USER_BY_ID, paramSource, new UserMapper()));
} catch (DataAccessException dae) {
return Optional.empty();
}
}
it could be shorter using Method Reference operator ::
#RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
public ResponseEntity<User> getUser(#PathVariable Long id) {
return Optional.ofNullable(userRepository.findOne(id))
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
Need use ResponseEntity or #ResponseStatus, or with "extends RuntimeException"
#DeleteMapping(value = "")
public ResponseEntity<Employee> deleteEmployeeById(#RequestBody Employee employee) {
Employee tmp = employeeService.deleteEmployeeById(employee);
return new ResponseEntity<>(tmp, Objects.nonNull(tmp) ? HttpStatus.OK : HttpStatus.NOT_FOUND);
}
or
#ResponseStatus(value=HttpStatus.NOT_FOUND, reason="was Not Found")