Purely functional web with WebFlux

As we might have noticed in the preceding diagram, while it has a lot of similarities to Web MVC, WebFlux also provides a lot of new features. In the era of tiny microservices, Amazon Lambdas, and similar cloud services, it is important to offer functionality that allows developers to create lightweight applications that have almost the same arsenal of framework featuresOne of the features that made competitor frameworks, such as Vert.x or Ratpack, more attractive was their ability to produce lightweight applications, which was achieved by functional route mapping and a built-in API that allowed us to write complex request routing logic. This is why the Spring Framework team decided to incorporate this feature into the WebFlux module. Moreover, the combination of pure functional routing fits sufficiently with new reactive programming approaches. For example, let's take a look at how to build complex routing using the new functional approach:

import static ...RouterFunctions.nest;                             // (1)
import static ...RouterFunctions.nest; //
import static ...RouterFunctions.route; //
...
import static ...RequestPredicates.GET; // (2)
import static ...RequestPredicates.POST; //
import static ...RequestPredicates.accept; //
import static ...RequestPredicates.contentType; //
import static ...RequestPredicates.method; //
import static ...RequestPredicates.path; //

@SpringBootApplication // (3)
public class DemoApplication { //
...
@Bean
public RouterFunction<ServerResponse> routes( // (4)
OrderHandler handler // (4.1)
) { //
return
nest(path("/orders"), // (5)
nest(accept(APPLICATION_JSON), //
route(GET("/{id}"), handler::get) //
.andRoute(method(HttpMethod.GET), handler::list) //
) //
.andNest(contentType(APPLICATION_JSON), //
route(POST("/"), handler::create) //
) //
);
}
}

The preceding code can be explained as follows:

  1. This is a declaration of static imports from the RouterFunctions class. As we can see, the RouterFunctions class provides an extensive list of factory methods that return RouterFunction interfaces with different behaviors.
  2. This is a declaration of static imports from the RequestPredicates class. As we can see from the preceding code, the RequestPredicates class allows to check an incoming request from a different perspective. In general, RequestPredicates provides access to a different implementation of the RequestPredicate interface, which is a functional interface and may be extended easily in order to verify incoming requests in a custom way.
  3. This is a common declaration of the Spring Boot application, whose class is annotated with @SpringBootApplication.
  4. This is a method declaration that initializes the RouterFunction<ServerResponse> bean. In this example, the method is invoked during the bootstrapping of an application.
  5. This is a declaration of RouterFunction, expressed with the support of the RouterFunctions and RequestPredicates APIs.

In the preceding example, we used an alternative way of declaring an application's web API. This technique provides a functional method for handler declaration and allows us to keep all routes explicitly defined in one place. In addition, an API such as the one used previously allows us to write our own request predicates easily. For example, the following code shows how to implement a custom RequestPredicate and apply it to the routing logic:

nest((serverRequest) -> serverRequest.cookies()
.containsKey("Redirect-Traffic"),
route(all(), serverRedirectHandler)
)

In the previous example, we created a small RouterFunction, which redirects traffic to another server if the "Redirect-Traffic" cookie is present.

The new functional web also introduced a new way of dealing with requests and responses. For example, the following code sample shows part of the OrderHandler implementation:

class OrderHandler {                                               // (1)
final OrderRepository orderRepository; //
...
public Mono<ServerResponse> create(ServerRequest request) { // (2)
return request //
.bodyToMono(Order.class) // (2.1)
.flatMap(orderRepository::save) //
.flatMap(o -> //
ServerResponse.created(URI.create("/orders/" + o.id)) // (2.2)
.build() //
); //
} //
... //
} //

The preceding code can be described as follows:

  1. This is the OrderHandler class declaration. In this example, we skip the constructor declaration in order to focus on the API of the functional routes.
  2. This is the create method declaration. As we can see, the method accepts ServerRequest, which is specific to the functional routes request type. As we can see at point (2.1), ServerRequest exposes the API, which allows the manual mapping of the request body either to Mono or Flux. In addition, the API allows us to specify the class to which the request body should be mapped. Finally, the functional addition in WebFlux offers an API that allows us to construct a response using the fluent API of the ServerResponse class.

As we can see, in addition to the API for functional route declaration, we have a functional API for request and response processing.

Even though the new API gives us a functional approach to declaring the handler and mapping, it does not give us a fully lightweight web application. There are some cases where the whole functionality of the Spring ecosystem may be redundant, therefore decreasing the overall startup time of the application. For example, suppose that we have to build a service that is responsible for matching user passwords. Usually, such a service burns a lot of CPU by hashing the incoming password and then comparing it with the stored password. The only functionality that we need is the PasswordEncoder interface from the Spring Security module, which allows us to compare the encoded password with the raw password using the PasswordEncoder#matchs method. Therefore, the whole Spring infrastructure with IoC, annotation processing, and autoconfiguration is redundant and makes our application slower in terms of startup time.

Fortunately, the new functional web framework allows us to build a web application without starting the whole Spring infrastructure. Let's consider the following example in order to understand how we can achieve this:

class StandaloneApplication {                                      // (1)

public static void main(String[] args) { // (2)
HttpHandler httpHandler = RouterFunctions.toHttpHandler( // (2.1)
routes(new BCryptPasswordEncoder(18)) // (2.2)
); //
ReactorHttpHandlerAdapter reactorHttpHandler = // (2.3)
new ReactorHttpHandlerAdapter(httpHandler); //

HttpServer.create() // (3)
.port(8080) // (3.1)
.handle(reactorHttpHandler) // (3.2)
.bind() // (3.3)
.flatMap(DisposableChannel::onDispose) // (3.4)
.block(); //
} //

static RouterFunction<ServerResponse> routes( // (4)
PasswordEncoder passwordEncoder //
) { //
return
route(POST("/check"), // (5)
request -> request //
.bodyToMono(PasswordDTO.class) // (5.1)
.map(p -> passwordEncoder //
.matches(p.getRaw(), p.getSecured())) // (5.2)
.flatMap(isMatched -> isMatched // (5.3)
? ServerResponse //
.ok() //
.build() //
: ServerResponse //
.status(HttpStatus.EXPECTATION_FAILED) //
.build() //
) //
); //
}
}

The following numbered list describes the preceding code sample:

  1. This is the declaration of the main application class. As we can see, there is no additional annotation from Spring Boot.
  2. Here, we have the declaration of the main method with the initialization of the required variables. At point (2.2), we invoke the routes method and then convert RouterFunction to HttpHandler. Then, at point (2.3), we use the built-in HttpHandler adapters called ReactorHttpHandlerAdapter
  3. At this point, we create an HttpServer instance, which is a part of the Reactor-Netty API. Here, we use a fluent API of the HttpServer class in order to set up the server. At point (3.1), we declare the port, put the created instance of ReactorHttpHandlerAdapter (at point 3.2), and, by calling bind at point (3.3), start the server engine. Finally, in order to keep the application alive, we block the main Thread and listen for the disposal event of the created server at point (3.4).
  4. This point shows the declaration of the routes method.
  1. This is the routes mapping logic, which handles a request for any POST methods with the /check path. Here, we start with the mapping of incoming requests with the support of the bodyToMono method. Then, once the body is converted, we use a PasswordEncoder instance in order to check the raw password against the encoded one (in our case, we use a strong BCrypt algorithm with 18 rounds of hashing, which may take a few seconds for encoding/matching) (5.2). Finally, if the password matches the stored one, ServerResponse returns with a status of either OK(200) or EXPECTATION_FAILED(417) if the password does not match the stored one.

The preceding example shows how easily we can set up a web application without having to run the whole Spring Framework infrastructure. The benefit of such a web application is that its startup time is much shorter. The startup time of the application is around ~700 milliseconds, whereas the startup process for the same application with the Spring Framework and Spring Boot infrastructure takes up to ~2 seconds (~2,000 milliseconds), which is approximately three times slower.

Note that startup times may vary, but the overall proportion should be the same.

To summarize the routing declaration technique by switching to a functional route declaration, we maintain all routing configuration in one place and use a reactive approach for incoming request processing. At the same time, such a technique offers almost the same flexibility as the usual annotation-based approach in terms of accessing incoming request parameters, path variables, and other important components of the request. It also provides us with the ability to avoid the whole Spring Framework infrastructure being run and has the same flexibility in terms of route setup, which may decrease the bootstrapping time of the application by up to three times.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.34.39