getAllPersons() {
+ return this.repository.findAll();
+ }
+}
diff --git a/2023-03/spring-36-docker/docker-compose-example/src/main/resources/application.yml b/2023-03/spring-36-docker/docker-compose-example/src/main/resources/application.yml
new file mode 100644
index 00000000..bf91784a
--- /dev/null
+++ b/2023-03/spring-36-docker/docker-compose-example/src/main/resources/application.yml
@@ -0,0 +1,8 @@
+spring:
+ datasource:
+ # Эти свойства будут перегружены свойствами в docker-compose.yml
+ url: jdbc:postgresql://localhost:5432/db
+ username: postgres
+ password: postgres
+ jpa:
+ generate-ddl: true
diff --git a/2023-03/spring-36-docker/docker-compose-example/src/main/resources/static/index.html b/2023-03/spring-36-docker/docker-compose-example/src/main/resources/static/index.html
new file mode 100644
index 00000000..a80ea187
--- /dev/null
+++ b/2023-03/spring-36-docker/docker-compose-example/src/main/resources/static/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Главная страницв
+
+
+Главная страница
+Список всех лиц доступен по ссылке.
+Перезапустив приложение можно добавить ещё в БД.
+
+
diff --git a/2023-03/spring-36-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java b/2023-03/spring-36-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java
new file mode 100644
index 00000000..93fe13c4
--- /dev/null
+++ b/2023-03/spring-36-docker/docker-compose-example/src/test/java/ru/otus/spring/docker/DockerComposeExampleApplicationTests.java
@@ -0,0 +1,15 @@
+package ru.otus.spring.docker;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+@AutoConfigureTestDatabase
+class DockerComposeExampleApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/2023-03/spring-36-docker/helloWorld.txt b/2023-03/spring-36-docker/helloWorld.txt
new file mode 100644
index 00000000..f2c3689e
--- /dev/null
+++ b/2023-03/spring-36-docker/helloWorld.txt
@@ -0,0 +1,20 @@
+docker image ls
+docker pull hello-world
+docker image ls
+docker image rm hello-world
+docker ps
+docker run hello-world
+docker ps
+docker run hello-world
+docker ps -all
+docker run --rm hello-world
+
+docker run -it --name=ubuntu-run ubuntu bash
+docker start ubuntu-run
+docker exec -it ubuntu-run bash
+
+docker run -d -p:8080:80 --name=my-nginx nginx
+docker ps -a
+curl http://localhost:8080
+docker kill my-nginx
+docker rm my-nginx
diff --git a/2023-03/spring-36-docker/image/Dockerfile b/2023-03/spring-36-docker/image/Dockerfile
new file mode 100644
index 00000000..c51e4bde
--- /dev/null
+++ b/2023-03/spring-36-docker/image/Dockerfile
@@ -0,0 +1,4 @@
+FROM nginx:1.11-alpine
+COPY index.html /usr/share/nginx/html/index.html
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/2023-03/spring-36-docker/image/index.html b/2023-03/spring-36-docker/image/index.html
new file mode 100644
index 00000000..f3e333e8
--- /dev/null
+++ b/2023-03/spring-36-docker/image/index.html
@@ -0,0 +1 @@
+Hello World
diff --git a/2023-03/spring-36-docker/image/readme.txt b/2023-03/spring-36-docker/image/readme.txt
new file mode 100644
index 00000000..9dde7083
--- /dev/null
+++ b/2023-03/spring-36-docker/image/readme.txt
@@ -0,0 +1,5 @@
+docker build -t my-demo-image:v1 .
+docker images | grep my-demo-image
+docker run -p:80:80 --rm my-demo-image:v1
+docker ps
+curl http://localhost
\ No newline at end of file
diff --git a/2023-05/spring-21-reactor/.gitignore b/2023-05/spring-21-reactor/.gitignore
new file mode 100644
index 00000000..fbe7a1ed
--- /dev/null
+++ b/2023-05/spring-21-reactor/.gitignore
@@ -0,0 +1,7 @@
+.idea/
+*.iml
+
+target/
+
+/node_modules
+/output
diff --git a/2023-05/spring-21-reactor/pom.xml b/2023-05/spring-21-reactor/pom.xml
new file mode 100644
index 00000000..2c5cd688
--- /dev/null
+++ b/2023-05/spring-21-reactor/pom.xml
@@ -0,0 +1,70 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-21-reactor
+ 1.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.0
+
+
+
+ 17
+ 17
+
+
+
+
+
+ io.projectreactor
+ reactor-bom
+ 2022.0.7
+ pom
+ import
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ 3.1.0
+ pom
+ import
+
+
+
+
+
+
+
+ io.projectreactor
+ reactor-core
+
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
diff --git a/2023-05/spring-21-reactor/src/main/java/ru/otus/CreateExample.java b/2023-05/spring-21-reactor/src/main/java/ru/otus/CreateExample.java
new file mode 100644
index 00000000..70b161b4
--- /dev/null
+++ b/2023-05/spring-21-reactor/src/main/java/ru/otus/CreateExample.java
@@ -0,0 +1,74 @@
+package ru.otus;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
+
+public class CreateExample {
+ private static final Logger logger = LoggerFactory.getLogger(CreateExample.class);
+
+ public static void main(String[] args) {
+ onEachNext();
+ //lazyObservable();
+ //creatorExample();
+ }
+
+ private static void onEachNext() {
+ Flux obs = Flux.just("one", "two", "three");
+ obs.doFirst(() -> logger.info("Starring:"))
+ .doOnComplete(() -> logger.info("The end!"))
+ .doOnEach(item -> logger.info("item_1:{}", item.get()))
+ .subscribe();
+
+ logger.info("-----");
+
+ obs.doOnNext(item -> logger.info("item_2:{}", item))
+ .map(String::length)
+ .doOnNext(item -> logger.info("length_2:{}", item))
+ .subscribe();
+ }
+
+ private static void lazyObservable() {
+ Flux obs = Flux.defer(() -> {
+ logger.info("creating new Observable");
+ return Flux.just("one", "two", "three");
+ });
+
+ obs.doOnNext(item -> logger.info("item_1:{}", item))
+ .subscribe();
+
+ logger.info("---------------");
+
+ obs.doOnNext(item -> logger.info("item_2:{}", item))
+ .subscribe();
+ }
+
+ private static void creatorExample() {
+ Flux obs = Flux.create(emitter -> {
+ emitter.next("one");
+ emitter.next("two");
+
+ emitter.error(new RuntimeException("Error!"));
+
+ emitter.next("three");
+ emitter.complete();
+ });
+
+ obs.onErrorResume(e -> {
+ logger.error("error:{}", e.getMessage(), e);
+ return Flux.just("r1", "r2", "r3");
+ })
+ .doOnNext(item -> logger.info("item__1:{}", item))
+ .subscribe();
+
+ logger.info("---------------");
+
+ Disposable disposable = obs.doOnNext(item -> logger.info("item__2:{}", item))
+ .subscribe(next -> logger.info("next:{}", next),
+ error -> logger.info("error:{}", error.getMessage()),
+ () -> logger.info("onComplete"));
+
+ logger.info("isDisposed:{}", disposable.isDisposed());
+ }
+}
diff --git a/2023-05/spring-21-reactor/src/main/java/ru/otus/OperatorsExample.java b/2023-05/spring-21-reactor/src/main/java/ru/otus/OperatorsExample.java
new file mode 100644
index 00000000..aa98d7d6
--- /dev/null
+++ b/2023-05/spring-21-reactor/src/main/java/ru/otus/OperatorsExample.java
@@ -0,0 +1,51 @@
+package ru.otus;
+
+import java.time.LocalDate;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+
+public class OperatorsExample {
+ private static final Logger logger = LoggerFactory.getLogger(OperatorsExample.class);
+
+ public static void main(String[] args) {
+ //merge();
+ fromList();
+ }
+
+ private static void merge() {
+ var listFlux1 = Flux.fromIterable(List.of(
+ new Person("John", "Dow", "male", LocalDate.of(1992, 3, 12)),
+ new Person("Jane", "Dow", "female", LocalDate.of(2001, 6, 23))));
+
+ var listFlux2 = Flux.fromIterable(List.of(
+ new Person("Howard", "Lovecraft", "male", LocalDate.of(1890, 8, 20)),
+ new Person("Joanne", "Rowling", "female", LocalDate.of(1965, 6, 30))));
+
+ var listFlux3 = Flux.fromIterable(List.of(
+ new Person("Ivan", "Petrov", "male", LocalDate.of(1890, 2, 10)),
+ new Person("Joanne", "Stuard", "female", LocalDate.of(1965, 1, 3))));
+
+ Flux.merge(listFlux1, listFlux2, listFlux3)
+ .subscribe(person -> logger.info("person:{}", person));
+
+ }
+
+ private static void fromList() {
+ var persons = List.of(
+ new Person("John", "Dow", "male", LocalDate.of(1992, 3, 12)),
+ new Person("Jane", "Dow", "female", LocalDate.of(2001, 6, 23)),
+ new Person("Howard", "Lovecraft", "male", LocalDate.of(1890, 8, 20)),
+ new Person("Joanne", "Rowling", "female", LocalDate.of(1965, 6, 30)));
+
+ var disposable = Flux.fromIterable(persons)
+ .filter(person -> person.birth().isAfter(LocalDate.of(1990, 1, 1)))
+ .map(p -> p.firstName() + " " + p.lastName())
+ .collectList()
+ .subscribe(item -> logger.info("item: {}", item));
+
+ logger.info("disposable.isDisposed:{}", disposable.isDisposed());
+ }
+}
diff --git a/2023-05/spring-21-reactor/src/main/java/ru/otus/Person.java b/2023-05/spring-21-reactor/src/main/java/ru/otus/Person.java
new file mode 100644
index 00000000..ddbe5422
--- /dev/null
+++ b/2023-05/spring-21-reactor/src/main/java/ru/otus/Person.java
@@ -0,0 +1,6 @@
+package ru.otus;
+
+import java.time.LocalDate;
+
+public record Person(String firstName, String lastName, String gender, LocalDate birth) {
+}
diff --git a/2023-05/spring-21-reactor/src/main/java/ru/otus/PublisherExample.java b/2023-05/spring-21-reactor/src/main/java/ru/otus/PublisherExample.java
new file mode 100644
index 00000000..aaab7853
--- /dev/null
+++ b/2023-05/spring-21-reactor/src/main/java/ru/otus/PublisherExample.java
@@ -0,0 +1,56 @@
+package ru.otus;
+
+import java.time.Duration;
+import java.util.function.BiFunction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.ConnectableFlux;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.SynchronousSink;
+import reactor.core.scheduler.Schedulers;
+
+public class PublisherExample {
+ private static final Logger logger = LoggerFactory.getLogger(PublisherExample.class);
+
+ public static void main(String[] args) throws Exception {
+ publisherExample();
+ }
+
+ public static void publisherExample() throws InterruptedException {
+ Flux ob = magicPublisher();
+ Thread.sleep(5000);
+ logger.info("First subscribed");
+ var disposable1 = ob.subscribe(item -> logger.info("item: {}", item));
+
+ logger.info("disposable1.isDisposed:{}", disposable1.isDisposed());
+ Thread.sleep(5000);
+
+ logger.info("Second subscribed");
+ var disposable2 = ob.subscribe(item -> logger.info("item second: {}", item));
+
+
+ logger.info("disposable2.isDisposed():{}", disposable2.isDisposed());
+
+ Thread.sleep(60_000);
+ }
+
+ public static Flux magicPublisher() {
+ var schedulerGenerator = Schedulers.newParallel("generator", 1);
+ var generator = Flux.generate(
+ () -> 0L,
+ (BiFunction, Long>)
+ (prev, sink) -> {
+ var newValue = prev + 1;
+ sink.next(newValue);
+ logger.info("newValue:{}", newValue);
+ return newValue;
+ })
+ .delayElements(Duration.ofSeconds(5), schedulerGenerator)
+ .map(id -> "new id:" + id)
+ .doOnNext(val -> logger.info("val:{}", val));
+
+ ConnectableFlux generatorConnectable = generator.publish();
+
+ return generatorConnectable.autoConnect(0);
+ }
+}
diff --git a/2023-05/spring-22-webflux/.gitignore b/2023-05/spring-22-webflux/.gitignore
new file mode 100644
index 00000000..e62c33c2
--- /dev/null
+++ b/2023-05/spring-22-webflux/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+*.iml
+
+target/
diff --git a/2023-05/spring-22-webflux/HttpRequests.http b/2023-05/spring-22-webflux/HttpRequests.http
new file mode 100644
index 00000000..3449be12
--- /dev/null
+++ b/2023-05/spring-22-webflux/HttpRequests.http
@@ -0,0 +1,47 @@
+###
+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/stream
+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/1
+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
+
+###
+GET http://localhost:8080/func/person/1
+Accept: */*
+Content-Type: application/json
+Cache-Control: no-cache
diff --git a/2023-05/spring-22-webflux/docker/runDb.src b/2023-05/spring-22-webflux/docker/runDb.src
new file mode 100755
index 00000000..aa76bbc6
--- /dev/null
+++ b/2023-05/spring-22-webflux/docker/runDb.src
@@ -0,0 +1,6 @@
+docker run --rm --name pg-docker \
+-e POSTGRES_PASSWORD=pwd \
+-e POSTGRES_USER=usr \
+-e POSTGRES_DB=demoDB \
+-p 5430:5432 \
+postgres:13
\ No newline at end of file
diff --git a/2023-05/spring-22-webflux/pom.xml b/2023-05/spring-22-webflux/pom.xml
new file mode 100644
index 00000000..02b44b6d
--- /dev/null
+++ b/2023-05/spring-22-webflux/pom.xml
@@ -0,0 +1,82 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-22-webflux
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.0
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jdbc
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-r2dbc
+
+
+
+ org.flywaydb
+ flyway-core
+
+
+
+ io.r2dbc
+ r2dbc-postgresql
+ 0.8.13.RELEASE
+
+
+
+ org.postgresql
+ postgresql
+ 42.6.0
+
+
+
+ org.jetbrains
+ annotations
+ 24.0.1
+
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/DataFiller.java b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/DataFiller.java
new file mode 100644
index 00000000..74236a47
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/DataFiller.java
@@ -0,0 +1,34 @@
+package ru.otus.spring;
+
+import java.util.Arrays;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+import reactor.core.scheduler.Scheduler;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repository.PersonRepository;
+
+@Component
+public class DataFiller implements ApplicationRunner {
+ private static final Logger logger = LoggerFactory.getLogger(DataFiller.class);
+
+ private final PersonRepository personRepository;
+ private final Scheduler workerPool;
+
+ public DataFiller(PersonRepository personRepository, Scheduler workerPool) {
+ this.personRepository = personRepository;
+ this.workerPool = workerPool;
+ }
+
+ @Override
+ public void run(ApplicationArguments args) {
+ personRepository.saveAll(Arrays.asList(
+ new Person("Pushkin", 22),
+ new Person("Lermontov", 22),
+ new Person("Tolstoy", 60)
+ )).publishOn(workerPool)
+ .subscribe(savedPerson -> logger.info("saved person:{}", savedPerson));
+ }
+}
diff --git a/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/FunctionalEndpointsConfig.java b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/FunctionalEndpointsConfig.java
new file mode 100644
index 00000000..0366526f
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/FunctionalEndpointsConfig.java
@@ -0,0 +1,67 @@
+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(name -> ok().body(repository.findAllByLastName(name), 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(Long.parseLong(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/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/WebfluxDemo.java b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/WebfluxDemo.java
new file mode 100644
index 00000000..5bd95469
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/WebfluxDemo.java
@@ -0,0 +1,16 @@
+package ru.otus.spring;
+
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class WebfluxDemo {
+
+
+ public static void main(String[] args) {
+ SpringApplication.run(WebfluxDemo.class);
+ }
+}
+
diff --git a/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/config/ApplConfig.java b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/config/ApplConfig.java
new file mode 100644
index 00000000..9813876b
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/config/ApplConfig.java
@@ -0,0 +1,39 @@
+package ru.otus.spring.config;
+
+import io.netty.channel.nio.NioEventLoopGroup;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicLong;
+import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
+import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
+import reactor.util.annotation.NonNull;
+
+@Configuration
+public class ApplConfig {
+ private static final int THREAD_POOL_SIZE = 2;
+
+ @Bean
+ public ReactiveWebServerFactory reactiveWebServerFactory() {
+ var eventLoopGroup = new NioEventLoopGroup(THREAD_POOL_SIZE,
+ new ThreadFactory() {
+ private final AtomicLong threadIdGenerator = new AtomicLong(0);
+ @Override
+ public Thread newThread(@NonNull Runnable task) {
+ return new Thread(task, "server-thread-" + threadIdGenerator.incrementAndGet());
+ }
+ });
+
+ var factory = new NettyReactiveWebServerFactory();
+ factory.addServerCustomizers(builder -> builder.runOn(eventLoopGroup));
+
+ return factory;
+ }
+
+ @Bean
+ public Scheduler workerPool() {
+ return Schedulers.newParallel("worker-thread", THREAD_POOL_SIZE);
+ }
+}
diff --git a/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/domain/Person.java b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..9721a730
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,53 @@
+package ru.otus.spring.domain;
+
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.PersistenceCreator;
+import org.springframework.data.relational.core.mapping.Table;
+
+@Table("person")
+public class Person {
+
+ @Id
+ private final Long id;
+
+ @NotNull
+ private final String lastName;
+
+ private final int age;
+
+
+ @PersistenceCreator
+ private Person(Long id, @NotNull String lastName, int age) {
+ this.id = id;
+ this.lastName = lastName;
+ this.age = age;
+ }
+
+ public Person(String lastName, int age) {
+ this(null, lastName, age);
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+
+ public @NotNull String getLastName() {
+ return lastName;
+ }
+
+
+ public int getAge() {
+ return age;
+ }
+
+ @Override
+ public String toString() {
+ return "Person{" +
+ "id=" + id +
+ ", lastName='" + lastName + '\'' +
+ ", age=" + age +
+ '}';
+ }
+}
diff --git a/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/domain/PersonDto.java b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/domain/PersonDto.java
new file mode 100644
index 00000000..72630739
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/domain/PersonDto.java
@@ -0,0 +1,6 @@
+package ru.otus.spring.domain;
+
+
+public record PersonDto(String id, String name, int age) {
+
+}
diff --git a/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/repository/PersonRepository.java b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/repository/PersonRepository.java
new file mode 100644
index 00000000..d420f092
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/repository/PersonRepository.java
@@ -0,0 +1,18 @@
+package ru.otus.spring.repository;
+
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import ru.otus.spring.domain.Person;
+
+public interface PersonRepository extends ReactiveCrudRepository {
+
+ @NotNull Mono findById(@NotNull Long id);
+
+ Mono save(Mono person);
+
+ Flux findAllByLastName(String lastName);
+
+ Flux findAllByAge(int age);
+}
diff --git a/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/rest/AnnotatedController.java b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/rest/AnnotatedController.java
new file mode 100644
index 00000000..b37dcd86
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/rest/AnnotatedController.java
@@ -0,0 +1,49 @@
+package ru.otus.spring.rest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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;
+import reactor.core.scheduler.Scheduler;
+
+
+@RestController
+public class AnnotatedController {
+ private static final Logger logger = LoggerFactory.getLogger(AnnotatedController.class);
+
+ private final Scheduler workerPool;
+
+ public AnnotatedController(Scheduler workerPool) {
+ this.workerPool = workerPool;
+ }
+
+ @GetMapping("/flux/one")
+ public Mono one() {
+ return Mono.just("one");
+ }
+
+ @GetMapping(path ="/flux/ten", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public Flux list() {
+ logger.info("list request");
+ return Flux.range(1, 10)
+ .delayElements(Duration.ofSeconds(1), workerPool)
+ .doOnNext(val -> logger.info("value:{}", val));
+ }
+
+ @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public Flux stream() {
+ logger.info("stream");
+ return Flux.generate(() -> 0, (state, emitter) -> {
+ emitter.next(state);
+ return state + 1;
+ })
+ .delayElements(Duration.ofSeconds(1L))
+ .map(Object::toString)
+ .map(val -> String.format("valStr:%s", val));
+ }
+}
diff --git a/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/rest/PersonController.java b/2023-05/spring-22-webflux/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..f5937ae2
--- /dev/null
+++ b/2023-05/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") Long 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(String.valueOf(person.getId()), person.getLastName(), person.getAge());
+ }
+}
diff --git a/2023-05/spring-22-webflux/src/main/resources/application.yml b/2023-05/spring-22-webflux/src/main/resources/application.yml
new file mode 100644
index 00000000..5935fcc0
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/resources/application.yml
@@ -0,0 +1,17 @@
+server:
+ port: 8080
+
+spring:
+ r2dbc:
+ url: r2dbc:postgresql://localhost:5430/demoDB
+ username: usr
+ password: pwd
+ flyway:
+ url: jdbc:postgresql://localhost:5430/demoDB
+ user: usr
+ password: pwd
+
+
+logging:
+ level:
+ org.springframework.jdbc.core.JdbcTemplate: TRACE
diff --git a/2023-05/spring-22-webflux/src/main/resources/db/migration/V1__initial_schema.sql b/2023-05/spring-22-webflux/src/main/resources/db/migration/V1__initial_schema.sql
new file mode 100644
index 00000000..06ee58a8
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/resources/db/migration/V1__initial_schema.sql
@@ -0,0 +1,6 @@
+create table person
+(
+ id bigserial not null primary key,
+ last_name varchar(50) not null,
+ age int not null
+);
diff --git a/2023-05/spring-22-webflux/src/main/resources/logback.xml b/2023-05/spring-22-webflux/src/main/resources/logback.xml
new file mode 100644
index 00000000..b1f9bfe2
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/main/resources/logback.xml
@@ -0,0 +1,11 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
diff --git a/2023-05/spring-22-webflux/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java b/2023-05/spring-22-webflux/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
new file mode 100644
index 00000000..5ed22cc5
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/test/java/ru/otus/spring/repository/PersonRepositoryTest.java
@@ -0,0 +1,29 @@
+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 org.springframework.boot.test.context.SpringBootTest;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import ru.otus.spring.domain.Person;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@SpringBootTest
+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/2023-05/spring-22-webflux/src/test/java/ru/otus/spring/rest/AnnotatedControllerTest.java b/2023-05/spring-22-webflux/src/test/java/ru/otus/spring/rest/AnnotatedControllerTest.java
new file mode 100644
index 00000000..95eb4d75
--- /dev/null
+++ b/2023-05/spring-22-webflux/src/test/java/ru/otus/spring/rest/AnnotatedControllerTest.java
@@ -0,0 +1,96 @@
+package ru.otus.spring.rest;
+
+import java.time.Duration;
+import java.util.List;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.test.StepVerifier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class AnnotatedControllerTest {
+
+ @Autowired
+ private WebTestClient webTestClient;
+ @LocalServerPort
+ private int port;
+
+
+ @Test
+ void oneTest() {
+ //given
+ var client = WebClient.create(String.format("http://localhost:%d", port));
+
+ //when
+ var result = client
+ .get().uri("/flux/one")
+ .accept(MediaType.APPLICATION_JSON)
+ .retrieve()
+ .bodyToMono(String.class)
+ .timeout(Duration.ofSeconds(3))
+ .block();
+
+ //then
+ assertThat(result).isEqualTo("one");
+ }
+
+ @Test
+ void streamTest() {
+ //given
+ var client = WebClient.create(String.format("http://localhost:%d", port));
+ var expectedSize = 5;
+
+ //when
+ List result = client
+ .get().uri("/stream")
+ .accept(MediaType.TEXT_EVENT_STREAM)
+ .retrieve()
+ .bodyToFlux(String.class)
+ .take(expectedSize)
+ .timeout(Duration.ofSeconds(3))
+ .collectList()
+ .block();
+
+ //then
+ assertThat(result).hasSize(expectedSize)
+ .contains(String.format("valStr:%s", 0),
+ String.format("valStr:%s", 1),
+ String.format("valStr:%s", 2),
+ String.format("valStr:%s", 3),
+ String.format("valStr:%s", 4));
+ }
+
+ @Test
+ void dataTest() {
+ //given
+ var webTestClientForTest = webTestClient.mutate()
+ .responseTimeout(Duration.ofSeconds(20))
+ .build();
+
+ //when
+ var result = webTestClientForTest
+ .get().uri("/flux/ten")
+ .accept(MediaType.TEXT_EVENT_STREAM)
+ .exchange()
+ .expectStatus().isOk()
+ .returnResult(Integer.class)
+ .getResponseBody();
+
+ //then
+ var step = StepVerifier.create(result);
+ StepVerifier.Step stepResult = null;
+ for (var idx = 1; idx <= 10; idx++) {
+ stepResult = step.expectNext(idx);
+ }
+ stepResult.verifyComplete();
+ }
+
+}
\ No newline at end of file
diff --git a/2023-05/spring-22-webflux/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2023-05/spring-22-webflux/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
new file mode 100644
index 00000000..71a31505
--- /dev/null
+++ b/2023-05/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();
+ }
+}