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.
Join the DZone community and get the full member experience.
Join For FreeIn 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 RouterFunction
s 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 RouterFunction
s from the application context. This works since Spring will inject all beans of type T
when a List<T>
is injected. The no- retrieved RouterFunction
s 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 RouterFunction
s 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 RouterFunction
s 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.
Published at DZone with permission of Dan Newton, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments