diff --git a/2022-05/spring-30/.gitignore b/2022-05/spring-30/.gitignore
new file mode 100644
index 00000000..549e00a2
--- /dev/null
+++ b/2022-05/spring-30/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/2022-05/spring-30/pom.xml b/2022-05/spring-30/pom.xml
new file mode 100644
index 00000000..56dbc7bd
--- /dev/null
+++ b/2022-05/spring-30/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-30
+ 1.0
+
+ pom
+
+
+ spring-30-exercise
+ spring-30-solution
+
+
diff --git a/2022-05/spring-30/spring-30-exercise/pom.xml b/2022-05/spring-30/spring-30-exercise/pom.xml
new file mode 100644
index 00000000..ce2c8644
--- /dev/null
+++ b/2022-05/spring-30/spring-30-exercise/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-30-exercise
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.6.10
+
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-integration
+
+
+ org.springframework
+ spring-messaging
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2022-05/spring-30/spring-30-exercise/src/main/java/ru/otus/spring/integration/App.java b/2022-05/spring-30/spring-30-exercise/src/main/java/ru/otus/spring/integration/App.java
new file mode 100644
index 00000000..d4379d7e
--- /dev/null
+++ b/2022-05/spring-30/spring-30-exercise/src/main/java/ru/otus/spring/integration/App.java
@@ -0,0 +1,50 @@
+package ru.otus.spring.integration;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.integration.annotation.IntegrationComponentScan;
+import org.springframework.integration.channel.QueueChannel;
+import org.springframework.integration.dsl.MessageChannels;
+import org.springframework.messaging.PollableChannel;
+import org.springframework.messaging.SubscribableChannel;
+import org.springframework.messaging.support.MessageBuilder;
+
+@SpringBootApplication
+@IntegrationComponentScan
+public class App {
+
+ public static void main( String[] args ) throws InterruptedException {
+ ConfigurableApplicationContext ctx = SpringApplication.run( App.class, args );
+
+ PollableChannel channel1 = ctx.getBean( "channel1", PollableChannel.class );
+ SubscribableChannel channel2 = ctx.getBean( "channel2", SubscribableChannel.class );
+
+ channel2.subscribe( System.out::println );
+ new Thread( () -> {
+ while ( true ) {
+ channel2.send( channel1.receive() );
+ }
+ } ).start();
+
+ channel1.send( MessageBuilder.withPayload( "Hello" ).build() );
+ channel1.send( MessageBuilder.withPayload( "Hello2" ).build() );
+
+ Thread.sleep( 2000 );
+
+ channel1.send( MessageBuilder.withPayload( "Hello3" ).build() );
+
+ Thread.sleep( 100000 );
+ }
+
+ @Bean
+ public PollableChannel channel1() {
+ return new QueueChannel( 100 );
+ }
+
+ @Bean
+ public SubscribableChannel channel2() {
+ return MessageChannels.direct( "channel2" ).get();
+ }
+}
diff --git a/2022-05/spring-30/spring-30-exercise/src/main/resources/application.yml b/2022-05/spring-30/spring-30-exercise/src/main/resources/application.yml
new file mode 100644
index 00000000..4b4d02dc
--- /dev/null
+++ b/2022-05/spring-30/spring-30-exercise/src/main/resources/application.yml
@@ -0,0 +1,3 @@
+logging:
+ level:
+ root: debug
\ No newline at end of file
diff --git a/2022-05/spring-30/spring-30-exercise/src/test/java/ru/otus/spring/integration/MessagesTest.java b/2022-05/spring-30/spring-30-exercise/src/test/java/ru/otus/spring/integration/MessagesTest.java
new file mode 100644
index 00000000..491cdcef
--- /dev/null
+++ b/2022-05/spring-30/spring-30-exercise/src/test/java/ru/otus/spring/integration/MessagesTest.java
@@ -0,0 +1,98 @@
+package ru.otus.spring.integration;
+
+
+import org.junit.jupiter.api.Test;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.ErrorMessage;
+import org.springframework.messaging.support.GenericMessage;
+import org.springframework.messaging.support.MessageBuilder;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+
+@SuppressWarnings("all")
+public class MessagesTest {
+
+ @Test
+ public void testCreateSimpleGenericMessage() {
+ // TODO: Создайте сообщение с payload-ом "Hello" с помощью конструктора
+ Message message = null;
+
+ assertNotNull( message );
+ assertEquals( GenericMessage.class, message.getClass() );
+ assertNotNull( message.getPayload() );
+ assertEquals( "Hello", message.getPayload() );
+ }
+
+ @Test
+ public void testCreateGenericMessage() {
+ // TODO: Создайте сообщение с пользователем с помощью конструктора
+ Message message = null;
+
+ assertNotNull( message );
+ assertEquals( GenericMessage.class, message.getClass() );
+ assertNotNull( message.getPayload() );
+ assertEquals( new User( "John", 23 ), message.getPayload() );
+ }
+
+ @Test
+ public void testGenericMessageWithHeaders() {
+ // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World"
+ Map headers = null;
+ Message message = null;
+
+ assertNotNull( message );
+ assertEquals( "Hello", message.getPayload() );
+ assertEquals( "World", message.getHeaders().get( "to", String.class ) );
+ }
+
+ @Test
+ public void testGenericMessageWithMessageHeaders() {
+ // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World"
+ MessageHeaders headers = null;
+ Message message = null;
+
+ assertNotNull( message );
+ assertEquals( "Hello", message.getPayload() );
+ assertEquals( "World", message.getHeaders().get( "to", String.class ) );
+ }
+
+ @Test
+ public void testErrorMessage() {
+ // TODO: Создайте сообщение об ошибки с объектом NullPointerException внутри
+ Message errorMessage = null;
+
+ assertNotNull( errorMessage );
+ assertEquals( ErrorMessage.class, errorMessage.getClass() );
+ assertEquals( NullPointerException.class, errorMessage.getPayload().getClass() );
+ }
+
+ @Test
+ public void testMessageBuilder() {
+ // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World" с помощью MessageBuilder
+ Message message = null;
+
+ assertNotNull( message );
+ assertEquals( "Hello", message.getPayload() );
+ assertEquals( "World", message.getHeaders().get( "to", String.class ) );
+ }
+
+ @Test
+ public void testBuildFromMessage() {
+ Message original = MessageBuilder
+ .withPayload( new User( "Kate", 30 ) )
+ .setHeader( "processor", "userService" )
+ .build();
+
+ // TODO: Создайте новое сообщение с теми же payload и header-ами c помощью MessageBuilder
+ Message newMessage = null;
+
+ assertNotNull( newMessage );
+ assertEquals( original.getPayload(), newMessage.getPayload() );
+ assertEquals( original.getHeaders().get( "processor" ), newMessage.getHeaders().get( "processor" ) );
+ }
+}
diff --git a/2022-05/spring-30/spring-30-exercise/src/test/java/ru/otus/spring/integration/MyMessageBuilder.java b/2022-05/spring-30/spring-30-exercise/src/test/java/ru/otus/spring/integration/MyMessageBuilder.java
new file mode 100644
index 00000000..55ce263b
--- /dev/null
+++ b/2022-05/spring-30/spring-30-exercise/src/test/java/ru/otus/spring/integration/MyMessageBuilder.java
@@ -0,0 +1,8 @@
+package ru.otus.spring.integration;
+
+import org.springframework.messaging.support.MessageBuilder;
+
+public class MyMessageBuilder {
+
+
+}
diff --git a/2022-05/spring-30/spring-30-exercise/src/test/java/ru/otus/spring/integration/User.java b/2022-05/spring-30/spring-30-exercise/src/test/java/ru/otus/spring/integration/User.java
new file mode 100644
index 00000000..e0ebbfa4
--- /dev/null
+++ b/2022-05/spring-30/spring-30-exercise/src/test/java/ru/otus/spring/integration/User.java
@@ -0,0 +1,36 @@
+package ru.otus.spring.integration;
+
+import java.util.Objects;
+
+public class User {
+
+ private final String name;
+ private final int age;
+
+ public User(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof User)) return false;
+ User user = (User) o;
+ return age == user.age &&
+ Objects.equals(name, user.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, age);
+ }
+}
diff --git a/2022-05/spring-30/spring-30-solution/pom.xml b/2022-05/spring-30/spring-30-solution/pom.xml
new file mode 100644
index 00000000..a772d617
--- /dev/null
+++ b/2022-05/spring-30/spring-30-solution/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-30-solution
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.6.1
+
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-integration
+
+
+ org.springframework
+ spring-messaging
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2022-05/spring-30/spring-30-solution/src/main/java/ru/otus/spring/integration/App.java b/2022-05/spring-30/spring-30-solution/src/main/java/ru/otus/spring/integration/App.java
new file mode 100644
index 00000000..b1ac9a1b
--- /dev/null
+++ b/2022-05/spring-30/spring-30-solution/src/main/java/ru/otus/spring/integration/App.java
@@ -0,0 +1,52 @@
+package ru.otus.spring.integration;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.integration.annotation.IntegrationComponentScan;
+import org.springframework.integration.channel.QueueChannel;
+
+import org.springframework.integration.dsl.MessageChannels;
+import org.springframework.messaging.PollableChannel;
+import org.springframework.messaging.SubscribableChannel;
+import org.springframework.messaging.support.MessageBuilder;
+
+@SpringBootApplication
+@IntegrationComponentScan
+public class App {
+
+ public static void main(String[] args) throws InterruptedException {
+ ConfigurableApplicationContext ctx = SpringApplication.run(App.class, args);
+
+ PollableChannel channel1 = ctx.getBean("channel1", PollableChannel.class);
+ SubscribableChannel channel2 = ctx.getBean("channel2", SubscribableChannel.class);
+
+ channel2.subscribe(System.out::println);
+
+ new Thread(() -> {
+ while (true) {
+ channel2.send(channel1.receive());
+ }
+ }).start();
+
+ channel1.send(MessageBuilder.withPayload("Hello").build());
+ channel1.send(MessageBuilder.withPayload("Hello2").build());
+
+ Thread.sleep(2000);
+
+ channel1.send(MessageBuilder.withPayload("Hello3").build());
+
+ Thread.sleep(100000);
+ }
+
+ @Bean
+ public PollableChannel channel1() {
+ return new QueueChannel(100);
+ }
+
+ @Bean
+ public SubscribableChannel channel2() {
+ return MessageChannels.direct("channel2").get();
+ }
+}
diff --git a/2022-05/spring-30/spring-30-solution/src/test/java/ru/otus/spring/integration/MessagesTest.java b/2022-05/spring-30/spring-30-solution/src/test/java/ru/otus/spring/integration/MessagesTest.java
new file mode 100644
index 00000000..6bd735d6
--- /dev/null
+++ b/2022-05/spring-30/spring-30-solution/src/test/java/ru/otus/spring/integration/MessagesTest.java
@@ -0,0 +1,101 @@
+package ru.otus.spring.integration;
+
+
+import org.junit.jupiter.api.Test;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.ErrorMessage;
+import org.springframework.messaging.support.GenericMessage;
+import org.springframework.messaging.support.MessageBuilder;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+
+@SuppressWarnings("all")
+public class MessagesTest {
+
+ @Test
+ public void testCreateSimpleGenericMessage() {
+ // TODO: Создайте сообщение с payload-ом "Hello" с помощью конструктора
+ Message message = new GenericMessage( "Hello" );
+
+ assertNotNull( message );
+ assertEquals( GenericMessage.class, message.getClass() );
+ assertNotNull( message.getPayload() );
+ assertEquals( "Hello", message.getPayload() );
+ }
+
+ @Test
+ public void testCreateGenericMessage() {
+ // TODO: Создайте сообщение с пользователем с помощью конструктора
+ Message message = new GenericMessage( new User( "John", 23 ) );
+
+ assertNotNull( message );
+ assertEquals( GenericMessage.class, message.getClass() );
+ assertNotNull( message.getPayload() );
+ assertEquals( new User( "John", 23 ), message.getPayload() );
+ }
+
+ @Test
+ public void testGenericMessageWithHeaders() {
+ // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World"
+ Map headers = Collections.singletonMap( "to", "World" );
+ Message message = new GenericMessage<>( "Hello", headers );
+
+ assertNotNull( message );
+ assertEquals( "Hello", message.getPayload() );
+ assertEquals( "World", message.getHeaders().get( "to", String.class ) );
+ }
+
+ @Test
+ public void testGenericMessageWithMessageHeaders() {
+ // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World"
+ MessageHeaders headers = new MessageHeaders( Collections.singletonMap( "to", "World" ) );
+ Message message = new GenericMessage<>( "Hello", headers );
+
+ assertNotNull( message );
+ assertEquals( "Hello", message.getPayload() );
+ assertEquals( "World", message.getHeaders().get( "to", String.class ) );
+ }
+
+ @Test
+ public void testErrorMessage() {
+ // TODO: Создайте сообщение об ошибки с объектом NullPointerException внутри
+ Message errorMessage = new ErrorMessage( new NullPointerException() );
+
+ assertNotNull( errorMessage );
+ assertEquals( ErrorMessage.class, errorMessage.getClass() );
+ assertEquals( NullPointerException.class, errorMessage.getPayload().getClass() );
+ }
+
+ @Test
+ public void testMessageBuilder() {
+ // TODO: Создайте сообщение с payload-ом "Hello" и header-ом "to":"World" с помощью MessageBuilder
+ Message message = MessageBuilder.withPayload( "Hello" )
+ .setHeader( "to", "World" )
+ .build();
+
+ assertNotNull( message );
+ assertEquals( "Hello", message.getPayload() );
+ assertEquals( "World", message.getHeaders().get( "to", String.class ) );
+ }
+
+ @Test
+ public void testBuildFromMessage() {
+ Message original = MessageBuilder
+ .withPayload( new User( "Kate", 30 ) )
+ .setHeader( "processor", "userService" )
+ .build();
+
+ // TODO: Создайте новое сообщение с теми же payload и header-ами c помощью MessageBuilder
+ Message newMessage = MessageBuilder.fromMessage( original ).build();
+
+ assertNotNull( newMessage );
+ assertEquals( original.getPayload(), newMessage.getPayload() );
+ assertEquals( original.getHeaders().get( "processor" ), newMessage.getHeaders().get( "processor" ) );
+ }
+}
diff --git a/2022-05/spring-30/spring-30-solution/src/test/java/ru/otus/spring/integration/User.java b/2022-05/spring-30/spring-30-solution/src/test/java/ru/otus/spring/integration/User.java
new file mode 100644
index 00000000..e0ebbfa4
--- /dev/null
+++ b/2022-05/spring-30/spring-30-solution/src/test/java/ru/otus/spring/integration/User.java
@@ -0,0 +1,36 @@
+package ru.otus.spring.integration;
+
+import java.util.Objects;
+
+public class User {
+
+ private final String name;
+ private final int age;
+
+ public User(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof User)) return false;
+ User user = (User) o;
+ return age == user.age &&
+ Objects.equals(name, user.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, age);
+ }
+}