How to dynamically select the controller with the same request using Spring WebFlux? - spring

I have an application implemented using Spring WebFlux, and the routing is done through RouterFunctions. Now I want the controller selection to be based on some customized class dynamically in runtime instead of just static URI pattern or request header, how to do that?
For example: there is a request with path /v1/xyz, today we want to have 60% random requests with that path go to controller A and 40% go to controller B, and tomorrow the percentage will be adjusted to 80% and 20%. So I need a mechanism to dynamically decide which controller the same request goes to, how to do that?
Thank you.

You could use Spring WebFlux Functional Endpoints
a lightweight functional programming model in which functions are
used to route and handle requests
that will give you more flexibility and control comparing to controllers.
#Bean
RouterFunction<ServerResponse> xyzRoute() {
return route(POST("/v1/xyz"), req -> handler(req));
}
Mono<ServerResponse> handler(ServerRequest serverRequest) {
return evaluateCondition()
.flatMap(condition -> {
if (Boolean.TRUE.equals(condition)) {
retrun handler1(serverRequest);
} else {
retrun handler2(serverRequest);
}
});
}
Mono<ServerResponse> handler1(ServerRequest serverRequest) {
...
}
Mono<ServerResponse> handler2(ServerRequest serverRequest) {
...
}

Related

Spring webflux Multiple Exception handler in functional Endpoint

im working ins Spring web flux project and I used functional endpoints instead of controller annotation but I didn't find a solution to handle multiple exceptions for the same endpoint , this is my code :
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.GET("/router/users/{id}"),this::renderException);
}
private Mono<ServerResponse> renderException(ServerRequest request) {
Map<String, Object> error = this.getErrorAttributes(request, ErrorAttributeOptions.defaults());
error.remove("status");
error.remove("requestId");
return ServerResponse.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(error));
}
for the endpoint /router/users/{id} i trigger UserNotFoundException and UserException and I want to return 404 for UserNotFoundException and 500 for UserException but I don't know how to do that in the functional endpoint. anyone can guide me on how to do this in the correct way like we did in using #ExceptionHandler in rest controller?
If returning proper code is all you care about then adding #ResponseStatus for your custom exceptions might be the best solution.
#ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
// more methods...
public UserNotFoundException(final String message) {
super(message);
}
}
But if you want to build ServerResponse by yourself, make use of project reactor operators, like switchIfEmpty() or onErrorMap(), etc. which could be used as following
Mono<ServerResponse> response() {
return exampleService.getUser()
.flatMap(user -> ServerResponse.ok().body(user, User.class))
.switchIfEmpty(ServerResponse.notFound().build());
}
You might want to take a look at docs Which operator do I need? - handling errors

Is there a way to pass the request parameter to the /health endpoint in webflux?

I am writing a custom REACTIVE Health Indicator (i.e. by implementing ReactiveHealthIndicator) to check the connectivity to another app. However to check this I require some request parameter in the request URL of /health endpoint.(e.g. URL that i want is "/health?countryCode=IN") is there a way to achieve this? Please note that as it is reactive, autowiring HttpServletRequest won't work.
#Component
public class CustomServicesHealthIndicator implements ReactiveHealthIndicator {
#Override
public Mono<Health> health() {
//#TODO : This will be called like localhost:8080/acuator/health?param=value.
// Need to read value of 'param' here and take some actions accordingly
return doHealthCheck()
.onErrorResume(ex -> Mono.just(new Health.Builder().down(ex).build()));
}
private Mono<Health> doHealthCheck(){
return Mono.just(new Health.Builder().up().build());
}
}
Documentation : https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#reactive-health-indicators

Rest api filtering in spring boot

I have a project in spring boot, in my controller I have many methods with similar functionality.
Methods for searching post, popular, latest etc and the urls with slight variation like -
url 1 - search/{topicId}
url 2 - search/popular/{topicId}
url 3 - search/latest/{topicId}
What I want, is to have a single method with filter in url like search/{topicId}?filter=popular
How to achieve this in spring boot?
OOPs... it does not depend on SpringBoot. It is simply a URL mapping...You can accept the type as a request param and can process as per business.....
#Controller
public class BookController {
#GetMapping(value = "/search/{topicId}")
#ResponseBody
public List<Object> getBooksByType(#RequestParam String type) {
try{
if("popular".equalsIgnoreCase(type)){
//do your business stuffs
}else if ("latest".equalsIgnoreCase(type)){
//do your business stuffs
}
}catch(Exception e){
e.printStackTrace();
}
return new ArrayList<>();
}
}

What is the best way to validate request in a Spring Webflux functional application

In a traditional web application it is easy to validate the request body in the controller method, eg.
ResponseEntity create(#Valid #ResponseBody Post post) {
}
If it is a MVC application, we can gather the errors by injecting a BindingResult, and decide if there is some validation errors from the input form.
In the pages, there are some helpers existed for Freemarker and Thymeleaf to display the messages.
But when I come to Webflux and try to use RouterFunction to define the routing in the applications. For example,
Mono<ServerResponse> create(ServerRequest req) {
return req.bodyToMono(Post.class)
.flatMap { this.posts.save(it) }
.flatMap { ServerResponse.created(URI.create("/posts/".concat(it.getId()))).build() }
}
#Bean
RouterFunction<ServerResponse> routes(PostHandler postController) {
return route(GET("/posts"), postController.&all)
.andRoute(POST("/posts"), postController.&create)
.andRoute(GET("/posts/{id}"), postController.&get)
.andRoute(PUT("/posts/{id}"), postController.&update)
.andRoute(DELETE("/posts/{id}"), postController.&delete)
}
A possible approach is converting the request data(Mono or Flux) to blocking and injecting a Validator and validate them manually.
But I think the codes will look a little ugly.
How to process the validation of request body or form data gracefully?
Is there a better to validate the request body or form data and do not lost the functional and reactive features for both WEB(rendering a view) and REST applications?
I've developed "Yet Another Validator" for this porpose.
https://github.com/making/yavi
It would be great if YAVI could meet your expectation.
Validation code will look like following:
static RouterFunction<ServerResponse> routes() {
return route(POST("/"), req -> req.bodyToMono(User.class) //
.flatMap(body -> validator.validateToEither(body) //
.leftMap(violations -> {
Map<String, Object> error = new LinkedHashMap<>();
error.put("message", "Invalid request body");
error.put("details", violations.details());
return error;
})
.fold(error -> badRequest().syncBody(error), //
user -> ok().syncBody(user))));
}
One of the ways I've managed to do it in my application is the following (code is in Kotlin but the idea is the same). I've declared RequestHandler class which performs validation:
#Component
class RequestHandler(private val validator: Validator) {
fun <BODY> withValidBody(
block: (Mono<BODY>) -> Mono<ServerResponse>,
request: ServerRequest, bodyClass: Class<BODY>): Mono<ServerResponse> {
return request
.bodyToMono(bodyClass)
.flatMap { body ->
val violations = validator.validate(body)
if (violations.isEmpty())
block.invoke(Mono.just(body))
else
throw ConstraintViolationException(violations)
}
}
}
Request objects can contain java validation annotations in this way:
data class TokenRequest constructor(#get:NotBlank val accessToken: String) {
constructor() : this("")
}
And handler classes use RequestHandler to perform validation:
fun process(request: ServerRequest): Mono<ServerResponse> {
return requestHandler.withValidBody({
tokenRequest -> tokenRequest
.flatMap { token -> tokenService.process(token.accessToken) }
.map { result -> TokenResponse(result) }
.flatMap { ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(Mono.just(it), TokenResponse::class.java)
}
}, request, TokenRequest::class.java)
}
Got the idea from this blog post.

How to use WebSession in Spring WebFlux to persist data?

I am trying to develop web application using Spring WebFlux5.0.1 and Spring boot v2.0 M6 version. Requirement is to store objects in session and use it in subsequent pages/controllers.
Controller
#Controller
public class TestController {
#RequestMapping("/")
public Mono<String> testSession(Model model,ServerWebExchange swe){
Mono<WebSession> session = swe.getSession();
System.out.println("In testSession "+session);
model.addAttribute("account", new Account());
return Mono.just("account");
}
}
I was able to get Websession object from ServerWebExchange but i dont see methods to set/get attributes
Need help to understand how to use WebSession object in reactive world
Is it what you want to do ?
swe.getSession().map(
session -> {
session.getAttribute("foo"); // GET
session.getAttributes().put("foo", "bar") // SET
}
);
The accepted solution is incomplete in my opinion since it doesn't show the whole controller method, here it is how it would be done:
#PostMapping("/login")
fun doLogin(#ModelAttribute credentials: Credentials, swe: ServerWebExchange): Mono<String> {
return swe.session.flatMap {
it.attributes["userId"] = credentials.userId
"redirect:/globalPosition".toMono()
}
}

Resources