Reactive Java with Spring WebFlux and Reactor
Reactive Java with Spring WebFlux and Reactor
Reactive programming is an important coding style that evolved from the functional world. Reactive code utilizes an event-driven philosophy of streams, producers, and subscribers to simplify complex logic and enable asynchronous, nonblocking handling of IO processing in applications. In Java, this means we can build applications using the java.nio
(nonblocking IO) package with expressive APIs. Many frameworks and approaches support reactivity in Java. One of the most popular is Spring WebFlux. This article is a hands-on introduction to reactive Java programming with Spring WebFlux.
Reactivity with Spring
Reactivity gives us a powerful single idiom for describing and combining functionality like web requests and data access. In general, we use event producers and subscribers to describe asynchronous event sources and what should happen when an event is activated.
In the typical Spring framework style, WebFlux provides an abstraction layer for building reactive web components. That means you can use a couple of different underlying reactive implementations. The default is Reactor, which we’ll use for the demonstration.
To begin, we’ll initialize a new application with the Spring command-line tool. There are a few ways to install this tool, but I like using SDKMan. You’ll need to have Java 17+ installed. You can find the instructions for installing SDKMan for your operating system here. Once installed you can add the Spring CLI with: $ sdk i springboot
. Now the command $ spring --version
should work.
To start the new application, type:
$ spring init --dependencies=webflux --build=maven --language=java spring-reactive
Next, cd
into the spring-reactive
directory. Spring has created a bare-bones layout for us, including a main class at src/main/java/com/example/springreactive2/DemoApplication.java
.
Let’s modify this class to add a reactive endpoint handler, as shown in Listing 1.
Listing 1. Adding a RESTful endpoint
package com.example.springreactive; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @RestController public class EchoController { @GetMapping("/hello") public Mono<String> hello() { return Mono.just("Hello, InfoWorld!"); } @GetMapping("echo/{str}") public Mono<String> echo(@PathVariable String str) { return Mono.just("Echo: " + str); } @GetMapping("echoquery") public Mono<String> echoQuery(@RequestParam("name") String name) { return Mono.just("Hello, " + name); } } }
Note: In the Reactor library, Mono
is a type that refers to a “monadic value.”
The @SpringBootApplication
took care of much of the configuration for us. We use an inner class called EchoController
with the @RestController
annotation to let Spring know what endpoints we’re using.
There are three examples in Listing 1, each mapped to a URL path with @GetMapping
. The first, /hello
, simply writes a greeting to the response. The second, /echo/{str}
shows how to take a URL parameter (a path variable) and use it in the response. And the third,/echoquery
, shows how to grab a request parameter (the values in the URL after a question mark) and use it.
In each case, we rely on the Mono.just()
method to describe the response. This is a simple way to create an event producer in the Reactor framework. It says: make an event producer with a single event, found in the argument, and hand it off to all subscribers. In this case, the subscribers are handled by the Spring WebFlux framework and the nonblocking server hosting it. In short, we get access to an entirely nonblocking pipeline to the response.
The inbound request processing is also built entirely on nonblocking IO. This makes scaling the server potentially very efficient because there are no blocking threads to limit concurrency. Especially in real-time systems, nonblocking IO can be very important to overall throughput.
Spring WebFlux uses the Netty server by default. If you prefer, you can use another server like Undertow or a Servlet 3.1 container like Tomcat. See the WebFlux documentation for more about server options.
Reactive programming by example
Reactive programming entails a whole mindset and set of concepts, which we won’t explore here. Instead, we’ll work through a few examples that expose the critical aspects of this programming style.
To start, let’s make an endpoint that accepts a file upload post and writes the contents to disk. You can see this method, along with its imports, in Listing 2. The remainder of the code stays the same.
Listing 2. Accepting and writing a file
import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.FilePart; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.util.FileSystemUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @PostMapping(value = "/writefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Mono<String> writeFile(@RequestPart("file") Flux<FilePart> filePartFlux) { Path path = Path.of("/tmp/file.txt"); // Delete the existing file if it already exists FileSystemUtils.deleteRecursively(path.toFile()); // Save the file parts to the specified path return filePartFlux .flatMap(filePart -> filePart.transferTo(path)) .then(Mono.just("File saved: " + path.toString())); }
The writeFile()
method is annotated with @PostMapping
, and this is configured to accept a multipart form upload. So far, this is a normal Spring Web configuration. WebFlux lets us use the @RequestPart
annotation in the method argument with a type of Flux<FilePart>
. This lets us accept the multipart chunks in a reactive, nonblocking way with Flux.
With the filePartFlux
in hand, we can make use of the reactive flatMap
method to write it to disk: filePartFlux.flatMap(filePart -> filePart.transferTo(path))
. Each “event” of the multipart file is handed to the transferTo
function to be added to the file. This is a very idiomatic reactive operation.
The use of higher-order functions like flatMap
to transform and process event streams is typical of reactive programming. It can be seen as having event producers, subscribers, and transformers like flatMap
. By taking one or many streams and manipulating them with chains of metafunctions, you can achieve powerful effects with relatively simple syntax.
To test the new endpoint, you can use a CURL command, as shown in Listing 3.
Listing 3. Test the writeFile endpoint with CURL
$ echo "Facing it, always facing it, that’s the way to get through." >> testfile.txt $ curl -X POST -F "file=@./testfile.txt" http://localhost:8080/writefile File saved: /tmp/file.txtmatthewcarltyson@dev3:~/spring-reactive2 $ cat /tmp/file.txt
In Listing 3, we create a testfile.txt
file with some content (“Facing it, always facing it, that’s the way to get through”), then send it to the endpoint, receive the response, and verify the new file contents.
Using the reactive HTTP client
Now, let’s make an endpoint that accepts an ID parameter. We’ll use the Spring reactive HTTP client to make a request for the character at that ID at the SWAPI (Star Wars API), then we’ll send the character data back to the user. You can see this new apiChain()
method and its imports in Listing 4.
Listing 4. Using the reactive web client
import org.springframework.web.reactive.function.client.WebClient; @GetMapping("character/{id}") public Mono<String> getCharacterData(@PathVariable String id) { WebClient client = WebClient.create("https://swapi.dev/api/people/"); return client.get() .uri("/{id}", id) .retrieve() .bodyToMono(String.class) .map(response -> "Character data: " + response); }
Now if you navigate to localhost:8080/character/10
, you’ll get the biographical information for Obi-Wan Kenobi.
Listing 4 works by accepting the ID path parameter and using it to make a request to the WebClient
class. In this case, we are creating an instance for our request, but you can create a WebClient
with a base URL and then reuse it repeatedly with many paths. We put the ID into the path and then call retrieve
followed by bodyToMono()
, which transforms the response into a Mono. Remember that this all remains nonblocking and asynchronous, so the code that waits for the response from SWAPI will not block the thread. Finally, we use map()
to formulate a response back to the user.
The overall effect of all these examples is to enable a high-performance stack from the server all the way to your code, with minimal fuss.
Conclusion
Reactive programming is a different way of thinking from the more familiar declarative style, which can make it more difficult to reason about simple scenarios. It is also harder to find programmers who understand reactive programming. In general, good technical or business requirements should determine whether you use a reactive framework versus a more standard framework.