1. Introduction
One of the main new features of Spring 5 will be a new Functional Web Framework built using reactive principles.
In this article, we’ll have a look on how it looks like in practice.
2. Maven Dependency
Let’s start by defining the Maven dependencies that we’re going to need:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.BUILD-SNAPSHOT</version> <relativePath/> </parent> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>
As we are using a snapshot version of Spring Boot, remember to add Spring snapshot repository:
<repositories> <repository> <id>repository.spring.snapshot</id> <name>Spring Snapshot Repository</name> <url>http://repo.spring.io/snapshot</url> </repository> </repositories>
3. Functional Web Framework
Before we start to look at what the new framework provides, it’s a good idea to read this article to brush up on the basics of the Reactive paradigm.
The framework introduces two fundamental components: HandlerFunction and RouterFunction, both located in the org.springframework.web.reactive.function.server package.
3.1. HandlerFunction
The HandlerFunction represents a function that handles incoming requests and generates responses:
@FunctionalInterface public interface HandlerFunction<T extends ServerResponse> { Mono<T> handle(ServerRequest request); }
This interface is primarily a Function<Request, Response<T>>, which is very much like a servlet. Compared to a standard servlet Servlet.service(ServletRequest req, ServletResponse res), a side-effect free HandlerFunction is naturally easier to test and reuse.
3.2. RouterFunction
RouterFunction serves as an alternative to the @RequestMapping annotation. It’s used for routing incoming requests to handler functions:
@FunctionalInterface public interface RouterFunction<T extends ServerResponse> { Mono<HandlerFunction<T>> route(ServerRequest request); // ... }
Typically, we can import RouterFunctions.route(), a helper function to create routes, instead of writing a complete router function. It allows us to route requests by applying a RequestPredicate. When the predicate is matched, then the second argument, the handler function, is returned:
public static <T extends ServerResponse> RouterFunction<T> route( RequestPredicate predicate, HandlerFunction<T> handlerFunction)
By returning a RouterFunction, route() can be chained and nested to build powerful and complex routing schemes.
3.3. A Quick Example
Here’s a simple example that serves requests to the root path “/” and returns a response with status 200:
RouterFunction<ServerResponse> routingFunction() { return route(path("/"), req -> ok().build()); }
The path(“/”) in the example above is a RequestPredicate. Besides matching a path, there’s a bunch of other commonly used predicates available in RequestPredicates, including HTTP methods, content type matching, etc.
3.4. Running on a Server
To run the functions in a server, we can wrap the RouterFunction in an HttpHandler for serving requests. The HttpHandler a reactive abstraction introduced in Spring 5.0 M1. With this abstraction, we can run our code in reactive runtimes such as Reactor Netty and Servlet 3.1+.
With the following code, we can run the functions in an embedded Tomcat:
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routingFunction()); Tomcat tomcat = new Tomcat(); Context rootContext = tomcat.addContext("", System.getProperty("java.io.tmpdir")); ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(httpHandler); HttpServlet servlet = new ServletHttpHandlerAdapter(httpHandler); Tomcat.addServlet(rootContext, "httpHandlerServlet", servlet); rootContext.addServletMappingDecoded("/", "httpHandlerServlet"); TomcatWebServer server = new TomcatWebServer(tomcat); server.start();
We can also deploy the functions in a standalone Servlet container that supports Servlet 3.1+, such as Tomcat 9.
Similar to the code above, we can wrap the RouterFunction in a servlet that extends ServletHttpHandlerAdapter, say com.baeldung.functional.RootServlet:
public class RootServlet extends ServletHttpHandlerAdapter { public RootServlet() { this(WebHttpHandlerBuilder .webHandler(toHttpHandler(routingFunction())) .prependFilter(new IndexRewriteFilter()) .build()); } RootServlet(HttpHandler httpHandler) { super(httpHandler); } //... }
Then register the Servlet as a bean:
@Bean public ServletRegistrationBean servletRegistrationBean() throws Exception { HttpHandler httpHandler = WebHttpHandlerBuilder .webHandler(toHttpHandler(routingFunction())) .prependFilter(new IndexRewriteFilter()) .build(); ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(new RootServlet(httpHandler), "/"); registrationBean.setLoadOnStartup(1); registrationBean.setAsyncSupported(true); return registrationBean; }
Or if you prefer a plain web.xml:
<servlet> <servlet-name>functional</servlet-name> <servlet-class>com.baeldung.functional.RootServlet</servlet-class> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>functional</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
As we are using spring-boot-starter, after excluding the embedded tomcat: spring-boot-starter-tomcat included by spring-boot-starter-web; and adding a provided dependency javax.servlet.javax.servlet-api, we are good to go.
4. Testing
A new way of testing for the functional web framework is also introduced in Spring 5. WebTestClient serves as the basis of spring-webflux integration testing support.
With the help of bindToRouterFunction() provided by WebTestClient, we can test the functions without starting an actual server:
WebTestClient client = WebTestClient .bindToRouterFunction(routingFunction()) .build(); client.get().uri("/").exchange().expectStatus().isOk();
If we already have a server running, use bindToServer() to test via socket:
WebTestClient client = WebTestClient .bindToServer() .baseUrl("http://localhost:8080") .build(); client.get().uri("/").exchange().expectStatus().isOk();
5. Conventional Web Request Patterns
Now that we have a basic understanding of the framework’s key components let’s see how they would fit in conventional web request patterns.
5.1. A Simple GET API
A simple HTTP GET API that returns “hello world”:
RouterFunction router = route(GET("/test"), request -> ok().body(fromObject("hello world"))); @Test public void givenRouter_whenGetTest_thenGotHelloWorld() throws Exception { client.get().uri("/test").exchange() .expectStatus().isOk() .expectBody(String.class).value().isEqualTo("hello world"); }
5.2. POST a Form
Posting a login form:
RouterFunction router = route(POST("/login"), request -> request.body(toFormData()).map(MultiValueMap::toSingleValueMap) .map(formData -> { if ("baeldung".equals(formData.get("user")) "you_know_what_to_do".equals(formData.get("token"))) { return ok() .body(Mono.just("welcome back!"), String.class) .block(); } return badRequest().build().block(); })); @Test public void givenLoginForm_whenPostValidToken_thenSuccess() throws Exception { MultiValueMap<String, String> formData = new LinkedMultiValueMap<>(2); formData.add("user", "baeldung"); formData.add("token", "you_know_what_to_do"); client.post().uri("/login") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .exchange(BodyInserters.fromFormData(formData)) .expectStatus().isOk() .expectBody(String.class) .value().isEqualTo("welcome back!"); }
Then a form with multipart data:
RouterFunction router = route(POST("/upload"), request -> request.body(toDataBuffers()).collectList() .map(dataBuffers -> { AtomicLong atomicLong = new AtomicLong(0); dataBuffers.forEach(d -> atomicLong.addAndGet(d.asByteBuffer().array().length)); return ok().body(fromObject(atomicLong.toString())).block(); })); @Test public void givenUploadForm_whenRequestWithMultipartData_thenSuccess() throws Exception { Resource resource = new ClassPathResource("/baeldung-weekly.png"); client.post().uri("/upload").contentType(MediaType.MULTIPART_FORM_DATA) .exchange(fromResource(resource)) .expectStatus().isOk() .expectBody(String.class) .value().isEqualTo(String.valueOf(resource.contentLength())); }
As you may notice, the multipart processing is blocking. For now, a non-blocking multipart implementation is still under investigation by the Spring team. You can track this issue for further updates.
5.3. RESTful API
We can also manipulate Resources in a RESTful API:
List<Actor> actors = new CopyOnWriteArrayList<>( Arrays.asList(BRAD_PITT, TOM_HANKS)); RouterFunction router = nest(path("/actor"), route(GET("/"), request -> ok().body(Flux.fromIterable(actors), Actor.class)) .andRoute(POST("/"), request -> request.bodyToMono(Actor.class).doOnNext(actors::add) .then(ok().build()))); @Test public void givenActors_whenAddActor_thenAdded() throws Exception { client.get().uri("/actor") .exchange() .expectStatus().isOk() .expectBody(Actor.class).list().hasSize(2); client.post().uri("/actor") .exchange(fromObject(new Actor("Clint", "Eastwood"))) .expectStatus().isOk(); client.get().uri("/actor") .exchange() .expectStatus().isOk() .expectBody(Actor.class).list().hasSize(3); }
As mentioned previously, RouterFunctions and RouterFunction gives us options on chaining and nesting route functions.
In this example, we nested two router functions to separately handle GET and POST request under the path “/actor”.
5.4. Serve Static Resources
RouterFunctions also provides a shortcut to serve static files:
RouterFunction router = resources( "/files/**", new ClassPathResource("files/")); @Test public void givenResources_whenAccess_thenGotContentHello() throws Exception { this.client.get().uri("/files/hello.txt") .exchange() .expectStatus().isOk() .expectBody(String.class).value().isEqualTo("hello"); }
5.5. Filters
RouterFunction allows filtering handler functions:
router.filter((request, next) -> { System.out.println("handling: " + request.path()); return next.handle(request); });
However, the filter above only applies to all handler functions routed by the router. When an URL is not explicitly handled, requests to such URLs will not go through the filter. Say we want to add URL rewriting features in such cases, router’s filter would do no help.
Currently, if we want to rewrite URLs in filters, we have to do it in a WebFilter, instead of the router’s filter:
WebHandler webHandler = toHttpHandler(routingFunction()); HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(webHandler) .prependFilter(((serverWebExchange, webFilterChain) -> { ServerHttpRequest request = serverWebExchange.getRequest(); if (request.getURI().getPath().equals("/")) { return webFilterChain.filter( serverWebExchange.mutate().request(builder -> builder .method(request.getMethod()) .contextPath(request.getContextPath()) .path("/test")) .build()); } else { return webFilterChain.filter(serverWebExchange); } })); @Test public void givenIndexFilter_whenRequestRoot_thenRewrittenToTest() throws Exception { this.client.get().uri("/").exchange() .expectStatus().isOk() .expectBody(String.class).value().isEqualTo("hello world"); }
6. Summary
In this article, we introduced the new functional web framework in Spring 5, with detailed examples showing how the framework works in typical scenarios.
Note that as of Spring 5.0.0.M5, @RequestMapping and RouterFunction cannot be mixed in the same application yet.
Laying its foundation on Reactor, the reactive framework would fully shine with reactive access to data stores. Unfortunately, most data stores do not provide such reactive access yet, except for a few NoSQL databases such as MongoDB.
As always, the full source code can be found over on Github.