Spring Boot: migrating to WebFlux

10 min read
July 15, 2020

Despite the proponents of traditional programming, the rise of reactive programming is not intended to stop. The load on modern systems is constantly increasing and at some point causes the need to change the way load control is maintained to keep the system responsive.

This article can serve as a basis for creating new applications or as an example migrating existing microservices.

Servlet vs. Reactive stack

Non-reactive applications are thread pool based so they are more vulnerable to overload. 

An instantaneous solution to this problem may be to increase the size of the thread pool or to shorten the time between service calls.

Over time, the non-profitability of such a method would come to light. Switching to a non-blocking asynchronous approach would be a longer-term solution.

The servlet stack

supports Tomcat and Jetty servers but accesses them via the Servlet API with blocking I / O, which allows the application to be blocked. For the same reason, the servlet stack requires more threads.

The reactive stack

uses the Netty server by default, but it also supports Tomcat and Jetty at the same time. The application on the reactive stack must not be blocked due to the small number of threads rotating in the event loop.

Although Spring MVC does not support reactive streams, it does try to embrace reactive principles to a greater extent in recent versions.

Reactive web clients for external calls that provide a variety of uses without intensive code refactoring can be taken as an example. 

Indeed, the Spring founders have made efforts to improve the co-operativeness of the two frameworks. Yet, as logic finds, by far the best solution would be to opt for one of these frameworks, depending on the situation.

Migration to WebFlux

For starters, let’s get acquainted with Spring WebFlux. This is basically Spring MVCs’ younger brother, whose performance is based on a reactive-based stack. 

The official documentation highlights two features that characterize WebFlux:

  • concurrency handling with fewer resources
  • functional programming

The first item is closely related to the introduction of the reactive-based stack, while functional programming enables the development of a declarative programming style and the use of new features of the Java programming language.

From the previous information, you can conclude that migration does not have to be conducted in its entirety.

Spring gives us the possibility of partial refactoring. For starters, it is advisable to find non-critical application points.

If we look at the migration process as a book, this phase would be an introduction. An aspect that fits that description would be an external service call.

In the Spring MVC manner, it would look something like this:

public List<Student> getStudents() {
       try {
           ResponseEntity<Student[]> response = restTemplate.getForEntity("https://decode.agency:8080/students", Student[].class);
           return Arrays.asList(response.getBody());
       } catch (RestClientException ex) {
           throw new StudentException(ex);
       }
   }

As a replacement for RestTemplate, Spring WebFlux introduces the reactive version, WebClient.

public Flux<Student> getStudentsWithClient() {
       return webClient
               .get()
               .uri("https://decode.agency:8080/students")
               .retrieve()
               .bodyToFlux(Student.class)
               .onErrorMap(StudentException::new);
   }

What strikes the eye is the return type.

Instead of a list, the term Flux appears this time. Spring WebFlux uses Reactive streams and introduces composable API types for communication, Mono, and Flux. The difference between the two types is in the length of the data sequence. Mono stores a sequence of 0..1, while Flux may contain a range of 0..N.

After all, the said method can be refactored to accommodate the return type of Spring MVC implementation

public List<Student> getStudentsWithClient() {
       return webClient
               .get()
               .uri("https://decode.agency:8080/students")
               .retrieve()
               .bodyToFlux(Student.class)
               .onErrorMap(StudentException::new).collectList().block();
     }

The method that unpacks Flux content is called a block.

This change loses the sense of reactivity as the method again becomes blocking. But this example is perfect for getting to know the reactive library.

Keep in mind that a blocking method should never be called within a method that returns a reactive type. That would block one of the few threads of the application running on Netty and cause bad consequences.

There are various ways in which the performance of reactive and non-reactive flow can be compared. One way is to call external services repeatedly with time unit measurements.

For the purposes of this article, a test was conducted in which an external service was called five times in a loop:

for(int i = 0; i < 5; i++) {
           restTemplate.getForObject("https://decode.agency:8080/students/1", Student.class);
       }
      
       for(int i = 0; i < 5; i++) {
           webClient
                   .get()
                   .uri("https://decode.agency:8080/students/1")
                   .retrieve()
                   .bodyToMono(Student.class)
                   .subscribe();
       }

According to logs printed in the console, the results in favor of reactive mode are more than obvious:

Five consecutive calls can be read from the printout of the first loop, each lasting approximately two seconds, and the total loop execution just over 10 seconds.

In addition, in the print section defining the threads, it is evident that the calls are handled within the non-reactive thread.

2020-04-11 03:28:34.705 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:34.774 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]
2020-04-11 03:28:36.788 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Response 200 OK
2020-04-11 03:28:36.789 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Reading to [com.java.school.Student]
2020-04-11 03:28:36.803 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:36.803 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]
2020-04-11 03:28:38.808 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Response 200 OK
2020-04-11 03:28:38.808 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Reading to [com.java.school.Student]
2020-04-11 03:28:38.810 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:38.810 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]
2020-04-11 03:28:40.817 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Response 200 OK
2020-04-11 03:28:40.818 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Reading to [com.java.school.Student]
2020-04-11 03:28:40.818 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:40.819 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]
2020-04-11 03:28:42.828 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Response 200 OK
2020-04-11 03:28:42.829 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Reading to [com.java.school.Student]
2020-04-11 03:28:42.829 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:42.830 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]
2020-04-11 03:28:44.843 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Response 200 OK
2020-04-11 03:28:44.844 DEBUG 3768 --- [nio-8081-exec-1] o.s.web.client.RestTemplate              : Reading to [com.java.school.Student]

In opposite, while examining the second loop prints, you can spot remarkable progress in the aspect of time. Running the loop took about as long as a single call, more precisely two and a half seconds. This time, calls were made on the netty server by the event loop concept and the names of the used threads support that fact.

2020-04-11 03:28:45.022 DEBUG 3768 --- [nio-8081-exec-1] o.s.w.r.f.client.ExchangeFunctions       : [1ab812d8] HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:45.406 DEBUG 3768 --- [nio-8081-exec-1] o.s.w.r.f.client.ExchangeFunctions       : [59e985a] HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:45.408 DEBUG 3768 --- [nio-8081-exec-1] o.s.w.r.f.client.ExchangeFunctions       : [49db5d25] HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:45.413 DEBUG 3768 --- [nio-8081-exec-1] o.s.w.r.f.client.ExchangeFunctions       : [40bb7f47] HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:45.416 DEBUG 3768 --- [nio-8081-exec-1] o.s.w.r.f.client.ExchangeFunctions       : [4e4d33d3] HTTP GET https://decode.agency:8080/students/1
2020-04-11 03:28:47.543 DEBUG 3768 --- [ctor-http-nio-1] o.s.w.r.f.client.ExchangeFunctions       : [1ab812d8] Response 200 OK
2020-04-11 03:28:47.543 DEBUG 3768 --- [ctor-http-nio-3] o.s.w.r.f.client.ExchangeFunctions       : [49db5d25] Response 200 OK
2020-04-11 03:28:47.543 DEBUG 3768 --- [ctor-http-nio-4] o.s.w.r.f.client.ExchangeFunctions       : [40bb7f47] Response 200 OK
2020-04-11 03:28:47.547 DEBUG 3768 --- [ctor-http-nio-2] o.s.w.r.f.client.ExchangeFunctions       : [59e985a] Response 200 OK
2020-04-11 03:28:47.574 DEBUG 3768 --- [ctor-http-nio-1] o.s.w.r.f.client.ExchangeFunctions       : [4e4d33d3] Response 200 OK

Fact! Every response lasts at least two seconds because of the imported delay in the external service.

The next step in the migration would be to adjust a critical part of the application that extends across all layers of the application, from repositories to controllers and external calls.

Parts that are rarely used or have no communication purpose and are not critical can be left as they are.

The following code snippet shows how to call multiple services in parallel using the WebFlux paradigm as opposed to the CompletableFuture available from Java 8.

public Instructions getInstructions (Long idStudent, Long idInstructor) {
       try {
           return CompletableFuture.completedFuture(Instructions.builder())
                   .thenCombine(CompletableFuture.supplyAsync(() -> singleObjectService.getStudent(idStudent)), Instructions.InstructionsBuilder::student)
                   .thenCombine(CompletableFuture.supplyAsync(() -> singleObjectService.getInstructor(idInstructor)), Instructions.InstructionsBuilder::instructor)
                   .get()
                   .build();
       } catch (ExecutionException | InterruptedException ex) {
           throw new InstructionsException(ex);
       }
   }
public Mono<Instructions> getInstructionsReactive (Long idStudent, Long idInstructor) {
       return Mono.just(Instructions.builder())
               .zipWith(singleObjectService.getStudentReactive(idStudent), Instructions.InstructionsBuilder::student)
               .zipWith(singleObjectService.getInstructorReactive(idInstructor), Instructions.InstructionsBuilder::instructor)
               .map(Instructions.InstructionsBuilder::build)
               .onErrorMap(InstructionsException::new);
   }

The above procedures can make a major transition to a Netty server, which is in some ways the main objective of migration, to make the most of non-blocking principles.

One of the advanced measures that can be done is migration to reactive repositories to allow the flow of data streams from the controller to the database and vice versa.

In terms of database connectivity, Spring WebFlux was initially based on NoSQL databases only. But lately, support for relational databases has evolved.

Additional changes can be made at the other end of the application, the controllers. For example, the @Controller annotation can be removed if functional endpoints are introduced.

This change is possible using the Router functions, which is visible in the following code snippet:

public Mono<ServerResponse> getStudents(ServerRequest serverRequest) {
       Flux<Student> students = schoolService.getStudentsWithClient();
       return ServerResponse.ok()
               .contentType(MediaType.APPLICATION_JSON)
               .body(students, Student.class);
   }
@Bean
   static RouterFunction router(SchoolHandler handler) {
       return route()
               .path("/students", builder -> builder
               .GET("/all", accept(APPLICATION_JSON)  , handler::getStudents))
               .build();
   }

This modification is more a matter of taste and style than it contributes to the performance of the application.

Conclusion

All in all, in the overall migration process, one has to be careful and fully consider the situation. In the absence of performance issues or high resource consumption, there is no need to switch to a reactive approach. This is the case with most applications that do not make frequent calls to external services. 

Potential collaborative issues should be borne in mind if both approaches are used simultaneously. The problem of using the block function in reactive wires has already been mentioned, and there are frequent problems with integration and Unit tests. This is most often due to the inclusion of libraries incompatible with WebFlux. 

In any case, it is advisable to opt for one of the approaches if at all possible. However, I would definitely recommend You to get acquainted with Spring WebFlux, at least out of curiosity.

You can also find more information in the official documentation.

I hope this article was informative if you have any questions feel free to contact us at business@decode.agency

Categories
Written by

Josip Peric

Software Engineer

Related articles