Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Creating Multiple RouterFunctions in Spring WebFlux

DZone's Guide to

Creating Multiple RouterFunctions in Spring WebFlux

Spring WebFlux makes it incredibly easy to define router functions across different domains. This guide will walk you through the process and the philosophy behind it.

· Java Zone ·
Free Resource

Download Microservices for Java Developers: A hands-on introduction to frameworks and containers. Brought to you in partnership with Red Hat.

In this post, we will be looking at defining multiple router functions to different logical domains in Spring WebFlux. This might not be a problem if you are creating microservices, as you will most likely only be working within a single domain for each service, but if you are not, then you will likely have the need to include multiple domains within your application that users or your own services can interact with. The code to do this is as simple as I hoped it would be and could be explained in a few sentences. To make this post a little more interesting, we will look at some of the Spring code that makes this all possible.

If you are new to WebFlux I recommend having a look at my previous post, Doing stuff with Spring WebFlux, where I wrote some thorough examples and explanations on the subject.

So let's set the scene first. You have two different domains within your application — say people and locations. You might want to keep them separated from each other, not only logically but also within your code. To do so, you need a way to define your routes in isolation from each other's domain. That is what we will look at in this post.

If you think you already know the answer to this problem, then you are probably right. It really is that simple. Let’s work our way up to it though. To create routes for just the people domain, create a RouterFunction bean that maps to the relevant handler functions, like below.

@Configuration
public class MyRouter {
    // works for a single bean
    @Bean
    public RouterFunction<ServerResponse> routes(PersonHandler personHandler) {
        return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get)
            .andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all)
            .andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post)
            .andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put)
            .andRoute(DELETE("/people/{id}"), personHandler::delete)
            .andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry);
    }
}


This creates the routes to the various handler functions in the PersonHandler.

So, now we want to add the routes for the location logic. We could simply add the routes to this bean, like below.

@Configuration
public class MyRouter {
    // not ideal!
    @Bean
    public RouterFunction<ServerResponse> routes(PersonHandler personHandler, LocationHandler locationHandler) {
        return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get)
            .andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all)
            .andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post)
            .andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put)
            .andRoute(DELETE("/people/{id}"), personHandler::delete)
            .andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry)
            .andRoute(GET("/locations/{id}").and(accept(APPLICATION_JSON)), locationHandler::get);
    }
}


The bean is now including a reference to the LocationHandler so the location route can be set up. The problem with this solution is that it requires the code to be coupled together. Furthermore, if you need to add even more handlers, you are soon going to be overwhelmed with the number of dependencies being injected into this bean.

The way around this is to create multiple RouterFunction beans. That’s it.

So, if we create one in the people domain, say PersonRouter and one in the location domain named LocationRouter, each can define the routes that they need and Spring will do the rest. This works because Spring goes through the application context and finds or creates any RouterFunction beans and consolidates them into a single function for later use.

Using this information we can write the code below.

@Configuration
public class PersonRouter {
    // solution
    @Bean
    public RouterFunction<ServerResponse> peopleRoutes(PersonHandler personHandler) {
        return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get)
            .andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all)
            .andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post)
            .andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put)
            .andRoute(DELETE("/people/{id}"), personHandler::delete)
            .andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry);
    }
}


and

@Configuration
public class LocationRouter {
    // solution
    @Bean
    public RouterFunction<ServerResponse> locationRoutes(LocationHandler locationHandler) {
        return RouterFunctions.route(GET("/locations/{id}").and(accept(APPLICATION_JSON)), locationHandler::get);
    }
}


PersonRouter can be kept with other people/person-related code, and LocationRouter can do the same.

To make this more interesting, why does this work?

RouterFunctionMapping is the class that retrieves all the RouterFunction beans created within the application context. The RouterFunctionMapping bean is created within WebFluxConfigurationSupport, which is the epicenter for Spring WebFlux configuration. By including the @EnableWebFlux annotation on a configuration class or by relying on auto-configuration, a chain of events starts and collecting all of our RouterFunctions is one of them.

Below is the RouterFunctionMapping class. I have removed its constructors and a few methods to make the snippet here a bit easier to digest.

public class RouterFunctionMapping extends AbstractHandlerMapping implements InitializingBean {

    @Nullable
    private RouterFunction<?> routerFunction;

    private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();

    // constructors

    // getRouterFunction

    // setMessageReaders

    @Override
    public void afterPropertiesSet() throws Exception {
        if (CollectionUtils.isEmpty(this.messageReaders)) {
            ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
            this.messageReaders = codecConfigurer.getReaders();
        }

        if (this.routerFunction == null) {
            initRouterFunctions();
        }
    }

    /**
      * Initialized the router functions by detecting them in the application context.
    */
    protected void initRouterFunctions() {
        if (logger.isDebugEnabled()) {
            logger.debug("Looking for router functions in application context: " +
                getApplicationContext());
        }

        List<RouterFunction<?>> routerFunctions = routerFunctions();
        if (!CollectionUtils.isEmpty(routerFunctions) && logger.isInfoEnabled()) {
            routerFunctions.forEach(routerFunction -> logger.info("Mapped " + routerFunction));
        }
        this.routerFunction = routerFunctions.stream()
            .reduce(RouterFunction::andOther)
            .orElse(null);
    }

    private List<RouterFunction<?>> routerFunctions() {
        SortedRouterFunctionsContainer container = new SortedRouterFunctionsContainer();
        obtainApplicationContext().getAutowireCapableBeanFactory().autowireBean(container);

        return CollectionUtils.isEmpty(container.routerFunctions) ? Collections.emptyList() :
            container.routerFunctions;
    }

    // getHandlerInternal

    private static class SortedRouterFunctionsContainer {

        @Nullable
        private List<RouterFunction<?>> routerFunctions;

        @Autowired(required = false)
        public void setRouterFunctions(List<RouterFunction<?>> routerFunctions) {
            this.routerFunctions = routerFunctions;
        }
    }
}


The path to retrieving all the routes starts in afterPropertiesSet, which is invoked after the RouterFunctionMapping bean is created. As its internal RouterFunction is null, it calls initRouterFunctions, triggering a series of methods leading to the execution of routerFunctions.

A new SortedRouterFunctionsContainer is constructed (private static class), setting itsrouterFunctions field by injecting all RouterFunctions from the application context. This works since Spring will inject all beans of type T when a List<T> is injected. The no- retrieved RouterFunctions are combined together to make a single RouterFunction that is used from now on to route all incoming requests to the appropriate handler.

That’s all there is to it. In conclusion, defining multiple RouterFunctions for different business domains is very simple, as you just create them in whatever area they most make sense, and Spring will go off and fetch them all. To demystify some of the magic, we looked into RouterFunctionMapping to see how the RouterFunctions we create are collected and combined so that they can be used to route requests to handlers. As a closing note, I do understand that this post, in some respects is quite trivial, but sometimes, the seemingly obvious information can be pretty helpful.

Download Building Reactive Microservices in Java: Asynchronous and Event-Based Application Design. Brought to you in partnership with Red Hat

Topics:
java ,spring webflux ,routerfunctions ,domains ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}