Spring Functional Web Framework Guide

An in-depth guide to Spring 5 Functional Web Framework and its essential components like Router Functions, Handler Functions, Filter Functions, and compositions of functional routes.

Overview

This article provides a detailed overview of Spring Functional Web Framework. The functional web framework supports Java 8 functional style request routing, that can replace controller based request processing in Spring.

On a high level, in this article we will cover concepts like Router Functions and Handler Functions and how to create logical compositions of multiple functional routers. While doing so we will have a deep look at various interfaces involved and their methods. Also, we will have a look at nested compositions of routing functions along with applying request and response filters.

What is Functional Web Framework?

Spring 5 introduced WebFlux along with a Functional Web Framework. The Spring 5 Functional Web Framework allows functional style request routing and request handling. Interestingly, this functional style routing is an alternative to the traditional Controllers in a Sprig MVC or a Spring WebFlux application.

At the base of the Functional Web Framework are three interfaces – RouterFunction (doc), HandlerFunction (doc), and HandlerFilterFunction (doc). In the coming sections, we will have a detailed look at them.

In order to handle a request in functional way, we need to provide a router function along with some predicate. If the request matches to the predicate a handler associated with the router function is invoked. Else, the predicate on the next router is evaluated. Lastly, if none of the routers are matched, an empty result is returned.

Router Functions Vs Controller

With the Controllers, we use @RequestMapping (or @GetMapping, @PutMapping, etc.) on the controller methods to map specific requests to specific methods. On the other hand, with the Functional Web Framework, we create router functions to map requests with specific path to a specific handlers.

For example, next snippet shows how to write a Router Function that intercepts requests of a specific path and assign handler to accomplish them.

Sample Router Function

RouterFunctions.route(GET("/persons/{id}"),
    request -> {
        Mono<Person> person =
                personService.getPerson(parseLong(request.pathVariable("id")));
            return ServerResponse.ok()
                .body(BodyInserters.fromPublisher(person, Person.class));
    })
    .and(route(GET("/persons"),
            request -> {
                Flux<Person> people =
                        personService.getPersons();
                return ServerResponse.ok()
                        .body(BodyInserters.fromPublisher(people, Person.class));
            })
    )
    .and(route(POST("/persons"),
            request -> {
                request.body(BodyExtractors.toMono(Person.class))
                       .doOnNext(personService::addPerson);
                return ServerResponse.ok().build();
            })
    );Code language: Java (java)

As can be seen above, the router function can handle total three endpoints.

  • GET /persons
  • GET /persons/{id}
  • POST /persons

Once the router function is ready, we only need to create a @Bean factory method for it.

@Bean
RouterFunction<ServerResponse> getPersonServiceRoutes() {
   return RouterFunctions.route(GET("/persons/{id}"),
    // ... Skipped
}Code language: Java (java)

The the above @Bean factory method of a RouterFunction is equivalent to the next Spring WebFlux controller. Note that the next controller does exactly the same thing.

Equivalent WebFlux Controller

@RestController
public class PersonController {

    private PersonService personService;

    @GetMapping("/persons")
    public Flux<Person> getPersons() {
        return personService.getPersons();
    }

    @GetMapping("/persons/{id}")
    public Mono<Person> getPerson(@PathVariable Long id) {
        return personService.getPerson(id);
    }

    @PostMapping("/persons")
    public void addPerson(@RequestBody Person person) {
        personService.addPerson(person);
    }
}Code language: Java (java)

It is important to note that the Functional Web Framework is not only specific to the WebFlux. But, framework is part of Spring Web module that is used in both Spring MVC and Spring WebFlux. That means, we can use functional style routing in both of the web frameworks.

Handler Functions

Spring Functional Web Framework defines a functional interfaceHandlerFunction, which represents a function that handles request.

@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
    Mono<T> handle(ServerRequest var1);
}Code language: Java (java)

As the name denotes, the handler functions do the essential part of handling the request. Handler Functions take the server request, process, and generate Server Response. Also, note that the framework represents requests and response in the form of newly introduced interfaces – ServerRequest and ServerResponse respectively. Both of these interfaces supports builders and works well with Java 8 DSL.

HandlerFunction<ServerResponse> findHandler =
        request -> ServerResponse.ok().body(
                  service.find(request.queryParam("name")));Code language: Java (java)

The snippet shows a handler function which accepts a server request, extracts a query parameter, invokes a service and finally build and return a server response.

An handler is similar to the service method of Servlets. However, the service methods accepts both request and response. Thus, they can cause side effects. On the other hand, handlers produce response and that is why they are side effect free.

Router Functions

A router is represented in the form of a functional interface – RouterFunction. The responsibility of a router function is to map or route requests to handler functions.

@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
    Mono<HandlerFunction<T>> route(ServerRequest var1);

    default RouterFunction<T> and(RouterFunction<T> other) {
        return new SameComposedRouterFunction(this, other);
    }

    default RouterFunction<?> andOther(RouterFunction<?> other) {
        return new DifferentComposedRouterFunction(this, other);
    }

    default RouterFunction<T> andRoute(RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
        return this.and(RouterFunctions.route(predicate, handlerFunction));
    }

    default RouterFunction<T> andNest(RequestPredicate predicate, RouterFunction<T> routerFunction) {
        return this.and(RouterFunctions.nest(predicate, routerFunction));
    }

    default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) {
        return new FilteredRouterFunction(this, filterFunction);
    }

    default void accept(Visitor visitor) {
        visitor.unknown(this);
    }

    default RouterFunction<T> withAttribute(String name, Object value) {
        Assert.hasLength(name, "Name must not be empty");
        Assert.notNull(value, "Value must not be null");
        Map<String, Object> attributes = new LinkedHashMap();
        attributes.put(name, value);
        return new AttributesRouterFunction(this, attributes);
    }

    default RouterFunction<T> withAttributes(Consumer<Map<String, Object>> attributesConsumer) {
        Assert.notNull(attributesConsumer, "AttributesConsumer must not be null");
        Map<String, Object> attributes = new LinkedHashMap();
        attributesConsumer.accept(attributes);
        return new AttributesRouterFunction(this, attributes);
    }
}Code language: Java (java)

Let’s go over some of the methods of this interface now. Rest of the methods are covered in the following sections.

route()

The most important method from the interface is route(ServletRequest) that returns a publisher of handler type. We can map this method to @RequestMapping (or @GetMapping, @PostMapping, etc.) annotations in a controller. However, router functions are more flexible.

Annotation based request mapping puts limitation on the path expressions. On the other hand, with function based routing we can generate routes dynamically during startup. For example, by iterating through a collection or enum fields etc.

withAttribute() and withAttributes()

The withAttribute( name, value ) and withAttributes( attributeConsumer ) methods are used to create a new routing function with the given attribute(s).

The withAttribute() method that accepts a key and a value pair, use the pair to create and return AttributeRouterFunction. If we need to pass multiple key value pairs to a router, we can use the withAttributes() method. This method accepts a Consumer of type Map<String, Object>, which populates the given attribute map.

Compose Router Functions?

When there are a multiple combinations of request predicates and respective handlers, we can logically compose them together. For the ease of understanding, consider the Composing technique similar to having conditional predicates and handlers. Next are a different Composing techniques for routing functions.

using and()

We have already seen and example of and(route) function above. The and(route) function returns a composed routing function, which first invokes the this function. If the request path doesn’t match to the predicate of this route then the given route is evaluated.

route(path("/persons/{id}"),
    request -> ok().body(getPerson(request.pathVariable("id")), Person.class))
.and(route(path("/persons"),
    request -> ok().body(getPersons(), Person.class)));Code language: Java (java)

For example, if the request contains the path variable (Id), a single person is returned. Else, the other router is used and all persons are returned. If none of the predicates are matched an empty Optional is returned.

using andOther()

The andOther(RouterFunction other) is very much similar to and(RouterFunction other), except for the type of other router. This function returns a composed routing function hand includes both this router and the other router. If the predicate at this router is not matched, it will evaluate the other router.

The only difference it has with and() function is that the other router can have a different response type than this router.

using andRoute()

The andRoute(RequestPredicate predicate, HandlerFunction handlerFunction) adds more to the flexibility of the routing functions. It helps building logical routing that is based on the request predicates. It returns a composed routing function that first evaluates the this router. If there is no result and the predicate matches, it routes the request to the handlerFunction.

route(path("/persons").and(contentType(APPLICATION_JSON)),
    request -> ok().body(getPersonsV2(), Person.class))
.andRoute(path("/persons").and(contentType(APPLICATION_XML)),
    request -> ok().body(getPersons(), Person.class));Code language: Java (java)

For example, if there is a request with path “/persons” and JSON media type, this router is invoked. Otherwise, if request with the same path has XML media type, the handler provided by andRoute(..) is invoked.

Nested Routing Functions

In this section we will see, how to create nested routing functions. The Functional WebFramework provides RouterFunctions#nest() type of router that we can use to create logically nested routes and associated handlers.

The nest(RequestPredicate predicate, RouterFunction routerFunction) method, declares a predicate along with a routing function. Where the routing function can also be a composed routing function. Interestingly the predicate defined in the nest() will also be applicable to each of the composed routers that is nested within.

Nesting is useful when a multiple routes have common predicates. For example the andRoute() example in the previous section. Both of the handlers support the same path, but a different media type. Thus, we will rewrite the routing function using nesting.

nest(path("/persons"),
    route(contentType(APPLICATION_JSON),
        request -> ok().body(getPersonsV2(), Person.class))
    .andRoute(contentType(APPLICATION_XML),
        request -> ok().body(getPersons(), Person.class)));Code language: Java (java)

As can be seen in the snippet the nest() route declares the most common predicate. And the nested routes declare the media types which are different for them.

We can provide a further level of nesting by using RouterFunction#andNest() method. For example:

nest(predicate),
    nest(predicate, router)
        .andNest(predicate, router)
        .andNest(predicate, router)Code language: Java (java)

Request Filters

We can filter the routed requests and their responses using the RouterFunction$filter() method. Filter is an interceptor which surrounds an invocation of a handler. Thus, they are useful in variety of scenarios like logging, caching, parsing, etc.

The filter method accepts a BiConsumer, which takes two arguments and returns one. The function receives the request and the handler objects and it needs to return a response. Based on the inputs, the filter can chose to invoke the handler and return its response. Else, it can skip calling the handler and return something else.

Next is an example of adding Filter on Functional Routes.

route(path("/persons"),
    request -> ok().body(getPersonsV2(), Person.class))
.filter((request, next) -> {
    log.info("Before handler, {}", request.queryParams());
    Mono<ServerResponse> result = next.handle(request);
    log.info("After handler");
    return result;
});Code language: Java (java)

In the snippet, the filter function just logs the request query parameters, before invoking the handler.

Define Each Route Separately

So far, we have seen a different ways to compose different routing functions. The routing function compositions are extremely flexible, as they can be formed dynamically during startup.

However in the most simplest form we can also define each of these routing functions separately, in a separate factory methods.

@Bean
public RouterFunction<ServerResponse> getPersonsRoute() {
    return route(path("/persons"),
        req -> ok().body(personService.getPersons(), Person.class));
}

@Bean
public RouterFunction<ServerResponse> getPersonsByIdRoute() {
    return route(path("/persons/{id}"),
        req ->
          ok()
         .body(personService.getPerson(req.pathVariable("id")), Person.class));
}

@Bean
public RouterFunction<ServerResponse> addPersonRoute() {
    return route(POST("/persons"),
        req -> {
            req.body(BodyExtractors.toMono(Person.class))
              .doOnNext(personService::addPerson);
            return ServerResponse.ok().build();
        }
    );
}Code language: Java (java)

There are three @Bean factory methods in the snippet. Each of the method is defining an independent router function.

Summary

In this detailed tutorial we had a complete overview of Spring 5 Functional Web Framework. Doing so, we learned the concepts of Routing Functions or Functional Routes and also learned the difference between Routing Function and Controller.

Also we visited two most important interfaces – RouterFunction and HandlerFunction and understood each of their methods. Most importantly, we leaned how to Compose multiple router functions together and form a logical group of routers and respective handlers.

To summarize, the functional web framework leverages Java 8 style of DSL to provide a flexible way of handling requests. Routing functions help to route specific requests to a specific request handlers handlers and we can easily form logical compositions of router functions.