Spring 5 WebFlux functional programming model

Spring 5 has the new module WebFlux to create reactive web applications. Spring WebFlux supports two programming models: traditional annotation-based programming model and functional programming model. In this post, we’ll going to scratch the surface of the new functional programming model. This post assumes that you already know concepts like Flux and Mono in Reactor.

We’ll use a calculator service as the example. This service supports operations like add, subtract, divide and multiply with two operands. The parameter operator specifies the operation to call, while parameters v1 and v2 specify the two operands. For example, the url /calculator?operator=add&v1=1&v2=2 invokes the add operation with values 1 and 2.

Basic setup

The easiest way to create a WebFlux project is using Spring Initializr. We create a Maven-based Java project with Spring Boot 2.0.0-M5 and add Reactive Web as the dependency.

Handler function

Handler functions are responsible for handling incoming HTTP requests. Each handler function takes a ServerRequest and returns a Mono<ServerResponse>. Multiple handler functions for the same service are typically organized in the same class.

In the CalculatorHandler listed below, we have different methods add, subtract, multiply and divide to handle different requests. These methods use calculate to parse the request and calculate the result.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Component
public class CalculatorHandler {

public Mono<ServerResponse> add(final ServerRequest request) {
return calculate(request, (v1, v2) -> v1 + v2);
}

public Mono<ServerResponse> subtract(final ServerRequest request) {
return calculate(request, (v1, v2) -> v1 - v2);
}

public Mono<ServerResponse> multiply(final ServerRequest request) {
return calculate(request, (v1, v2) -> v1 * v2);
}

public Mono<ServerResponse> divide(final ServerRequest request) {
return calculate(request, (v1, v2) -> v1 / v2);
}

private Mono<ServerResponse> calculate(final ServerRequest request,
final BiFunction<Integer, Integer, Integer> calculateFunc) {
final Tuple2<Integer, Integer> operands = extractOperands(request);
return ServerResponse
.ok()
.body(Mono.just(calculateFunc.apply(operands.getT1(), operands.getT2())), Integer.class);
}

private Tuple2<Integer, Integer> extractOperands(final ServerRequest request) {
return Tuples.of(parseOperand(request, "v1"), parseOperand(request, "v2"));
}

private int parseOperand(final ServerRequest request, final String param) {
try {
return Integer.parseInt(request.queryParam(param).orElse("0"));
} catch (final NumberFormatException e) {
return 0;
}
}
}

Router function

Router functions are responsible for routing the incoming HTTP requests to the correct handler function. Each router function takes a ServerRequest, and returns a Mono<HandlerFunction>.

In the bean of type RouterFunction created by routerFunction, we only match the request path /calculator. We extract the parameter operator from the request and use reflections to find the correct handler function in CalculatorHandler to handle the request. We can see that Reactor makes the error handling very easy to implement and understand.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class Config {

@Bean
@Autowired
public RouterFunction<ServerResponse> routerFunction(final CalculatorHandler calculatorHandler) {
return RouterFunctions.route(RequestPredicates.path("/calculator"), request ->
request.queryParam("operator").map(operator ->
Mono.justOrEmpty(
ReflectionUtils.findMethod(CalculatorHandler.class, operator, ServerRequest.class))
.flatMap(method -> (Mono<ServerResponse>) ReflectionUtils
.invokeMethod(method, calculatorHandler, request))
.switchIfEmpty(ServerResponse.badRequest().build())
.onErrorResume(
ex -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build()))
.orElse(ServerResponse.badRequest().build()));
}
}

Test

We can test this service using TestRestTemplate provided by Spring Boot.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class FunctionalStarterApplicationTests {

@Autowired
private TestRestTemplate restTemplate;

@Test
public void calculate() {
assertThat(doCalculate("add", "3", "5")).isEqualTo("8");
assertThat(doCalculate("subtract", "3", "5")).isEqualTo("-2");
assertThat(doCalculate("multiply", "3", "5")).isEqualTo("15");
assertThat(doCalculate("divide", "9", "3")).isEqualTo("3");
}

private String doCalculate(final String operator, final String operand1, final String operand2) {
return this.restTemplate.getForObject(
String.format("/calculator?operator=%s&v1=%s&v2=%s", operator, operand1, operand2),
String.class);
}
}

Source code

Full source code can be found on GitHub VividcodeIO/spring5-webflux-functional-starter.

Comments