├── .gitignore ├── LICENSE.txt ├── README.md ├── pom.xml ├── react-starter-demo.png └── src ├── main ├── java │ └── com │ │ └── reactive │ │ └── examples │ │ ├── ReactiveSpringBootApplication.java │ │ ├── client │ │ └── UserClient.java │ │ ├── config │ │ ├── CustomConnectionFactoryInitializer.java │ │ └── SwaggerConfiguration.java │ │ ├── controller │ │ ├── UserClientController.java │ │ └── UserController.java │ │ ├── dto │ │ └── UserDepartmentDTO.java │ │ ├── initialize │ │ └── UserInitializer.java │ │ ├── model │ │ ├── Department.java │ │ └── User.java │ │ ├── repository │ │ ├── DepartmentRepository.java │ │ └── UserRepository.java │ │ └── service │ │ └── UserService.java └── resources │ ├── application.yml │ └── schema.sql └── test └── java └── com └── reactive └── examples └── test └── controller └── UserControllerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | .idea/ 3 | *.iml 4 | *.iws 5 | target/ 6 | 7 | # Mac 8 | .DS_Store 9 | 10 | # Maven 11 | log/ 12 | target/ 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Suman Das 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample Reactive - Spring Boot application 2 | 3 | The purpose of this project is to demonstrate how we can 4 | use [Spring WebFlux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html) to create 5 | a simple reactive web application. 6 | 7 | This project uses [PostgreSQL](https://github.com/r2dbc/r2dbc-postgresql) implementation of the R2DBC SPI. 8 | 9 | # How to build and run 10 | 11 | project can be compiled with JDK 8 and above `javac`. 12 | 13 | To compile just do `mvn clean package`. 14 | 15 | ## Prerequisites 16 | 17 | * JAVA 8 should be installed 18 | * Postgres should be up and running at : 19 | 20 | To run the application execute the following: 21 | 22 | ``` 23 | java -jar target/reactive-examples*.jar 24 | ``` 25 | 26 | You can also use the Swagger-UI to test the application. 27 | ![alt text](react-starter-demo.png) 28 | 29 | for more detailed technical information please check my 30 | post : 31 | 32 | The server will start at . 33 | 34 | ## Exploring the Rest APIs 35 | 36 | The Swagger UI will open at : 37 | 38 | The application contains the following REST APIs 39 | 40 | ``` 41 | 1. GET /users - Get All Users 42 | 43 | 2. POST /users - To create a User 44 | 45 | 3. GET /users/{userId} - Retrieve an User by Id 46 | 47 | 3. PUT /users/{userId} - Update an User 48 | 49 | 4. DELETE /users/{userId} - Delete an User 50 | 51 | 4. GET /users/events - Stream users to a browser as Server-Sent Events 52 | ``` 53 | 54 | It contain a sample WebClient to retrieve data from our User Management application. 55 | 56 | 57 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.3.0.RC1 9 | 10 | 11 | com.reactive.examples 12 | reactive-examples 13 | 0.0.1-SNAPSHOT 14 | reactive-examples 15 | Demo reactive project for Spring Boot 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-data-r2dbc 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-webflux 29 | 30 | 31 | 32 | com.h2database 33 | h2 34 | runtime 35 | 36 | 37 | io.r2dbc 38 | r2dbc-h2 39 | runtime 40 | 41 | 42 | io.r2dbc 43 | r2dbc-postgresql 44 | runtime 45 | 46 | 47 | org.postgresql 48 | postgresql 49 | runtime 50 | 51 | 52 | org.projectlombok 53 | lombok 54 | true 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-test 59 | test 60 | 61 | 62 | org.junit.vintage 63 | junit-vintage-engine 64 | 65 | 66 | 67 | 68 | io.projectreactor 69 | reactor-test 70 | test 71 | 72 | 73 | 74 | io.springfox 75 | springfox-swagger2 76 | 3.0.0 77 | 78 | 79 | io.springfox 80 | springfox-swagger-ui 81 | 3.0.0 82 | 83 | 84 | io.springfox 85 | springfox-boot-starter 86 | 3.0.0 87 | 88 | 89 | io.springfox 90 | springfox-spring-webflux 91 | 3.0.0 92 | 93 | 94 | 95 | 96 | 97 | 98 | org.springframework.boot 99 | spring-boot-maven-plugin 100 | 101 | 102 | 103 | 104 | 105 | jcenter-snapshots 106 | jcenter 107 | http://oss.jfrog.org/artifactory/oss-snapshot-local/ 108 | 109 | 110 | spring-milestones 111 | Spring Milestones 112 | https://repo.spring.io/milestone 113 | 114 | 115 | 116 | 117 | spring-milestones 118 | Spring Milestones 119 | https://repo.spring.io/milestone 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /react-starter-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sumanentc/spring-webflux-reactive-rest-api-example/4461dcc678d1e22dcafa0b5df67faddcfa425a87/react-starter-demo.png -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/ReactiveSpringBootApplication.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ReactiveSpringBootApplication { 8 | public static void main(String[] args){ 9 | SpringApplication.run(ReactiveSpringBootApplication.class,args); 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/client/UserClient.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.client; 2 | 3 | import com.reactive.examples.model.User; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.reactive.function.client.WebClient; 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | 11 | @Component 12 | @Slf4j 13 | public class UserClient { 14 | private WebClient client = WebClient.create("http://localhost:8080"); 15 | public Mono getUser(String userId){ 16 | return client.get() 17 | .uri("/users/{userId}", userId) 18 | .retrieve() 19 | .bodyToMono(User.class).log(" User fetched "); 20 | } 21 | 22 | public Flux getAllUsers(){ 23 | return client.get() 24 | .uri("/users") 25 | .exchange().flatMapMany(clientResponse -> clientResponse.bodyToFlux(User.class)).log("Users Fetched : "); 26 | } 27 | 28 | public Mono createUser(User user){ 29 | Mono userMono = Mono.just(user); 30 | return client.post().uri("/users").contentType(MediaType.APPLICATION_JSON) 31 | .body(userMono,User.class).retrieve().bodyToMono(User.class).log("Created User : "); 32 | 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/config/CustomConnectionFactoryInitializer.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.config; 2 | 3 | import io.r2dbc.spi.ConnectionFactory; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.io.ClassPathResource; 7 | import org.springframework.data.r2dbc.connectionfactory.init.CompositeDatabasePopulator; 8 | import org.springframework.data.r2dbc.connectionfactory.init.ConnectionFactoryInitializer; 9 | import org.springframework.data.r2dbc.connectionfactory.init.ResourceDatabasePopulator; 10 | 11 | @Configuration 12 | public class CustomConnectionFactoryInitializer { 13 | @Bean 14 | public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { 15 | ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); 16 | initializer.setConnectionFactory(connectionFactory); 17 | CompositeDatabasePopulator populator = new CompositeDatabasePopulator(); 18 | populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("schema.sql"))); 19 | initializer.setDatabasePopulator(populator); 20 | return initializer; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/config/SwaggerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.reactive.config.WebFluxConfigurer; 6 | import springfox.documentation.builders.ApiInfoBuilder; 7 | import springfox.documentation.builders.PathSelectors; 8 | import springfox.documentation.builders.RequestHandlerSelectors; 9 | import springfox.documentation.service.ApiInfo; 10 | import springfox.documentation.spi.DocumentationType; 11 | import springfox.documentation.spring.web.plugins.Docket; 12 | 13 | @Configuration 14 | public class SwaggerConfiguration implements WebFluxConfigurer { 15 | @Bean 16 | public Docket api() { 17 | return new Docket(DocumentationType.SWAGGER_2) 18 | .apiInfo(apiInfo()) 19 | .enable(true) 20 | //.genericModelSubstitutes(Mono.class, Flux.class, Publisher.class) 21 | .select() 22 | .apis(RequestHandlerSelectors.any()) 23 | .paths(PathSelectors.any()) 24 | .build(); 25 | } 26 | 27 | private ApiInfo apiInfo() { 28 | return new ApiInfoBuilder().title("Reactive Stream Starter Demo") 29 | .description("Reactive Stream Starter Demo") 30 | .version("1.0").build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/controller/UserClientController.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.controller; 2 | 3 | import com.reactive.examples.client.UserClient; 4 | import com.reactive.examples.model.User; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | import reactor.core.publisher.Flux; 10 | import reactor.core.publisher.Mono; 11 | 12 | @RestController 13 | @RequestMapping("/client/users") 14 | public class UserClientController { 15 | 16 | @Autowired 17 | private UserClient userClient; 18 | 19 | @GetMapping("/{userId}") 20 | public Mono> getUserById(@PathVariable String userId){ 21 | Mono user = userClient.getUser(userId); 22 | return user.map( u -> ResponseEntity.ok(u)) 23 | .defaultIfEmpty(ResponseEntity.notFound().build()); 24 | } 25 | 26 | @GetMapping 27 | public Flux getAllUsers(){ 28 | return userClient.getAllUsers(); 29 | } 30 | 31 | @PostMapping 32 | @ResponseStatus(HttpStatus.CREATED) 33 | public Mono create(@RequestBody User user){ 34 | return userClient.createUser(user); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.controller; 2 | 3 | import com.reactive.examples.dto.UserDepartmentDTO; 4 | import com.reactive.examples.model.User; 5 | import com.reactive.examples.service.UserService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | import reactor.core.publisher.Flux; 11 | import reactor.core.publisher.Mono; 12 | 13 | import java.util.List; 14 | 15 | @RestController 16 | @RequestMapping("/users") 17 | public class UserController { 18 | @Autowired 19 | private UserService userService; 20 | 21 | @PostMapping 22 | @ResponseStatus(HttpStatus.CREATED) 23 | public Mono create(@RequestBody User user){ 24 | return userService.createUser(user); 25 | } 26 | 27 | @GetMapping 28 | public Flux getAllUsers(){ 29 | return userService.getAllUsers(); 30 | } 31 | 32 | @GetMapping("/{userId}") 33 | public Mono> getUserById(@PathVariable Integer userId){ 34 | Mono user = userService.findById(userId); 35 | return user.map( u -> ResponseEntity.ok(u)) 36 | .defaultIfEmpty(ResponseEntity.notFound().build()); 37 | } 38 | 39 | @PutMapping("/{userId}") 40 | public Mono> updateUserById(@PathVariable Integer userId, @RequestBody User user){ 41 | return userService.updateUser(userId,user) 42 | .map(updatedUser -> ResponseEntity.ok(updatedUser)) 43 | .defaultIfEmpty(ResponseEntity.badRequest().build()); 44 | } 45 | 46 | @DeleteMapping("/{userId}") 47 | public Mono> deleteUserById(@PathVariable Integer userId){ 48 | return userService.deleteUser(userId) 49 | .map( r -> ResponseEntity.ok().build()) 50 | .defaultIfEmpty(ResponseEntity.notFound().build()); 51 | } 52 | 53 | @GetMapping("/age/{age}") 54 | public Flux getUsersByAge(@PathVariable int age) { 55 | return userService.findUsersByAge(age); 56 | } 57 | 58 | @PostMapping("/search/id") 59 | public Flux fetchUsersByIds(@RequestBody List ids) { 60 | return userService.fetchUsers(ids); 61 | } 62 | 63 | @GetMapping("/{userId}/department") 64 | public Mono fetchUserAndDepartment(@PathVariable Integer userId){ 65 | return userService.fetchUserAndDepartment(userId); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/dto/UserDepartmentDTO.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @Builder 11 | @NoArgsConstructor 12 | public class UserDepartmentDTO { 13 | private Integer userId; 14 | private String userName; 15 | private int age; 16 | private double salary; 17 | private Integer departmentId; 18 | private String departmentName; 19 | private String loc; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/initialize/UserInitializer.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.initialize; 2 | 3 | import com.reactive.examples.model.Department; 4 | import com.reactive.examples.model.User; 5 | import com.reactive.examples.repository.DepartmentRepository; 6 | import com.reactive.examples.repository.UserRepository; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.stereotype.Component; 12 | import reactor.core.publisher.Flux; 13 | 14 | import java.util.Arrays; 15 | import java.util.List; 16 | 17 | @Component 18 | @Profile("!test") 19 | @Slf4j 20 | public class UserInitializer implements CommandLineRunner { 21 | 22 | @Autowired 23 | private UserRepository userRepository; 24 | 25 | @Autowired 26 | private DepartmentRepository departmentRepository; 27 | 28 | @Override 29 | public void run(String... args) { 30 | initialDataSetup(); 31 | } 32 | 33 | private List getData(){ 34 | return Arrays.asList(new User(null,"Suman Das",30,10000), 35 | new User(null,"Arjun Das",5,1000), 36 | new User(null,"Saurabh Ganguly",40,1000000)); 37 | } 38 | 39 | private List getDepartments(){ 40 | return Arrays.asList(new Department(null,"Mechanical",1,"Mumbai"), 41 | new Department(null,"Computer",2,"Bangalore")); 42 | } 43 | 44 | private void initialDataSetup() { 45 | userRepository.deleteAll() 46 | .thenMany(Flux.fromIterable(getData())) 47 | .flatMap(userRepository::save) 48 | .thenMany(userRepository.findAll()) 49 | .subscribe(user -> { 50 | log.info("User Inserted from CommandLineRunner " + user); 51 | }); 52 | 53 | departmentRepository.deleteAll() 54 | .thenMany(Flux.fromIterable(getDepartments())) 55 | .flatMap(departmentRepository::save) 56 | .thenMany(departmentRepository.findAll()) 57 | .subscribe(user -> { 58 | log.info("Department Inserted from CommandLineRunner " + user); 59 | }); 60 | 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/model/Department.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.data.annotation.Id; 7 | import org.springframework.data.relational.core.mapping.Column; 8 | import org.springframework.data.relational.core.mapping.Table; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | @Table("department") 14 | public class Department { 15 | @Id 16 | private Integer id; 17 | private String name; 18 | @Column("user_id") 19 | private Integer userId; 20 | private String loc; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/model/User.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.data.annotation.Id; 7 | import org.springframework.data.relational.core.mapping.Table; 8 | 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | @Table("users") 14 | public class User { 15 | 16 | @Id 17 | private Integer id; 18 | private String name; 19 | private int age; 20 | private double salary; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/repository/DepartmentRepository.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.repository; 2 | 3 | import com.reactive.examples.model.Department; 4 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 5 | import reactor.core.publisher.Mono; 6 | 7 | public interface DepartmentRepository extends ReactiveCrudRepository { 8 | Mono findByUserId(Integer userId); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.repository; 2 | 3 | import com.reactive.examples.model.User; 4 | import org.springframework.data.r2dbc.repository.Query; 5 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 6 | import reactor.core.publisher.Flux; 7 | 8 | public interface UserRepository extends ReactiveCrudRepository { 9 | @Query("select * from users where age >= $1") 10 | Flux findByAge(int age); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/reactive/examples/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.service; 2 | 3 | import com.reactive.examples.dto.UserDepartmentDTO; 4 | import com.reactive.examples.model.Department; 5 | import com.reactive.examples.model.User; 6 | import com.reactive.examples.repository.DepartmentRepository; 7 | import com.reactive.examples.repository.UserRepository; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | import reactor.core.scheduler.Schedulers; 15 | 16 | import java.util.List; 17 | import java.util.function.BiFunction; 18 | 19 | @Service 20 | @Slf4j 21 | @Transactional 22 | public class UserService { 23 | 24 | @Autowired 25 | private UserRepository userRepository; 26 | 27 | @Autowired 28 | private DepartmentRepository departmentRepository; 29 | 30 | public Mono createUser(User user){ 31 | return userRepository.save(user); 32 | } 33 | 34 | public Flux getAllUsers(){ 35 | return userRepository.findAll(); 36 | } 37 | 38 | public Mono findById(Integer userId){ 39 | return userRepository.findById(userId); 40 | } 41 | 42 | public Mono updateUser(Integer userId, User user){ 43 | return userRepository.findById(userId) 44 | .flatMap(dbUser -> { 45 | dbUser.setAge(user.getAge()); 46 | dbUser.setSalary(user.getSalary()); 47 | return userRepository.save(dbUser); 48 | }); 49 | } 50 | 51 | public Mono deleteUser(Integer userId){ 52 | return userRepository.findById(userId) 53 | .flatMap(existingUser -> userRepository.delete(existingUser) 54 | .then(Mono.just(existingUser))); 55 | } 56 | 57 | public Flux findUsersByAge(int age){ 58 | return userRepository.findByAge(age); 59 | } 60 | 61 | public Flux fetchUsers(List userIds) { 62 | return Flux.fromIterable(userIds) 63 | .parallel() 64 | .runOn(Schedulers.elastic()) 65 | .flatMap(i -> findById(i)) 66 | .ordered((u1, u2) -> u2.getId() - u1.getId()); 67 | } 68 | 69 | private Mono getDepartmentByUserId(Integer userId){ 70 | return departmentRepository.findByUserId(userId); 71 | } 72 | 73 | public Mono fetchUserAndDepartment(Integer userId){ 74 | Mono user = findById(userId).subscribeOn(Schedulers.elastic()); 75 | Mono department = getDepartmentByUserId(userId).subscribeOn(Schedulers.elastic()); 76 | return Mono.zip(user, department, userDepartmentDTOBiFunction); 77 | } 78 | 79 | private BiFunction userDepartmentDTOBiFunction = (x1, x2) -> UserDepartmentDTO.builder() 80 | .age(x1.getAge()) 81 | .departmentId(x2.getId()) 82 | .departmentName(x2.getName()) 83 | .userName(x1.getName()) 84 | .userId(x1.getId()) 85 | .loc(x2.getLoc()) 86 | .salary(x1.getSalary()).build(); 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: dev 4 | --- 5 | spring: 6 | profiles: dev 7 | r2dbc: 8 | url: r2dbc:postgresql://localhost:5432/test 9 | username: postgres 10 | password: postgres 11 | logging: 12 | level: 13 | org.springframework.data.r2dbc: Debug 14 | --- 15 | spring: 16 | profiles: test 17 | r2dbc: 18 | url: r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE 19 | name: sa 20 | password: 21 | --- 22 | spring: 23 | profiles: prod 24 | r2dbc: 25 | url: r2dbc:postgresql://localhost:5432/test 26 | username: postgres 27 | password: postgres 28 | logging: 29 | level: 30 | org.springframework.data.r2dbc: Debug 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users ; 2 | CREATE TABLE users ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, age integer,salary decimal); 3 | DROP TABLE IF EXISTS department ; 4 | CREATE TABLE department ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,user_id integer, name VARCHAR(100) NOT NULL, loc VARCHAR(100)); -------------------------------------------------------------------------------- /src/test/java/com/reactive/examples/test/controller/UserControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.reactive.examples.test.controller; 2 | 3 | import com.reactive.examples.model.User; 4 | import com.reactive.examples.repository.UserRepository; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.data.r2dbc.core.DatabaseClient; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.test.annotation.DirtiesContext; 15 | import org.springframework.test.context.ActiveProfiles; 16 | import org.springframework.test.context.junit.jupiter.SpringExtension; 17 | import org.springframework.test.web.reactive.server.WebTestClient; 18 | import reactor.core.publisher.Flux; 19 | import reactor.core.publisher.Mono; 20 | import reactor.test.StepVerifier; 21 | 22 | import java.util.Arrays; 23 | import java.util.List; 24 | 25 | import static org.junit.jupiter.api.Assertions.assertTrue; 26 | 27 | 28 | @ExtendWith(SpringExtension.class) 29 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 30 | @DirtiesContext 31 | @AutoConfigureWebTestClient 32 | @ActiveProfiles("test") 33 | @Slf4j 34 | public class UserControllerTest { 35 | 36 | @Autowired 37 | private WebTestClient webTestClient; 38 | 39 | @Autowired 40 | private UserRepository userRepository; 41 | 42 | 43 | @Autowired 44 | private DatabaseClient databaseClient; 45 | 46 | 47 | private List getData(){ 48 | return Arrays.asList(new User(null,"Suman Das",30,10000), 49 | new User(null,"Arjun Das",5,1000), 50 | new User(null,"Saurabh Ganguly",40,1000000)); 51 | } 52 | 53 | @BeforeEach 54 | public void setup(){ 55 | List statements = Arrays.asList("DROP TABLE IF EXISTS users ;", 56 | "CREATE TABLE users ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, age integer,salary decimal);"); 57 | 58 | statements.forEach(it -> databaseClient.execute(it) 59 | .fetch() 60 | .rowsUpdated() 61 | .block()); 62 | 63 | userRepository.deleteAll() 64 | .thenMany(Flux.fromIterable(getData())) 65 | .flatMap(userRepository::save) 66 | .doOnNext(user ->{ 67 | System.out.println("User Inserted from UserControllerTest: " + user); 68 | }) 69 | .blockLast(); 70 | 71 | } 72 | 73 | @Test 74 | public void getAllUsersValidateCount(){ 75 | webTestClient.get().uri("/users").exchange() 76 | .expectStatus().isOk() 77 | .expectHeader().contentType(MediaType.APPLICATION_JSON_VALUE) 78 | .expectBodyList(User.class) 79 | .hasSize(3) 80 | .consumeWith(user ->{ 81 | List users = user.getResponseBody(); 82 | users.forEach( u ->{ 83 | assertTrue(u.getId() != null); 84 | }); 85 | }); 86 | } 87 | @Test 88 | public void getAllUsersValidateResponse(){ 89 | Flux userFlux = webTestClient.get().uri("/users").exchange() 90 | .expectStatus().isOk() 91 | .expectHeader().contentType(MediaType.APPLICATION_JSON_VALUE) 92 | .returnResult(User.class) 93 | .getResponseBody(); 94 | StepVerifier.create(userFlux.log("Receiving values !!!")) 95 | .expectNextCount(3) 96 | .verifyComplete(); 97 | 98 | } 99 | @Test 100 | public void getUserById(){ 101 | webTestClient.get().uri("/users".concat("/{userId}"),"1") 102 | .exchange().expectStatus().isOk() 103 | .expectBody() 104 | .jsonPath("$.name","Suman Das"); 105 | } 106 | @Test 107 | public void getUserById_NotFound(){ 108 | webTestClient.get().uri("/users".concat("/{userId}"),"6") 109 | .exchange().expectStatus().isNotFound(); 110 | } 111 | @Test 112 | public void createUser(){ 113 | User user = new User(null,"Rahul Dravid",45,5555555); 114 | webTestClient.post().uri("/users").contentType(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE)) 115 | .body(Mono.just(user),User.class) 116 | .exchange() 117 | .expectStatus().isCreated() 118 | .expectBody() 119 | .jsonPath("$.id").isNotEmpty() 120 | .jsonPath("$.name").isEqualTo("Rahul Dravid"); 121 | } 122 | @Test 123 | public void deleteUser(){ 124 | webTestClient.delete().uri("/users".concat("/{userId}"),"1") 125 | .accept(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE)) 126 | .exchange() 127 | .expectStatus().isOk() 128 | .expectBody(Void.class); 129 | } 130 | @Test 131 | public void updateUser(){ 132 | double newsalary = 12345; 133 | User user = new User(null,"Suman Das",31,newsalary); 134 | webTestClient.put().uri("/users".concat("/{userId}"),"1") 135 | .contentType(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE)) 136 | .accept(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE)) 137 | .body(Mono.just(user),User.class) 138 | .exchange() 139 | .expectStatus().isOk() 140 | .expectBody() 141 | .jsonPath("$.salary").isEqualTo(newsalary); 142 | } 143 | @Test 144 | public void updateUser_notFound(){ 145 | double newsalary = 12345; 146 | User user = new User(null,"Suman Das",31,newsalary); 147 | webTestClient.put().uri("/users".concat("/{userId}"),"6") 148 | .contentType(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE)) 149 | .accept(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE)) 150 | .body(Mono.just(user),User.class) 151 | .exchange() 152 | .expectStatus().isBadRequest(); 153 | } 154 | /* 155 | @Test 156 | public void testStreamUsers(){ 157 | setUpStreamingData(); 158 | Flux userFlux = webTestClient.get().uri("/users/events").exchange() 159 | .expectStatus().isOk() 160 | .returnResult(UserCapped.class) 161 | .getResponseBody() 162 | .take(5); 163 | StepVerifier.create(userFlux) 164 | .expectNextCount(5) 165 | .verifyComplete(); 166 | } 167 | 168 | private void setUpStreamingData(){ 169 | mongoOperations.dropCollection(UserCapped.class); 170 | mongoOperations.createCollection(UserCapped.class, CollectionOptions.empty().maxDocuments(20).size(50000).capped()); 171 | Flux userCappedFlux = Flux.interval(Duration.ofSeconds(1)) 172 | .map(i -> new UserCapped(null,"Stream User " + i,20,1000)).take(5); 173 | 174 | userCappedRepository.insert(userCappedFlux) 175 | .doOnNext( 176 | item -> log.info("UserCapped Inserted from CommandLineRunner " + item)) 177 | .blockLast(); 178 | } 179 | 180 | */ 181 | } 182 | --------------------------------------------------------------------------------