If you are a Java programmer chances are that you have worked with Spring Boot and you may or may not have heard of project reactor, WebFlux etc. In case you haven’t, then Spring Project Reactor is a spring library with which you can build Spring Reactive systems i.e. asynchronous or non-blocking APIs. This post will avoid getting into too many details for reactive systems as that topic deserves it’s own dedicated blogpost. I will write that sometime in the future. Hence, this post will focus on integration with Spring via frameworks and other implementation details. In this post you will build a reactive Spring API using WebFlux that saves data in MongoDB and manages it using Spring data-mongodb-reactive.
Reactive systems are here to stay, and it would only make sense for you to get familiar with it.
Build spring reactive api
For this let’s follow a logical approach whereby you will first be coding the controllers, followed by the classes in the Service layer followed by the repository. In addition to all this, you will also be adding some unit tests for both the controller and the service layer.
A simple cars API with a MongoDB backend
As you may have noticed from the above paragraph, in this tutorial you will build a simple Spring reactive API to CRUD car data to a MongoDB database.
CarController
import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import com.mydaytodo.tutorials.spring.reactive.mongo.service.CarService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/car")
@Slf4j
public class CarController {
private final CarService carService;
public CarController(CarService carService) {
this.carService = carService;
}
@GetMapping
public Flux<Car> getCars() {
return carService.getAllCars();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<Car> createCar(@RequestBody Car car) {
return carService.create(car);
}
@GetMapping("/{id}")
public Mono<Car> getCarById(@PathVariable("id") String id) {
return carService.getCarById(id);
}
@DeleteMapping("/{id}")
public Mono<Void> deleteCar(@PathVariable String id) {
return carService.deleteCar(id);
}
@PutMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Car> updateCar(@PathVariable("id") String id, @RequestBody Car car) {
return carService.updateCar(id, car);
}
}
CarService
import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import com.mydaytodo.tutorials.spring.reactive.mongo.repository.CarRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
@Slf4j
public class CarService {
@Autowired
private CarRepository carRepository;
private final Car TEST_CAR = Car.builder().name("BMW").build();
private final Car TEST_CAR_2 = Car.builder().name("MG").build();
public CarService(CarRepository carRepository) {
this.carRepository = carRepository;
}
public Flux<Car> getAllCars() {
log.info("In the final all cars");
return carRepository.findAll();
}
public Mono<Car> getCarById(String id) {
return carRepository.findById(id);
}
public Mono<Car> getCarByName(String name) {
return carRepository.findByName(name);
}
public Mono<Car> create(Car car) {
return carRepository.save(car);
}
public Mono<Void> deleteCar(String id) {
return carRepository.deleteById(id);
}
public Mono<Car> updateCar(String id, Car car) {
return carRepository.findById(id)
.flatMap(existingCar -> {
existingCar.setName(car.getName());
return carRepository.save(existingCar);
});
}
}
CarRepository
import com.mongodb.lang.NonNull;
import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import reactor.core.publisher.Mono;
public interface CarRepository extends ReactiveMongoRepository<Car, String> {
Mono<Car> findByName(String id);
}
Car Model
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Document(collection = "cars")
@Builder
public class Car {
@Id
private String id;
private String name;
}
Add some unit tests
- CarController
import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import com.mydaytodo.tutorials.spring.reactive.mongo.service.CarService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import reactor.core.publisher.Mono;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
@Slf4j
@WebFluxTest(CarController.class)
public class CarControllerTest {
private MockMvc mockMvc;
@Autowired
private WebTestClient webTestClient;
private static final String TEST_ID = "ISRD_1221";
@MockBean
private CarService carService;
private final Car TEST_CAR = Car.builder().name("BMW").build();
private final Car TEST_CAR_2 = Car.builder().name("MG").build();
@BeforeEach
void setup() {
}
@Test
void testCreateCar() {
given(carService.create(any())).willReturn(Mono.just(TEST_CAR));
webTestClient.post().uri("/car")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(TEST_CAR)
.exchange()
.expectStatus().isCreated()
.expectBody(Car.class).isEqualTo(TEST_CAR);
}
@Test
void testGetCarById() {
given(carService.getCarById(any())).willReturn(Mono.just(TEST_CAR));
webTestClient.get().uri("/car/"+TEST_ID)
.exchange()
.expectStatus().isOk()
.expectBody(Car.class).isEqualTo(TEST_CAR);
}
@Test
void testDeleteCar() {
given(carService.getCarById(any())).willReturn(Mono.just(TEST_CAR));
webTestClient.delete().uri("/car/"+TEST_ID)
.exchange()
.expectStatus().isOk();
}
@Test
void updateCar() {
given(carService.updateCar(any(), any())).willReturn(Mono.just(TEST_CAR));
webTestClient.put().uri("/car/"+TEST_ID)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(TEST_CAR)
.exchange()
.expectStatus().isNoContent();
}
}
- CarService
import com.mydaytodo.tutorials.spring.reactive.mongo.model.Car;
import com.mydaytodo.tutorials.spring.reactive.mongo.repository.CarRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@Slf4j
@ExtendWith(MockitoExtension.class)
public class CarServiceTest {
@Mock
private CarRepository carRepository;
@InjectMocks
private CarService carService;
private final Car TEST_CAR = Car.builder().name("BMW").build();
private final Car TEST_CAR_2 = Car.builder().name("MG").build();
private static final String TEST_ID = "ISRD_1221";
private static final String TEST_NAME = "i30";
@BeforeEach
public void setup() {
}
@Test
public void testGetCar() {
when(carRepository.findById(anyString())).thenReturn(Mono.just(Car.builder().name("BMW").build()));
Mono<Car> carByIdRes = carService.getCarById(TEST_ID);
StepVerifier.create(carByIdRes)
.expectNextMatches(car -> car.getName().equals("BMW"))
.verifyComplete();
}
@Test
public void testGetCarByName() {
when(carRepository.findByName(anyString())).thenReturn(Mono.just(Car.builder().name("i30").build()));
Mono<Car> carByIdRes = carService.getCarByName(TEST_NAME);
StepVerifier.create(carByIdRes)
.expectNextMatches(car -> car.getName().equals(TEST_NAME))
.verifyComplete();
}
@Test
public void testCreateCar() {
when(carRepository.save(any())).thenReturn(Mono.just(TEST_CAR));
Mono<Car> createCar = carService.create(TEST_CAR);
StepVerifier.create(createCar)
.expectNextMatches(car -> car.getName().equals(TEST_CAR.getName()))
.verifyComplete();
}
@Test
public void testDeleteCar() {
when(carRepository.deleteById(anyString())).thenReturn(Mono.empty());
Mono<Void> delRet = carService.deleteCar(TEST_ID);
StepVerifier.create(delRet)
.expectComplete()
.verify();
}
@Test
public void testUpdateCar() {
when(carRepository.findById(anyString())).thenReturn(Mono.just(TEST_CAR));
when(carRepository.save(any())).thenReturn(Mono.just(TEST_CAR_2));
Mono<Car> savedCar = carService.updateCar(TEST_ID, TEST_CAR_2);
StepVerifier.create(savedCar)
.expectNextMatches(car -> car.getName().equals(TEST_CAR_2.getName()))
.verifyComplete();
}
@Test
public void testGetAllCars() {
when(carRepository.findAll()).thenReturn(Flux.just(TEST_CAR, TEST_CAR_2));
Flux<Car> carStream = carService.getAllCars();
StepVerifier.create(carStream)
.expectNext(TEST_CAR)
.expectNext(TEST_CAR_2)
.verifyComplete();
}
}
How can you see test coverage for your code?
You can do this using this tool called jacoco, which is an open-source Java tool that can help you see what lines of your code are tested. Jacoco is short for say, Java code coverage. This is the plugin you need to add to your pom.xml.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- attached to Maven test phase -->
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
How to run this API?
You can get all the source code for this api on Github here. To run this api,
git clone git@github.com:cptdanko/reactive-spring-mongo.git
cd reactive-spring-mongo/reactive.mongo
mvn clean install
mvn spring-boot:run
Conclusion
Reactive systems are here to stay so I hope you found this little tutorial useful and feel more comfortable about building reactive systems after reading this.
If you find any of my posts useful and want to support me, you can buy me a coffee 🙂
https://www.buymeacoffee.com/bhumansoni
While you are here, maybe try one of my apps for the iPhone.
Products – My Day To-Do (mydaytodo.com)
Here are some of my other bloposts on Java
How to build a full stack Spring boot API with ReactJS frontend – My Day To-Do (mydaytodo.com)
How to call REST API with WebClient – My Day To-Do (mydaytodo.com)
How to build a jokes client in Java Spring Boot with RestTemplate – My Day To-Do (mydaytodo.com)
Have a read of some of my other posts on AWS
Upload to AWS S3 bucket from Java Spring Boot app – My Day To-Do (mydaytodo.com)
Deploy NodeJS, Typescript app on AWS Elastic beanstalk – (mydaytodo.com)
How to deploy spring boot app to AWS & serve via https – My Day To-Do (mydaytodo.com)
3 Comments
How to build a blog engine with React & Spring Boot - Part 1 - My Day To-Do · September 21, 2024 at 12:28 am
[…] How to build a Spring Reactive API […]
How to unit test react-redux app - My Day To-Do · September 22, 2024 at 5:43 am
[…] How to build a Spring Reactive API […]
How to build a blog engine with React & Spring Boot – Part 3 CI/CD pipeline · October 4, 2024 at 10:35 pm
[…] How to build a Spring Reactive API […]