Wednesday, January 3, 2018

Spring Webflux - Part 3

Reactive Programming is the newest trend in programming world. In this article series of Spring Webflux I have been discussing on how to build a Reactive Application  using  Spring Webflux. In the Part 2 we have discussed how to build a simple reactive application using Spring Boot. Lets dig deep into more advanced routing methods and filter functions in this article.

Context Path for Application


In Spring MVC can give a context path to our application. In Spring Webflux we don't have a configuration for that. But we can set a context-path for multiple routes. Following is how we can do that. 

@Configuration
public class RouterWithContext {
    @Bean
    public RouterFunction<ServerResponse> routeWithContext(SampleHandler handler) {
        return nest(path("/context"),
                route(GET("/sample"), handler::handleNestedRoute));
    }
}

RouterFunctions.nest(...) will let you build nested routes in your application. It will take a RequestPredicate and a RouterFunction as arguments. Nested route is analogous to having a context path in Spring MVC.

Up to now we have just tried out HTTP GET Method in our application. What happen if we want to have request-body and we want to use it in our application. Lets do add a functionality to our application to accept a request-body and echo it back with some modifications. For this we will use a HTTP POST with a simple request body.

Our Request body should look as follows.

public class RequestBody {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

The handler function should first take out the request body and reshape the content and sends back the response. We can do it in imperative way using ServerRequest.bodyToMono(...).block() method which will give us the request-body. And the we can reshape it and generate the response. But since we are doing reactive programming using Mono.block() is an anti-pattern. What we can do is we can reshape the request-body in a more functional way.

    public Mono<ServerResponse> handleRequestWithABody(ServerRequest request) {
        return request
                .bodyToMono(RequestBody.class)
                .flatMap(requestBody -> {
                    ResponseBody responseBody = new ResponseBody();
                    responseBody.setStatus("Success");
                    responseBody.setDescription("Successfully Handled the request");
                    responseBody.setRequestBody(requestBody);
                    return ServerResponse.ok().body(fromObject(responseBody));
                })
                .switchIfEmpty(
                        ServerResponse.status(HttpStatus.BAD_REQUEST).build());
    }

We will first acquire the request-body as a stream using ServerRequest.bodyToMono(...). And then we can apply a 'flatmap' on that data-stream and reshape the content and get a data-stream. Finally there is some magic-code, the 'switchIfEmpty' which will check whether the data-stream is empty and send a default ServerResponse. Now we done with the handler-function. So, we have add the route now. This route has some specialty in it. This route consumes a request-body. So, it is preferable if we could mention the media-type which is accepted by this route. So, we can configure our route as follows.

route(POST("/echo").and(contentType(MediaType.APPLICATION_JSON)), handler::handleRequestWithABody);

As you can see here RequestPredicates can be concatenated and return a RequestPredicate. Here we have concatenated RequestPredicate which checks for HTTP Method and a RequestPredicate which checks for Content-Type Header.

Adding a Filter


Most often than not we want to secure our APIs. Spring Security has a more precise ways of securing an application, but here we want to have a simple key-base authentication for our application. For that we can use a simple filter function. First of all we have write an authenticator. Authenticator is a filter-function which will take ServerRequest and a HandlerFunction as arguments and return a ServerResponse.

@Component
public class Authenticator {
    @Value("${configuration.api.access-key}")
    private String apiKey;

    public Mono<ServerResponse> filterRoute(ServerRequest request, HandlerFunction<ServerResponse> handlerFunction){
        if (request.headers().asHttpHeaders().containsKey("x-api-key")) {
            if (request.headers().asHttpHeaders().get("x-api-key").get(0).equals(apiKey)) {
                return handlerFunction.handle(request);
            }
            return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
        } else {
            return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
        }
    }
}

Finally you can add the filter to the router.

    @Bean
    public RouterFunction<ServerResponse> routeWithContext(SampleHandler handler) {
        return nest(path("/context"),
                sampleRoute(handler)
                        .and(handleRequestBodyRoute(handler)))
                .filter(authenticator::filterRoute);
    }

You can find the sample code here.

No comments:

Post a Comment