Spring Boot greatly simplifies creating a Spring-based application or microservice.
It takes an opinionated approach with sensible defaults for everything you might need and can get you quickly up and running. It uses annotations (no XML needed) and no code generation.
With WebFlux, we can quickly create asynchronous, nonblocking, and event-driven applications using HTTP or WebSocket connections. Spring uses its own Reactive Streams implementation, Reactor (with Flux and Mono), in many of its APIs. Of course, you can use another implementation within your application, such as RxJava if you so choose.
In this chapter, we’ll take a look at implementing a full project using Spring Boot, WebFlux, and Reactor with a MongoDB persistence layer.
Getting Started
There are several ways to start a Spring Boot project. Among them are the following:
- 1.
Go to the
Spring Initializr
and create a project template from there. There are also tools like Spring Tool Suite that take advantage of the spring initializer from your IDE.
- 2.
Create your own Maven-based project.
- 3.
Create your own Gradle-based project.
For the purposes of this book, we will choose option three and create a Gradle, Java-based 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 MongoDB in order to have a fully reactive web application.
The code for this project is available on GitHub at
adamldavis/humblecode
.
Gradle Plugin
The most basic
Gradle build for Spring Boot with WebFlux looks something like the following:
buildscript {
ext {
springBootVersion = '2.0.4’
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'groovy'
apply plugin: 'idea'
dependencies { //1
compile('org.springframework.boot:spring-boot-starter-webflux') //2
compile('org.codehaus.groovy:groovy')
compileOnly('org.projectlombok:lombok') //3
compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive') //4
testCompile('org.springframework.boot:spring-boot-starter-test') //5
testCompile('io.projectreactor:reactor-test') //6
}
- 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 based on the version of Spring Boot specified. You also don’t need to specify the main class. That is determined through annotations.
- 2.
We include the “webflux” starter to enable Spring’s WebFlux and “reactor-test” to allow us to test Reactor-based code more easily.
- 3.
We’re including Project Lombok here just to simplify the model classes. Lombok gives you annotations that automatically generate boilerplate code like getters and setters.
- 4.
Here we include the Spring Data start for using MongoDB
with Reactor integration.
- 5.
We include the “spring-boot-starter-test” artifact to help with our testing of the application.
- 6.
We include “reactor-test” to make testing Reactor-related code easier.
Keep in mind that 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
.
At the time of writing, Spring provides reactive integrations “only” for Redis, MongoDB, and Cassandra. You can do this by simply switching “mongodb
” for the database you want in the “starter” compile dependency. There is an asynchronous driver available for PostgreSQL,
postgres-async-driver
, so it might be supported in the future.
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. For example, the last four lines might be something like the following:
2018-09-28 15:23:41.813 INFO 19132 --- [main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-09-28 15:23:41.876 INFO 19132 --- [server-epoll-13] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0%0:8003
2018-09-28 15:23:41.876 INFO 19132 --- [main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8003
2018-09-28 15:23:41.879 INFO 19132 --- [main] c.h.humblecode.HumblecodeApplication : Started HumblecodeApplication in 3.579 seconds (JVM running for 4.029)
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
. For example, create a class named HumblecodeApplication and put it in the com.humblecode package and put the following:
package com.humblecode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.*;
import org.springframework.context.annotation.Bean;
import reactor.core.publisher.Flux;
@SpringBootApplication
public class HumblecodeApplication {
public static void main(String[] args) { //1
SpringApplication.run(
HumblecodeApplication.class, args);
}
@Bean
public Flux<String> exampleBean() { //2
return Flux.just("example");
}
}
- 1.
The main method calls SpringApplication.run to start the application.
- 2.
Beans can be created directly using the @Bean annotation on methods. Here we create a simple Flux<String> of just one element.
The @SpringBootApplication
annotation tells Spring a number of things:
- 1.
To use auto-configuration.
- 2.
To use component scanning. It will scan all packages and subpackages for classes annotated with Spring annotations.
- 3.
This class is a Java-based configuration class, so you can define beans here using the @Bean annotation on a method that returns a bean.
Auto-Configuration
Spring Boot considers the runtime of your application and automatically configures your application based on many factors, such as libraries on the classpath.
It follows the motto: “If everyone has to do it, then why does everyone have to do it?”
For example, to create a typical MVC web app, you will need to add a configuration class and multiple dependencies and configure a Tomcat container. With Spring Boot, all you need to add is a dependency and a controller class, and it will automatically add an embedded Tomcat instance.
Configuration
files can be defined as properties files, yaml, and other ways. To start with, create a file named “application.properties” under “src/main/resources” and add the following:
server.port=8003
app.name=Humble Code
This sets the server to run on port 8003 and sets a user-defined property app.name which can be any value.
Later on you can add your own configuration classes to better configure things like security in your
application. For example, here’s the beginning of a SecurityConfig class that would enable Spring Security in your 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 web site 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.*;
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;}
}
- 1.
The first two annotations are Lombok annotations. @Data tells Lombok to add getters and setters for every field, a toString() method, equals and hashCode() methods, and a constructor.
- 2.
The @Document annotation is the Spring Data mongo annotation to declare this class represents a mongo document.
- 3.
The @Id annotation denotes the id property of this document.
After installing
MongoDB, you can start it 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, and Spring will generate the code backing any method we define using a standard naming scheme. By returning Reactor classes, like Flux or Mono, these methods will automatically be reactive.
For example, we can create a repository for Courses
:
import com.humblecode.humblecode.model.Course;
import org.springframework.data.mongodb.\
repository.ReactiveMongoRepository;
import reactor.core.publisher.Flux;
import java.util.UUID;
public interface CourseRepository extends
ReactiveMongoRepository<Course, UUID> { //1
Flux<Course> findAllByNameLike(String searchString); //2
Flux<Course> findAllByName(String name); //3
}
- 1.
The first generic type is the type this repository stores (Course), and the second is the type of Course’s ID.
- 2.
This method finds all Courses with the names that match the given search String.
- 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 interface, our repository will have tons of useful methods such as findById, insert, and save all returning Reactor types (Mono or Flux).
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:
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@Controller
public class WebController {
@GetMapping("/")
public Mono<String> hello() {
return Mono.just("home");
}
}
As the preceding method returns the string “home” wrapped by a Mono, it would render the corresponding view template (located under src/main/resources/templates), if we have one; otherwise it would just return the string itself.
The GetMapping annotation is identical to using @RequestMapping(path = “/”, method = RequestMethod.GET).
By default a WebFlux-based Spring Boot application uses an embedded Netty instance, although you can configure it to use Tomcat, Jetty, or Undertow instead.
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() //1
.blockOptional() //2
.filter(count -> count == 0) //3
.ifPresent(it -> //4
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) //5
.subscribeOn(Schedulers.single()) //6
.subscribe()
); //7
}
- 1.
Get the count from the CourseRepository (which has the type Mono<Long>).
- 2.
Call “blockOptional()” which will block until the Mono returns a value and converts the output to an Optional<Long>.
- 3.
Keep the value only if it is zero.
- 4.
If it was zero, we create a Flux of three Course objects we want to save.
- 5.
Map those Courses to the repository’s “save” method using flatMap.
- 6.
Specify the Scheduler to use as Schedulers.single().
- 7.
Subscribe the Flux so it executes.
Here the code uses a mix of Java 8’s Optional interface 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.
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 template file, home.ftl (some is left out for brevity):
<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 which we will define later. The loadCourses
function is defined something like the following:
jQuery.ajax({method: 'get',
url: '/api/courses'}).done( //1
function(list) { //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.
First we call our restful API, which we will define later.
- 2.
Since we’re using jQuery, it automatically determines that the response is JSON and parses the returned data.
- 3.
Using forEach we build an HTML list to display each Course with a link to load each Course.
- 4.
We update the DOM to include the list we built.
- 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 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 CourseControl is defined thusly:
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.*;
import java.util.*;
@RestController
public class CourseControl {
final CourseRepository courseRepository;
public CourseControl(
CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
@GetMapping("/api/courses")
public Flux<Course> getCourses() {
return courseRepository.findAll();
}
@GetMapping("/api/course/{id}")
public Mono<Course> getCourse(
@PathVariable("id") String id) {
return courseRepository.findById(
UUID.fromString(id));
}
}
Note how we can return Reactor data types like Flux 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.
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 how the save method is set to consume a
JSON map of values (using a Map just to keep the code simple):
@PostMapping(value = "/api/course",
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 instance. As you may recall, a Mono can only return one instance or fail with an error.
The corresponding JavaScript code will be similar to the previous example except the ajax call will be more like the following (assuming “name” and “price” are ids of inputs):
var name = jQuery('#name').val();
var price = jQuery('#price').val();
jQuery.ajax({method: 'post', url: '/api/course/',
data: {name: name, price: price}})
Here’s the update method which will be activated by a PUT
request using the given “id” and also expecting a
JSON map of values:
@PutMapping(value = "/api/course/{id}",
consumes = MediaType.APPLICATION_JSON_VALUE)
public Mono<Course> updateCourse(
@RequestParam("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. 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.
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 and is annotated with @EnableWebFlux (here the class is named WebFluxConfig, but it could be named anything). Adding that annotation not only tells Spring Boot to enable WebFlux but also to look at this class for extra
configuration. For example:
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.config.*;
import org.springframework.web.server.*;
import reactor.core.publisher.Mono;
@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 teapot) which is an actual HTTP status code that exists. 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 from the “org.springframework.security.config.annotation.web.reactive” package 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(
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.
This annotation tells Spring Security to secure your WebFlux application.
- 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.
Here we make sure that a user must be logged in to reach any path under the “/user/” path.
- 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.
You must define a PasswordEncoder. Here we define a plain-text encoding just for demo purposes. In a real system, you should use a StandardPasswordEncoder or, even better, BcryptPasswordEncoder.
The
UserRepository would be defined as follows:
public interface UserRepository extends
ReactiveMongoRepository<User, UUID> {}
Testing
Spring Boot provides thorough built-in support for
testing. For example, annotating a JUnit test class with @RunWith(SpringRunner.class) and @SpringBootTest, we can run integration tests with our entire application running as follows:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.\
SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.\
TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
@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.
We can also test the main function of our application such as the ability to get a list of courses in JSON like the following test demonstrates:
@Test public void testGetCourses() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(
Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity<String> requestEntity =
new HttpEntity<>(headers);
ResponseEntity<String> response = testRestTemplate
.exchange("/api/courses", HttpMethod.GET,
requestEntity, String.class);
assertThat(response.getStatusCode())
. isEqualTo(HttpStatus.OK);
assertThat(response.getBody())
.contains("\"name\":\"Beginning Java\",\"price\":2000");
}