Spring WebFlux supports two types of programming models:
- The traditional annotation-based model with @Controller, @RestController, @RequestMapping, and other annotations that you have been using in Spring MVC.
- A brand new Functional style model based on Java 8 lambdas for routing and handling requests.
In the previous tutorial, we have seen how to use a traditional annotation-based programming model to build reactive CRUD REST APIs.
In this tutorial, we will new functional-style programming model to build reactive CRUD REST APIs using Spring Boot 3, Spring WebFlux, MongoDB, and IntelliJ IDEA.
1. Create Spring Boot Application
Let's create a Spring boot application using Spring Intializr.
Refer to the below screenshot to enter the details while generating the Spring boot project using Spring Intializr:
Note that we are using Spring WebFlux, MongoDB Reactive, and Lombok libraries.
Here is the complete pom.xml file for your reference:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>net.javaguides</groupId>
<artifactId>springboot-webflux-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-webflux-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. Project Structure
Refer to the below screenshot to create the packing or project structure for the application:
3. Configure MongoDB
You can configure MongoDB by simply adding the following property to the application.properties file:
spring.data.mongodb.uri=mongodb://localhost:27017/ems
Spring Boot will read this configuration on startup and automatically configure the data source.
4. Create Domain Class
Let's create a Post MongoDB document and add the following content to it:
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.time.LocalDateTime;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Document
public class Post {
@Id
private String id;
private String title;
private String description;
private String body;
@Field(name = "created_on")
private LocalDateTime createdOn;
@Field(name = "updated_on")
private LocalDateTime updatedOn;
}
5. Creating Repository - EmployeeRepository
Next, we’re going to create the data access layer which will be used to access the MongoDB database.
Let's create a PostReactiveRepository interface and add the following content to it:
package net.javaguides.springbootwebfluxdemo.repository;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;
import net.javaguides.springbootwebfluxdemo.entity.Post;
public interface PostReactiveRepository extends ReactiveCrudRepository<Post, String> {
Mono<Boolean> existsByTitle(String title);
}
The PostReactiveRepository interface extends from ReactiveMongoRepository which exposes various CRUD methods on the Document. Spring Boot automatically plugs in an implementation of this interface called SimpleReactiveMongoRepository at runtime.
So you get all the CRUD methods on the Document readily available to you without needing to write any code.
6. Create PostMapper - Map Entity to Dto and Vice Versa
Let's create PostMapper class to map an entity to Dto and vice versa:
package net.javaguides.springbootwebfluxdemo.mapper;
import org.springframework.stereotype.Component;
import net.javaguides.springbootwebfluxdemo.dto.PostDto;
import net.javaguides.springbootwebfluxdemo.entity.Post;
@Component
public class PostMapper {
public Post mapToPost(PostDto postInput) {
return Post.builder()
.title(postInput.getTitle())
.description(postInput.getDescription())
.body(postInput.getBody())
.build();
}
public PostDto mapToPostDto(Post post) {
return PostDto.builder()
.id(post.getId())
.title(post.getTitle())
.description(post.getDescription())
.body(post.getBody())
.build();
}
}
7. Create a Service Layer
PostService Interface
Let's create a PostService interface and add below CRUD methods to it:
package net.javaguides.springbootwebfluxdemo.service;
import net.javaguides.springbootwebfluxdemo.dto.PostDto;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface PostService {
Mono<PostDto> save(PostDto postDto);
Flux<PostDto> findAllPosts();
Mono<PostDto> update(PostDto postDto, String id);
Mono<Void> delete(String id);
}
PostServiceImpl class
Let's create PostServiceImpl class that implements the PostService interface and its methods:
package net.javaguides.springbootwebfluxdemo.service.impl;
import lombok.AllArgsConstructor;
import net.javaguides.springbootwebfluxdemo.service.PostService;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import net.javaguides.springbootwebfluxdemo.repository.PostReactiveRepository;
import net.javaguides.springbootwebfluxdemo.mapper.PostMapper;
import net.javaguides.springbootwebfluxdemo.dto.PostDto;
import net.javaguides.springbootwebfluxdemo.entity.Post;
import java.time.LocalDateTime;
@Service
@AllArgsConstructor
public class PostServiceImpl implements PostService {
private PostReactiveRepository postReactiveRepository;
private PostMapper postMapper;
@Override
public Mono<PostDto> save(PostDto postDto) {
Post post = postMapper.mapToPost(postDto);
post.setCreatedOn(LocalDateTime.now());
post.setUpdatedOn(LocalDateTime.now());
return postReactiveRepository.save(post).map(p -> {
postDto.setId(p.getId());
return postDto;
}
);
}
@Override
public Flux<PostDto> findAllPosts() {
return postReactiveRepository.findAll()
.map(postMapper::mapToPostDto)
.switchIfEmpty(Flux.empty());
}
public Boolean postExistsWithTitle(String title) {
return postReactiveRepository.existsByTitle(title).block();
}
@Override
public Mono<PostDto> update(PostDto postDto, String id) {
return postReactiveRepository.findById(id)
.flatMap(savedPost -> {
Post post = postMapper.mapToPost(postDto);
post.setId(savedPost.getId());
return postReactiveRepository.save(post);
})
.map(postMapper::mapToPostDto);
}
@Override
public Mono<Void> delete(String id) {
return postReactiveRepository.deleteById(id);
}
}
8. Create Controller Layer - Define Handler Function
Let's create a Handler function for CRUD operations.
Let's create PostHandler class and add the following content to it:
package net.javaguides.springbootwebfluxdemo.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import net.javaguides.springbootwebfluxdemo.service.impl.PostServiceImpl;
import net.javaguides.springbootwebfluxdemo.dto.PostDto;
@Component
@RequiredArgsConstructor
public class PostHandler {
private final PostServiceImpl postService;
public Mono<ServerResponse> listPosts(ServerRequest serverRequest) {
Flux<PostDto> allPosts = postService.findAllPosts();
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(allPosts, PostDto.class)
.switchIfEmpty(notFound);
}
public Mono<ServerResponse> savePost(ServerRequest serverRequest) {
Mono<PostDto> postDtoMono = serverRequest.bodyToMono(PostDto.class);
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
return postDtoMono.flatMap(postDto ->
ServerResponse
.status(HttpStatus.CREATED)
.contentType(MediaType.APPLICATION_JSON)
.body(postService.save(postDto), PostDto.class))
.switchIfEmpty(notFound);
}
public Mono<ServerResponse> updatePost(ServerRequest serverRequest) {
String id = serverRequest.pathVariable("id");
Mono<PostDto> postDtoMono = serverRequest.bodyToMono(PostDto.class);
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
return postDtoMono.flatMap(postDto ->
ServerResponse
.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(postService.update(postDto, id), PostDto.class))
.switchIfEmpty(notFound);
}
public Mono<ServerResponse> deletePost(ServerRequest serverRequest) {
String id = serverRequest.pathVariable("id");
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
return ServerResponse
.status(HttpStatus.NO_CONTENT)
.build(postService.delete(id))
.switchIfEmpty(notFound);
}
}
The ServerRequest provides access to the HTTP method, URI, headers, and query parameters, while access to the body is provided through the body methods.
The ServerResponse provides access to the HTTP response and, since it is immutable, you can use a build method to create it.
9. Define Route Function to Route the Requests
RouterFunction is the equivalent of a @RequestMapping annotation, but with the major difference that router functions provide not just data, but also behavior.
package net.javaguides.springbootwebfluxdemo;
import net.javaguides.springbootwebfluxdemo.controller.PostHandler;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.nest;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@SpringBootApplication
public class SpringbootWebfluxDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebfluxDemoApplication.class, args);
}
@Bean
RouterFunction<ServerResponse> routes(PostHandler postHandler) {
return
nest(path("/api/posts"),
nest(accept(MediaType.APPLICATION_JSON),
route(method(HttpMethod.GET), postHandler::listPosts)
.andRoute(DELETE("/{id}"), postHandler::deletePost)
.andRoute(POST("/"), postHandler::savePost)
.andRoute(PUT("/{id}"), postHandler::updatePost)));
}
}
10. Testing Reactive CRUD REST APIs using WebClientTest Class
Let's write the Integration test cases to test functional endpoints CRUD REST APIs using the WebTestClient class.
We are using the below WebTestClient class method to prepare CRUD REST API requests:
post() - Prepare an HTTP POST request.
delete() - Prepare an HTTP DELETE request.
get() - Prepare an HTTP GET request.
put() - Prepare an HTTP PUT request.
Here is the complete code for testing Spring WebFlux Reactive CRUD Rest APIs using WebTestClient:
package net.javaguides.springbootwebfluxdemo;
import net.javaguides.springbootwebfluxdemo.dto.EmployeeDto;
import net.javaguides.springbootwebfluxdemo.dto.PostDto;
import net.javaguides.springbootwebfluxdemo.entity.Post;
import net.javaguides.springbootwebfluxdemo.service.EmployeeService;
import net.javaguides.springbootwebfluxdemo.service.PostService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import java.util.Collections;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostHandlerTests {
@Autowired
private WebTestClient webTestClient;
//@MockBean
@Autowired
private PostService postService;
@Test
public void testCreatePost() throws Exception {
PostDto post = new PostDto();
post.setTitle("Blog Post 1");
post.setDescription("Blog Post 1 Description");
post.setBody("Blog Post 1 Body");
// when - action or behaviour that we are going test
webTestClient.post().uri("/api/posts/")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(post), PostDto.class)
.exchange()
.expectStatus().isCreated()
.expectBody()
.consumeWith(System.out::println)
.jsonPath("$.title").isEqualTo(post.getTitle())
.jsonPath("$.description").isEqualTo(post.getDescription())
.jsonPath("$.body").isEqualTo(post.getBody());
}
@Test
public void testGetAllPosts() {
webTestClient.get().uri("/api/posts")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBodyList(PostDto.class)
.consumeWith(System.out::println);
}
@Test
public void testUpdateEmployee() throws Exception {
PostDto post = new PostDto();
post.setTitle("Blog Post 1");
post.setDescription("Blog Post 1 Description");
post.setBody("Blog Post 1 Body");
PostDto updatedPost = new PostDto();
updatedPost.setTitle("Blog Post 1 updated");
updatedPost.setDescription("Blog Post 1 Description updated");
updatedPost.setBody("Blog Post 1 Body updated");
PostDto savedPost = postService.save(post).block();
webTestClient.put()
.uri("api/posts/{id}", Collections.singletonMap("id", savedPost.getId()))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(updatedPost), PostDto.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.consumeWith(System.out::println)
.jsonPath("$.title").isEqualTo(updatedPost.getTitle())
.jsonPath("$.description").isEqualTo(updatedPost.getDescription())
.jsonPath("$.body").isEqualTo(updatedPost.getBody());
}
@Test
public void testDeletePost() {
PostDto post = new PostDto();
post.setTitle("Blog Post 2");
post.setDescription("Blog Post 2 Description");
post.setBody("Blog Post 2 Body");
PostDto savedPost = postService.save(post).block();
webTestClient.delete()
.uri("/api/posts/{id}", Collections.singletonMap("id", savedPost.getId()))
.exchange()
.expectStatus().isNoContent()
.expectBody()
.consumeWith(System.out::println);
}
}
Output:
Here is the output of all the JUnit test cases:
Conclusion
In the previous tutorial, we have seen how to use a traditional annotation-based programming model to build reactive CRUD REST APIs.
In this tutorial, we have seen how to use a functional-style programming model to build reactive CRUD REST APIs.
Check out all Spring boot tutorials at https://www.javaguides.net/p/spring-boot-tutorial.html
Check out all Microservices tutorials at https://www.javaguides.net/p/spring-boot-microservices-tutorial.html
Comments
Post a Comment
Leave Comment