Reactive RESTful service with Spring 5, Spring Boot 2 and MongoDB (part 1)

In this series I will share my experience on writing a reactive RESTful service with Spring 5 and Spring Boot 2, the goal is to build a simple yet fully functional service: from the entity persistence layer with MongoDB up to the REST layer with WebFlux and security with OAuth2 and SSL

If you are not familiar with Reactive programming and Spring 5’s implementation you can find interesting material here, here and here.

At the time of the writing, the available version of Spring Boot is 2.0.0.BUILD-SNAPSHOT. It brings with it Spring Framework 5.0.0.BUILD-SNAPSHOT which is also the latest version.

Part1 will focus on the core of the service: entities, repositories, REST api and testing. In part2 we will see how to secure the service.

The entire code of the project is available on github

The service

We will develop a very basic reactive service that allows us to manage customers through a REST api.

Used technologies

  • Spring Boot 2.0.0.BUILD-SNAPSHOT
    • Spring 5
    • Spring Data MongoDB Reactive
    • Spring WebFlux
    • Spring Security OAuth2
    • Other Spring modules
  • Embedded MongoDB (for ease of use)
  • Undertow (can be replaced with Tomcat or Jetty)
  • Java 8
  • Maven 3

The project

Let’s start with a Java 8 Maven project that we turn into a Spring Boot project by making the pom inherit from spring-boot-starter-parent. We also add the starter module spring-boot-starter-test for unit tests.
This version of Boot is not available in the public Maven Repository so we need to add a reference to spring-snapshots and spring-milestones repositories. This operation is explained here.

Domain entities

package customerservice.domain;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(Include.NON_NULL)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public final class Customer {

	@JsonSerialize(using = ToStringSerializer.class)
	private ObjectId id;
	private String firstName;
	private String lastName;
	private Gender gender;
	@JsonFormat(shape = Shape.STRING)
	private LocalDate birthDate;
	private MaritalStatus maritalStatus;
	private Address address;
	private Map<PhoneType, String> phones;
	private String email;
	@NotNull
	private CustomerType customerType;

	private Customer() {}
	...
}

The class Customer contains information about the customer, the address will be stored in its own class Address that has a one to one relationship with Customer

package customerservice.domain;

@JsonInclude(Include.NON_NULL)
public final class Address {

	private Integer streetNumber;
	private String streetName;
	private String city;
	private String zipcode;
	private String stateOrProvince;
	private String country;

	private Address() {}
	...
}

A few things are worth mentioning here:

  • All entities are immutable.
  • Builders (the Builder pattern) are used to create instances.
  • Jackson annotations @Json* are used to customize serialization and deserialization
    • @JsonIgnoreProperties(ignoreUnknown = true): during deserialization ignore fields that can not be mapped to a Java field
    • @JsonInclude(Include.NON_NULL): don’t serialize fields when they contain Null
    • @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class): output JSON in snake case; “first_name” instead of “firstName”
    • @JsonSerialize(using = ToStringSerializer.class): serialize the String representation of ObjectId (24 hexadecimal characters)
    • @JsonFormat(shape = Shape.STRING): serialize the String representation of Java 8’s LocalDate, if jackson-datatype-jsr310 dependency is present in the classpath then the date will be formatted in ISO-8601 format (yyyy-MM-dd)

Usage

Create a customer

Customer myCustomer = Customer.ofType(CustomerType.PERSON)
    .withFirstName("John")
    .withLastName("Doe")
    .withBirthDate(LocalDate.of(1990, Month.MARCH, 25))
    .build();

The static method ofType() takes a mandatory parameter of type CustomerType and returns a builder, each field of Customer has a corresponding with*() method in the builder to set its value. build() returns a new instance of Customer with the given values.

Create an address

Address myAddress = Address.ofCountry("myCountry")
    .withStreetNumber(110)
    .withStreetName("My street")
    .withCity("My City")
    .build();

For Address the mandatory field is country and the static method returning the builder is ofCountry(), the rest is similar to Customer.

Update a customer

Immutable objects don’t have setters and so a new instance is created every time the value of a field must change.

Customer myCustomer = Customer.ofType(CustomerType.PERSON)
    .withFirstName("John")
    .withLastName("Doe")
    .build();
// Now, myCustomer points to a new instances with updated first name
myCustomer = Customer.from(myCustomer).withFirstName("Jessica").build();

The static method from() takes a parameter of type Customer, creates a builder and initiates its fields with the values of the parameter then returns the builder, with*() methods can then be used to change any field before finally calling build() to return a new instance of Customer.

Update an address

Likewise, we update addresses

Address myAddress = Address.ofCountry("myCountry")
    .withStreetNumber(110)
    .withStreetName("My street")
    .withCity("My City")
    .build();
myAddress = Address.from(myAddress).withStreetNumber(75).build();

Persistence

Spring Data will be used to manage the persistence layer by adding the starter module spring-boot-starter-data-mongodb-reactive to the project, those of you who have already used Spring Data with MongoDB may have noticed the similarity of the name with the “non reactive” counterpart module spring-boot-starter-data-mongodb

We will also add a dependency on embedded MongoDB

<dependency>
 <groupId>de.flapdoodle.embed</groupId>
 <artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>

The repository

The repository is created by extending the interface ReactiveCrudRepository

public interface CustomerRepository extends ReactiveCrudRepository<Customer, ObjectId> {}

Just like CrudRepository, the interface ReactiveCrudRepository takes 2 parameters: the type of the entity it is persisting and the type of the primary key of that entity, it also offers the same CRUD methods with a difference, if the method returns an object of type T in CrudRepository then it will return a Mono of T in ReactiveCrudRepository, and if it returns a collection of T in CrudRepository then it will return a Flux of T in ReactiveCrudRepository.

Mono and Flux are the Reactive types Spring Reactive uses, they are similar to Java’s Future and it’s implementation CompletableFuture.

If you want to know more about Reactive types you can read this post.

Also worth mentioning, there is no need to annotate the interface with @Repository, Spring already knows it’s a repository.

Testing

For this test we will bootstrap Spring with the annotation @RunWith(SpringJUnit4ClassRunner.class) because we want to use MongoDB, Spring will take care of starting it and configuring a repository for us, we can then inject the repository in the test class.

@Autowired
private CustomerRepository repo;

@DirtiesContext

Spring offers this interesting annotation that can be used at the class level, it basically resets the context with a clean DB for each test. Maybe I misused it but I noticed that it dramatically slows down tests execution, so I replaced it with a simple repo.deleteAll() in a method annotated with @Before.

@Before
public void cleanDB() {
 repo.deleteAll().block();
}

Example

@Test
public void shouldCreateAPerson() {

 // 1
 Address address = Address.ofCountry("Shadaloo").build();
 // 2
 Customer customer = Customer.ofType(PERSON).withFirstName("Ken").withAddress(address).build()
 // 3
 Customer saved = repo.save(customer).block();

 // 4
 assertThat(saved.getId()).isNotNull();
 assertThat(saved.getFirstName()).isEqualTo("Ken");
 assertThat(saved.getAddress().getCountry()).isEqualTo("Shadaloo");
}

// 1 Create the address
// 2 Create the customer
// 3 Save the customer and block until the Mono associated with repo.save() returns the saved customer, this is very important because of the asynchronous nature or Mono
// 4 Check returned values

The entire test class can be found under src/test/java/customerservice/repository/mongodb

The REST API

Spring 5 comes with a new module called WebFlux which can be seen as the reactive counterpart of Spring MVC, it uses the class DispatcherHandler as the “Central dispatcher for HTTP request handlers/controllers” just like DispatcherServlet in Spring MVC.

Let’s add it to the pom.xml

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

We also add an application server; tomcat, jetty and undertow can be used.

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

The REST api will handle the following requests:

URL HTTP METHOD DESCRIPTION
/customers GET Return all customers
/customers/{id} GET Return one customer
/customers POST Add a new customer
/customers/{id} PUT Update a customer
/customers/{id} DELETE Delete a customer

The REST controller

package customerservice.restapi;
@RestController
@RequestMapping(path = "/customers", produces = { APPLICATION_JSON_UTF8_VALUE })
public class CustomerController {

 private CustomerRepository repo;

 public CustomerController(CustomerRepository repo) {
  this.repo = repo;
 }
 .....
}

The controller handles requests with the path /customers and returns a JSON payload. There is no need to annotate the repository with @Autowired, Spring already knows that we are doing constructor injection and does the necessary injection.

The method to handle a GET on /customers looks like this

@RequestMapping(method = GET)
public Mono<ResponseEntity<List<Customer>>> allCustomers() {
return repo.findAll() //1
 .collectList() //2
 .filter(customers -> customers.size() > 0) //3
 .map(customers -> ok(customers)) //4
 .defaultIfEmpty(noContent().build()); //5
}

//1 findAll() returns a Flux of Customer
//2 The Flux is converted into a Mono of List or Customer
//3 If the list is not empty
//4 then return OK (HTTP 200) response containing the list in the body.
//5 otherwise return NO_CONTENT (HTTP 204) response.

The other methods of the controller are implemented in the same way, we start with a Mono or a Flux returned by the repository, then we pass it through a chain of methods before returning a Mono of ResponseEntity

If you want to do Reactive programming get ready to do a lot of method chaining 🙂

Unit testing

The controller can be unit tested without bootstraping Spring, all we need is to mock the repository and inject it into the controller (This example uses Mockito)

@RunWith(MockitoJUnitRunner.class)
public class CustomerControllerTest {

 @Mock
 private CustomerRepository repo;

 @InjectMocks
 private CustomerController controller;

 @Test
 public void shouldReturnAllCustomers() {

 // Given
 final List<Customer> customers = asList(Customer.ofType(PERSON).build(), Customer.ofType(COMPANY).build());
 when(repo.findAll()).thenReturn(Flux.fromIterable(customers));

 // When
 final ResponseEntity<List<Customer>> response = controller.allCustomers().block();

 // Then
 assertThat(response.getStatusCode()).isEqualTo(OK);
 assertThat((Iterable<Customer>) response.getBody()).asList().containsAll(customers);
}

Controller methods return either a Mono or a Flux, both have a .block() method that blocks until the ResponseEntity is returned. This is not reactive but I think is ok for unit tests.

We will see how to test in a reactive way in the next paragraph.

Integration testing

WebFlux comes with 2 useful classes that can be used for testing: WebClient and WebTestClient.

WebTestClient is comparable to MockMvc when used without a running server and is comparable to TestRestTemplate when used with a running server, whereas WebClient is comparable to RestTemplate.

Let’s see how it works

@RunWith(SpringRunner.class)
public class CustomerControllerIntegrationTest {

 @MockBean
 private CustomerRepository repo;

 private WebTestClient webClient;

 @Before
 public void init() {
  webClient = WebTestClient
   .bindToController(new CustomerController(repo))
   .build();
 }
 ...
}

WebTestClient.bindController() means that we are testing the controller CustomerController in a mocked environment.

Spring has a useful annotation @MockBean that is comparable to Mockito’s @Mock and can be used to mock Spring beans.

@Test
public void shouldReturnAllCustomers() {

List<Customer> mockCustomers = asList(Customer.ofType(PERSON).build(), Customer.ofType(COMPANY).build());
given(repo.findAll()).willReturn(Flux.fromIterable(mockCustomers)); //1

webClient.get().uri("/customers").accept(APPLICATION_JSON_UTF8).exchange() //2
 .expectStatus().isOk() //3
 .expectHeader().contentType(APPLICATION_JSON_UTF8)
 .expectBodyList(Customer.class).hasSize(2).consumeWith(customers -> { //
  assertThat(customers.stream().map(Customer::getCustomerType).collect(toList())
   .containsAll(asList(PERSON, COMPANY)));
 });
}

//1 Mock findAll() with expected behavior.
//2 Issue a GET on /customers
//3 Expect HTTP 200
//4 Expect the list of customers returned by findAll()

Testing with curl

curl is a command line tool that can be used to test RESTful services, it’s available for all platforms.

First we start the Spring Boot application with mvn spring-boot:run, this command must be typed in the root directory of the project where pom.xml is located .

Add a customer

curl -i -X POST -H “Content-Type:application/json” http://localhost:8443/customers -d ‘{“first_name”:”John”,”last_name”:”Doe”,”birth_date”:”1950-01-01″,”customer_type”:”PERSON”,”address”:{“street_name”:”Wellington”,”country”:”canada”}}’

If everything goes well, curl will return HTTP 201, the header Location of the response will contain the URL of the created customer, the hexadecimal numbers after /customers/ is the id of the customer.

HTTP/1.1 201 Created

Location: /customers/5904fde985cc4d1c01ed9c1a

Content-Length: 0

Retrieve a customer

curl -i -X GET -H “Accept:application/json” http://localhost:8443/customers/5904fde985cc4d1c01ed9c1a

We use the id of the customer returned by the previous POST to retrieve a single cusomer, curl will return HTTP 200 and the body will contain a JSON representation of the customer.

HTTP/1.1 200 OK
………..
{
“id” : “5904fde985cc4d1c01ed9c1a”,
“first_name” : “John”,
“last_name” : “Doe”,
“birth_date” : “1990-01-01”,
“address” : {
“country” : “canada”
},
“customer_type” : “PERSON”
}

Notice that the id we pass in the GET request is returned in the field “id” of the response.

Retrieve all customers

curl -i -X GET -H “Accept:application/json” http://localhost:8443/customers

Will return HTTP 200

HTTP/1.1 200 OK
………..
[{
“id” : “5904fde985cc4d1c01ed9c1a”,
“first_name” : “John”,
“last_name” : “Doe”,
“birth_date” : “1990-01-01”,
“address” : {
“country” : “canada”
},
“customer_type” : “PERSON”
}]

This GET always returns a list customers (inside square brackets) even if only one customer is returned.

Delete a customer

curl -i -X DELETE http://localhost:8443/customers/5904fde985cc4d1c01ed9c1a

Will respond with HTTP 204 indicating that the customer has been successfully deleted.

Coming next…

We now have a fully functional Reactive RESTful service, in part2 we will see how to secure it with Auth2 and SSL and the impact it has on tests.

Thanks for reading.

Resources