© Adam L. Davis 2020
A. L. DavisSpring Quick Reference Guidehttps://doi.org/10.1007/978-1-4842-6144-6_17

17. Spring WebFlux

Adam L. Davis1 
(1)
Oviedo, FL, USA
 

Spring WebFlux is similar to Spring MVC but allows you to use reactive streams and is asynchronous and nonblocking, which, if used correctly, can allow your application to have better performance.

With WebFlux, we can quickly create asynchronous, nonblocking, and event-driven applications using HTTP or WebSocket connections. Spring uses its own Reactive Streams implementation, Reactor1 (with Flux<T> and Mono<T>), in many of its APIs. You can use another implementation within your application, such as RxJava if you choose, but project Reactor has the best integration with WebFlux.

By default, a Spring WebFlux application uses an embedded instance of Netty,2 an asynchronous event-driven application framework, although you can configure it to use an embedded Tomcat, Jetty, or Undertow instead.

In this chapter, we’ll take a look at implementing a full project using Spring Boot, WebFlux, and Reactor with a MongoDB persistence layer.

With Spring WebFlux, we can very easily create a nonblocking, asynchronous application with a backing MongoDB, Redis, or Cassandra database or any relational database with a R2DBC driver implemented.

Getting Started

For the purposes of this chapter, we will create a Gradle-build, Java-based Spring Boot project.

Spring Boot is highly customizable, and you can add whichever “starters” you want for your project (web, mail, freemarker, security, etc.). This makes it as lightweight as possible.

We’re going to create a WebFlux-based project that uses Spring’s Reactor project along with MongoDB3 to create a fully reactive web application.

The code for this project is available on GitHub at adamldavis/humblecode.4

Gradle Plugin

The basic Gradle build for Spring Boot looks something like the following:
plugins {                                                       //1
  id 'org.springframework.boot' version '2.3.0.RELEASE'
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'
  id 'java'
}
group = 'com.humblecode'                                             //2
version = '0.0.2-SNAPSHOT'
sourceCompatibility = 11
repositories {
  mavenCentral()
}
dependencies {
  compile('org.springframework.boot:spring-boot-starter-webflux')     //3
  compileOnly('org.projectlombok:lombok')                   //4
  compile(
'org.springframework.boot:spring-boot-starter-data-mongodb-reactive') //5
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile('io.projectreactor:reactor-test')             //6
}
  1. 1.

    The first thing you might notice is the lack of versions specified; Spring Boot provides those for you and ensures that everything is compatible.

     
  2. 2.

    We specify the groupId and version for the build artifact. We also specify the Java source version as 11. You don’t need to specify the main class. That is determined by Spring Boot through annotations.

     
  3. 3.

    We include the “webflux” starter to enable Spring’s WebFlux.

     
  4. 4.

    We’re including project lombok here just to simplify the model classes. Lombok supplies annotations and automatically generates boilerplate code like getters and setters based on which annotations are used on each class.

     
  5. 5.

    Here we include the spring-data starter for using reactive MongoDB with Reactor integration.

     
  6. 6.

    Finally, we include “spring-boot-starter-test” and “reactor-test” to use the test support supplied by Spring.

     

Keep in mind that in order for the back end to be completely reactive, our integration with the database needs to be asynchronous. This is not possible with every type of database. In this case, we are using MongoDB which supports asynchronous operations.

As of writing, Spring Data provides direct reactive integration for Redis, MongoDB, and Cassandra. You can do this by simply switching “mongodb” for the database you want in the “starter” compile dependency. Spring Data also provided support for R2DBC for relational databases (see Chapter 6 for more).

Tasks

The Spring Boot plugin adds several tasks to the build.

To run the project, run “gradle bootRun” (which runs on port 8080 by default). Look at the command-line output to see useful information like which port your application is running on.

When you’re ready to deploy, run “gradle bootRepackage” which builds a fat jar with everything you need to run the full application in one jar.

SpringBootApplication

The main class is specified by annotating it with @SpringBootApplication . Create a file named DemoApplication.java in the com.example.demo package and put the following:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
  @Bean
  public Service sampleService() {
    return new SampleService(); } //2
}
Later on, we can add our own configuration classes to better configure things like Security in our application. For example, here’s the beginning of a SecurityConfig class that would enable Spring Security in our application:
@EnableWebFluxSecurity
public class SecurityConfig

Later on, we’ll explore adding security to a WebFlux project.

Our Domain Model

For this section, we will be implementing a very simple website with a RESTful API for online learning. Each course will have a price (in cents), a name, and a list of segments.

We will use the following domain model Course class definition:
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.*;
@Data //1
@AllArgsConstructor
@Document //2
public class Course {
  @Id UUID id = UUID.randomUUID(); //3
  public String name;
  public long price = 2000; // $20.00 is default price
  public final List<Segment> segments = new ArrayList<>();
  public Course(String name) {this.name = name;}
  public void setSegments(List<Segment> segments) {
    this.segments.clear();
    this.segments.addAll(segments);
  }
  // custom toString method
}
  1. 1.

    The first two annotations are Lombok annotations. @Data tells Lombok to add getters and setters for every field, equals and hashCode methods, a constructor, and a toString method.5

     
  2. 2.

    The @Document annotation is the spring-data-mongodb annotation to declare this class represents a MongoDB document.

     
  3. 3.

    The @Id annotation denotes the id property of this document.

     
After installing mongodb, you can start it (on Unix-based systems) with the following command:
mongod –dbpath data/ --fork --logpath ∼/mongodb/logs/mongodb.log

ReactiveMongoRepository

First, we need to create an interface to our back-end database, in this case MongoDB.

Using the spring-boot-starter-data-mongodb-reactive dependency that we included, we can simply create a new interface that extends ReactiveMongoRepository<T,ID>, and (in addition to the methods already on this interface) Spring will generate the code backing any method we define using a standard naming scheme (as we learned about in Chapter 6). By returning Reactor classes, like Flux<T> or Mono<T>, these methods will automatically be reactive.

For example, we can create a repository for Courses:
public interface CourseRepository extends
    ReactiveMongoRepository<Course, UUID> {            //1
  Flux<Course> findAllByNameLike(String searchString); //2
  Flux<Course> findAllByName(String name);             //3
}
  1. 1.

    The first generic type is the type this repository stores (Course) and the second is the type of Course’s ID.

     
  2. 2.

    This method finds all Courses with the names that match the given search String and returns a Flux<Course>.

     
  3. 3.

    This method finds all Courses with the given name. If we were sure names are unique, we could have used Mono<Course> findByName(String name).

     

Simply by extending the ReactiveMongoRepository<T,ID> interface, our repository will have tons of useful methods such as findById, insert, and save all returning Reactor types (Mono<T> or Flux<T>).

Controllers

Next, we need to make a basic controller for rendering our view templates.

Annotate a class with @Controller to create a web controller, for example:
@Controller
public class WebController {
  @GetMapping("/hello")
  public String hello() { return "home"; }
}

As the preceding method returns the string “home”, it would render the corresponding view template.

The GetMapping annotation is identical to using @RequestMapping(path="/hello", method = RequestMethod.GET). It’s not reactive yet, we will add that later on in this chapter.

By default, Spring WebFlux uses an embedded Netty instance. Using the embedded container means that container is just another “bean” which makes configuration a lot easier. It can be configured using application.properties and other application configuration files.

Next, we’d like to add some initial data to our repository so there’s something to look at. We can accomplish this by adding a method annotated with @PostConstruct that only adds data to the courseRepository when the count is zero:
@PostConstruct
public void setup() {
  courseRepository.count().blockOptional().filter(count -> count == 0)
  .ifPresent(it ->
    Flux.just(
      new Course("Beginning Java"),
      new Course("Advanced Java"),
      new Course("Reactive Streams in Java"))
    .doOnNext(c -> System.out.println(c.toString()))
    .flatMap(courseRepository::save)
    .subscribeOn(Schedulers.single())
    .subscribe() // need to do this to actually execute save
  );
}

Here the code uses a mix of Java 8’s Optional<T> with Reactor. Note that we must call subscribe on a Flux or else it won’t ever execute. We accomplish this here by calling subscribe() with no parameters. Since count() returns a Mono<Long>, we call blockOptional() which will block (wait for the Mono to complete) and then use the given value; if it is zero, we then save three Course objects to the courseRepository. For a refresher on using Flux and Mono, please see Chapter 12.

View Templates

In any Spring Boot project, we could use one of many view template renderers. In this case, we include the freemarker spring starter to our build file under dependencies:
compile('org.springframework.boot:spring-boot-starter-freemarker')
We put our templates under src/main/resources/templates. Here’s the important part of the file, home.ftl:
<div class="page-header">
    <h1>Welcome to ${applicationName}!</h1>
</div>
<article id="content" class="jumbotron center"></article>
<script type="application/javascript">
jQuery(document).ready(HC.loadCourses);
</script>
This calls the corresponding JavaScript to get the list of Courses from our RestController. The loadCourses function is defined something like the following:
jQuery.ajax({method: 'get', url: '/api/courses'}).done( //1
function(data) {
  var list = data;                                      //2
  var ul = jQuery('<ul class="courses btn-group"></ul>');
  list.forEach((crs) => {                               //3
    ul.append('<li class="btn-link" onclick="HC.loadCourse(\''+
    crs.id+'\'); return false">'
    + crs.name + ': <i>' + crs.price + '</i></li>')
  });
  jQuery('#content').html(ul);                          //4
}
).fail( errorHandler );                                 //5
  1. 1.

    First, we call our RESTful API, which we will define later.

     
  2. 2.

    Since we’re using jQuery, it automatically determines the response is JSON and parses the returned data.

     
  3. 3.

    Using forEach, we build an HTML list to display each Course with a link to load each Course.

     
  4. 4.

    We update the DOM to include the list we built.

     
  5. 5.

    Here we specify the error handling function in case anything goes wrong with the HTTP request.

     

Although we’re using jQuery here, we could have chosen any JavaScript library/framework. For Spring Boot, JavaScript files should be stored at src/main/resources/static/js.

RESTful API

By default, Spring encodes data from a @RestController into JSON, so the corresponding CourseController is defined thusly:
@RestController
public class CourseController {
      final CourseRepository courseRepository;
      public CourseControl(CourseRepository courseRepository) {
            this.courseRepository = courseRepository;
      }
      @GetMapping("/api/courses")
      public Flux<Course> getCourses() {
            return courseRepository.findAll();
      }
      @GetMapping("/api/courses/{id}")
      public Mono<Course> getCourse(@PathVariable("id") String id) {
            return courseRepository.findById(UUID.fromString(id));
      }
}
Listing 17-1

CourseController.java

Note how we can return Reactor data types like Flux<T> directly from a RestController since we are using WebFlux. This means that every HTTP request will be nonblocking and use Reactor to determine the threads on which to run your operations.

Note that we are calling the repository directly from the controller in this example. In a production system, it is best practice to add a “service” layer in between the controller and repository to hold the business logic.

Now we have the ability to read Courses, but we also need the ability to save and update them.

Since we’re making a RESTful API, we use @PostMapping to handle HTTP POST for saving new entities and @PutMapping to handle PUT for updating.

Here’s the save method:
@PostMapping(value = "/api/courses",
      consumes = MediaType.APPLICATION_JSON_VALUE)
public Mono<Course> saveCourse(@RequestBody Map<String,Object> body) {
      Course course = new Course((String) body.get("name"));
      course.price = Long.parseLong(body.get("price").toString());
      return courseRepository.insert(course);
}

Note that the insert method returns a Reactor Mono<Course>; you may recall, a Mono<T> can only return zero or one instance or fail with an error.

Here’s the update method:
@PutMapping(value = "/api/courses/{id}",
      consumes = MediaType.APPLICATION_JSON_VALUE)
public Mono<Course> updateCourse(@PathVariable("id") String id,
                                 @RequestBody Map<String,Object> body) {
  Mono<Course> courseMono = courseRepository.findById(UUID.fromString(id));
  return courseMono.flatMap(course -> {
      if (body.containsKey("price")) course.price =
      Long.parseLong(body.get("price").toString());
      if (body.containsKey("name")) course.name =
            (String) body.get("name");
      return courseRepository.save(course);
    });
}

Note how we use flatMap here to update the course and return the result of the save method which also returns a Mono<T>. If we had used map, the return type would be Mono<Mono<Course>>. By using flatMap, we “flatten” it to just Mono<Course> which is the return type we want here.

../images/498572_1_En_17_Chapter/498572_1_En_17_Figa_HTML.jpg For more information on Reactor, see Chapter 12.

Further Configuration

In a real application, we will most likely want to override many of the default configurations for our application. For example, we will want to implement custom error handling and security.

First, to customize WebFlux, we add a class that extends WebFluxConfigurationSupport (here it’s named WebFluxConfig, but it could be named anything):
@EnableWebFlux
public class WebFluxConfig extends WebFluxConfigurationSupport {
  @Override
  public WebExceptionHandler responseStatusExceptionHandler() {
    return (exchange, ex) -> Mono.create(callback -> {
            exchange.getResponse().setStatusCode(HttpStatus.I_AM_A_TEAPOT);
      System.err.println(ex.getMessage());
      callback.success(null);
    });
  }
}

Here we override the responseStatusExceptionHandler to set the status code to 418 (I’m a teapot6) which is an actual HTTP status code that exists (just for demonstration purposes). There are many methods that you can override to provide your own custom logic.

Finally, no application would be complete without some form of security. First, make sure to add the spring-security dependency to your build file:
compile('org.springframework.boot:spring-boot-starter-security')
Next, add a class and annotate it with @EnableWebFluxSecurity and define beans as follows:
@EnableWebFluxSecurity //1
public class SecurityConfig {
  @Bean
  public SecurityWebFilterChain
         springSecurityFilterChain(ServerHttpSecurity http) {
    http
      .authorizeExchange()
      .pathMatchers("/api/**", "/css/**", "/js/**", "/images/**", "/")
      .permitAll() //2
      .pathMatchers("/user/**").hasAuthority("user") //3
      .and()
      .formLogin();
      return http.build();
    }
    @Bean
    public MapReactiveUserDetailsService
        userDetailsService(@Autowired UserRepository userRepository) {
      List<UserDetails> userDetails = new ArrayList<>();
      userDetails.addAll(
            userRepository.findAll().collectList().block());//4
      return new MapReactiveUserDetailsService(userDetails);
  }
  @Bean
  public PasswordEncoder myPasswordEncoder() { //5
      // never do this in production of course
      return new PasswordEncoder() {/*plaintext encoder*/};
  }
}
  1. 1.

    This annotation tells Spring Security to secure your WebFlux application.

     
  2. 2.

    We define what paths are allowed to all users using the ant-pattern where “**” means any directory or directories. This allows everyone access to the main page and static files.

     
  3. 3.

    Here we make sure that a user must be logged in to reach any path under the “/user/” path.

     
  4. 4.

    This line converts all Users from the UserRepository into a List. This is then passed to the MapReactiveUserDetailsService which provides users to Spring Security.

     
  5. 5.

    You must define a PasswordEncoder. Here we define a plaintext encoding just for demo purposes. In a real system, you should use a StandardPasswordEncoder or BCryptPasswordEncoder.

     

Testing

Spring Boot provides thorough built-in support for testing. For example, annotating a JUnit (4) test class with @RunWith(SpringRunner.class) and @SpringBootTest, we can run integration tests with our entire application running as follows:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class HumblecodeApplicationTests {
  @Autowired
  private TestRestTemplate testRestTemplate;
  @Test
  public void testFreeMarkerTemplate() {
    ResponseEntity<String> entity = testRestTemplate
          .getForEntity("/", String.class);
    assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(entity.getBody()).contains("Welcome to");
  }
}

This simple test boots up our Spring Boot application and verifies that the root page returns with HTTP OK (200) status code and the body contains the text “Welcome to”. Using “webEnvironment = WebEnvironment.RANDOM_PORT” specifies that the Spring Boot application should pick a random port to run locally on every time the test is run.

Spring Data R2DBC

R2DBC7 (Reactive Relational Database Connectivity ) is a standard and programming interface for integrating with a relational database such as MySQL in a reactive, nonblocking way.

Although still at an early stage, there are several implementations of drivers including MySQL, H2, Microsoft SQL Server, and PostgreSQL.

R2DBC is covered more fully in Chapter 6.

Netty or Tomcat

By default, Spring WebFlux will use an embedded Netty web container. However, if spring-web is included anywhere on your classpath, your application will run using Tomcat instead. Spring Web pulls in Tomcat as a dependency, and that is the default from auto-configuration.

WebFlux supports running either using Netty (an asynchronous, nonblocking, event-driven network application framework) or the Servlet 3.1 nonblocking standard (using Tomcat or Jetty).

To ensure you run on Netty, you should exclude Tomcat from your dependencies.

WebClient

If you have Spring WebFlux on your classpath, you can also use WebClient to call remote web services. Compared to RestTemplate, this client has a more functional feel and is fully reactive, using Netty as the concurrency engine.

You can create an instance of WebClient using the builder pattern starting from the static WebClient.builder() method, for example:
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
// later on...
WebClient myWebClient = WebClient.builder()
    .baseUrl("http://localhost:8080")
    .defaultCookie("cookieKey", "cookieValue")
    .defaultHeader(HttpHeaders.CONTENT_TYPE,
               MediaType.APPLICATION_JSON_VALUE)
  .build();

This builds a WebClient with a given baseUrl so that all requests made will start with this URL. It also provides a cookie and header to use for every request. There are many more methods to configure the WebClient.8

Each request starts with defining the HTTP method, and then you can specify an additional URL path (with or without path variables) and call exchange which returns a Mono<ClientResponse>, for example:
// get the Course with ID=1 and print it out:
myWebClient.get()
          .uri("/api/courses/{id}", 1L)
          .exchange()
          .flatMap((ClientResponse response) ->
                       response.bodyToMono(Course.class))
          .subscribe(course -> System.out.println("course = " + course));