diff --git a/2022-11/spring-22-webflux/.gitignore b/2022-11/spring-22-webflux/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2022-11/spring-22-webflux/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2022-11/spring-22-webflux/HttpRequests.http b/2022-11/spring-22-webflux/HttpRequests.http
new file mode 100644
index 00000000..d731fea4
--- /dev/null
+++ b/2022-11/spring-22-webflux/HttpRequests.http
@@ -0,0 +1,36 @@
+###
+GET http://localhost:8080/flux/one
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+GET http://localhost:8080/flux/ten
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+GET http://localhost:8080/person
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+GET http://localhost:8080/person/637d2eeef46b8331e91ca40a
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+GET http://localhost:8080/func/person?name=Lermontov
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
+###
+GET http://localhost:8080/func/person?age=22
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
+
diff --git a/2022-11/spring-22-webflux/pom.xml b/2022-11/spring-22-webflux/pom.xml
new file mode 100644
index 00000000..1c8bdb2a
--- /dev/null
+++ b/2022-11/spring-22-webflux/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-22-webflux
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.7.5
+
+
+
+
+ 11
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb-reactive
+
+
+ de.flapdoodle.embed
+ de.flapdoodle.embed.mongo
+
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/FunctionalEndpointsConfig.java b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/FunctionalEndpointsConfig.java
new file mode 100644
index 00000000..70acbca7
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/FunctionalEndpointsConfig.java
@@ -0,0 +1,68 @@
+package ru.otus.spring;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.web.reactive.function.BodyInserters.fromValue;
+import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
+import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
+import static org.springframework.web.reactive.function.server.RouterFunctions.route;
+import static org.springframework.web.reactive.function.server.ServerResponse.badRequest;
+import static org.springframework.web.reactive.function.server.ServerResponse.notFound;
+import static org.springframework.web.reactive.function.server.ServerResponse.ok;
+
+@Configuration
+public class FunctionalEndpointsConfig {
+ @Bean
+ public RouterFunction composedRoutes(PersonRepository repository) {
+ return route()
+ // эта функция должна стоять раньше findAll - порядок следования роутов - важен
+ .GET("/func/person",
+ queryParam("name", StringUtils::isNotEmpty),
+ request -> request.queryParam("name")
+ .map(repository::findAllByLastName)
+ .map(person -> ok().body(person, Person.class))
+ .orElse(badRequest().build())
+ )
+ // пример другой реализации - начиная с запроса репозитория
+ .GET("/func/person", queryParam("age", StringUtils::isNotEmpty),
+ req ->
+ repository
+ .findAllByAge(req.queryParam("age").map(Integer::parseInt)
+ .orElseThrow(IllegalArgumentException::new))
+ .collectList()
+ .transform(persons -> ok().contentType(APPLICATION_JSON).body(persons, Person.class))
+ )
+ // Обратите внимание на использование хэндлера
+ .GET("/func/person", accept(APPLICATION_JSON), new PersonHandler(repository)::list)
+ // Обратите внимание на использование pathVariable
+ .GET("/func/person/{id}", accept(APPLICATION_JSON),
+ request -> repository.findById(request.pathVariable("id"))
+ .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromValue(person)))
+ .switchIfEmpty(notFound().build())
+ ).build();
+ }
+
+ // Это пример хэндлера, который даже не бин
+ static class PersonHandler {
+
+ private final PersonRepository repository;
+
+ PersonHandler(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ Mono list(ServerRequest request) {
+ // Обратите внимание на пример другого порядка создания response от Flux
+ return ok().contentType(APPLICATION_JSON).body(repository.findAll(), Person.class);
+ }
+ }
+}
diff --git a/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/WebfluxDemo.java b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/WebfluxDemo.java
new file mode 100644
index 00000000..c2650111
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/WebfluxDemo.java
@@ -0,0 +1,27 @@
+package ru.otus.spring;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+import java.util.Arrays;
+
+@SpringBootApplication
+public class WebfluxDemo {
+ private static final Logger logger = LoggerFactory.getLogger(WebfluxDemo.class);
+
+ public static void main(String[] args) {
+ var context = SpringApplication.run(WebfluxDemo.class);
+ var repository = context.getBean(PersonRepository.class);
+
+ repository.saveAll(Arrays.asList(
+ new Person("Pushkin", 22),
+ new Person("Lermontov", 22),
+ new Person("Tolstoy", 60)
+ )).subscribe(p -> logger.info("person name:{}", p.getLastName()));
+ }
+}
+
diff --git a/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/domain/Person.java b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..d40218ec
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,55 @@
+package ru.otus.spring.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+@Document
+public class Person {
+
+ @Id
+ private String id;
+
+ @JsonProperty("name")
+ @Field("name")
+ private String lastName;
+
+ private int age;
+
+ public Person() {
+ }
+
+ public Person(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public Person(String lastName, int age) {
+ this.lastName = lastName;
+ this.age = age;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+}
diff --git a/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/domain/PersonDto.java b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/domain/PersonDto.java
new file mode 100644
index 00000000..8f6a3223
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/domain/PersonDto.java
@@ -0,0 +1,31 @@
+package ru.otus.spring.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PersonDto {
+
+ private final String id;
+
+ @JsonProperty("name")
+ private final String name;
+
+ private final int age;
+
+ public PersonDto(String id, String name, int age) {
+ this.id = id;
+ this.name = name;
+ this.age = age;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getAge() {
+ return age;
+ }
+}
diff --git a/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/repository/PersonRepository.java b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/repository/PersonRepository.java
new file mode 100644
index 00000000..78a1cbdc
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/repository/PersonRepository.java
@@ -0,0 +1,19 @@
+package ru.otus.spring.repository;
+
+import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+
+public interface PersonRepository extends ReactiveMongoRepository {
+
+ Flux findAll();
+
+ Mono findById(String id);
+
+ Mono save(Mono person);
+
+ Flux findAllByLastName(String lastName);
+
+ Flux findAllByAge(int age);
+}
diff --git a/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/rest/AnnotatedController.java b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/rest/AnnotatedController.java
new file mode 100644
index 00000000..aeba6a6d
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/rest/AnnotatedController.java
@@ -0,0 +1,34 @@
+package ru.otus.spring.rest;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+
+@RestController
+public class AnnotatedController {
+
+ @GetMapping("/flux/one")
+ public Mono one() {
+ return Mono.just("one");
+ }
+
+ @GetMapping(path ="/flux/ten", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public Flux list() {
+ return Flux.range(1, 10).delayElements(Duration.ofSeconds(1));
+ }
+
+ @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public Flux stream() {
+ return Flux.generate(() -> 0, (state, emitter) -> {
+ emitter.next(state);
+ return state + 1;
+ })
+ .delayElements(Duration.ofSeconds(1L))
+ .map(i -> "" + i);
+ }
+}
diff --git a/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/rest/PersonController.java b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..f351117c
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/main/java/ru/otus/spring/rest/PersonController.java
@@ -0,0 +1,47 @@
+package ru.otus.spring.rest;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.domain.PersonDto;
+import ru.otus.spring.repository.PersonRepository;
+
+@RestController
+public class PersonController {
+
+ private final PersonRepository repository;
+
+ public PersonController(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ @GetMapping("/person")
+ public Flux all() {
+ return repository.findAll()
+ .map(this::toDto);
+ }
+
+ @GetMapping("/person/{id}")
+ public Mono> byId(@PathVariable("id") String id) {
+ return repository.findById(id)
+ .map(this::toDto)
+ .map(ResponseEntity::ok)
+ .switchIfEmpty(Mono.fromCallable(() -> ResponseEntity.notFound().build()));
+ }
+
+ @PostMapping("/person")
+ public Mono save(@RequestBody Mono dto) {
+ return repository.save(dto);
+ }
+
+ @GetMapping("/person/find")
+ public Flux byName(@RequestParam("name") String name) {
+ return repository.findAllByLastName(name);
+ }
+
+ private PersonDto toDto(Person person) {
+ return new PersonDto(person.getId(), person.getLastName(), person.getAge());
+ }
+}
diff --git a/2022-11/spring-22-webflux/src/main/resources/application.yml b/2022-11/spring-22-webflux/src/main/resources/application.yml
new file mode 100644
index 00000000..6a372567
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/main/resources/application.yml
@@ -0,0 +1,4 @@
+spring:
+ mongodb:
+ embedded:
+ version: "3.5.5"
\ No newline at end of file
diff --git a/2022-11/spring-22-webflux/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java b/2022-11/spring-22-webflux/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
new file mode 100644
index 00000000..b88bf86b
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.repository;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import ru.otus.spring.domain.Person;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@DataMongoTest
+class PersonRepositoryTest {
+
+ @Autowired
+ private PersonRepository repository;
+
+ @Test
+ void shouldSetIdOnSave() {
+ Mono personMono = repository.save(new Person("Bill", 12));
+
+ StepVerifier
+ .create(personMono)
+ .assertNext(person -> assertNotNull(person.getId()))
+ .expectComplete()
+ .verify();
+ }
+}
diff --git a/2022-11/spring-22-webflux/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2022-11/spring-22-webflux/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
new file mode 100644
index 00000000..71a31505
--- /dev/null
+++ b/2022-11/spring-22-webflux/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
@@ -0,0 +1,28 @@
+package ru.otus.spring.rest;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerResponse;
+
+@SpringBootTest
+class PersonControllerTest {
+
+ @Autowired
+ private RouterFunction route;
+
+ @Test
+ void testRoute() {
+ WebTestClient client = WebTestClient
+ .bindToRouterFunction(route)
+ .build();
+
+ client.get()
+ .uri("/func/person")
+ .exchange()
+ .expectStatus()
+ .isOk();
+ }
+}