├── .gitignore ├── README.md ├── add-sink.md ├── cloudamqp.md ├── scale-out.md ├── sink.md └── source.md /.gitignore: -------------------------------------------------------------------------------- 1 | tmp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Cloud Stream Tutorial 2 | 3 | 本チュートリアルでは、簡単なSourceとSinkを作成して、Spring Cloud Streamの基本的な開発方法を学びます。 4 | 5 | 次の図のように、SourceがHTTPで受け付けたTweetメッセージを送信し、Sinkがメッセージを受信してログに出力します。 6 | Message BinderにはRabbitMQを使用します。 7 | 8 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/a785e432-9a25-c467-b394-a1875b644376.png) 9 | 10 | Spring Cloud Streamに関する詳しい情報は 11 | [Event Driven Microservices with Spring Cloud Stream](http://www.slideshare.net/makingx/event-driven-microservices-with-spring-cloud-stream-jjugccc-ccca3) 12 | を参照してください。 13 | 14 | 1. [Sourceの作成](source.md) 15 | 1. [Sinkの作成](sink.md) 16 | 1. [Sinkのスケールアウト](scale-out.md) 17 | 1. [新しいSinkを追加](add-sink.md) 18 | 1. [[補足資料] CloudAMQPの利用](cloudamqp.md) 19 | 20 | 21 | ### 利用規約 22 | 23 | 無断で本ドキュメントの一部または全部を改変したり、本ドキュメントを用いた二次的著作物を作成することを禁止します。ただし、ドキュメント修正のためのPull Requestは大歓迎です。 24 | -------------------------------------------------------------------------------- /add-sink.md: -------------------------------------------------------------------------------- 1 | ## 新しいSinkを追加 2 | 3 | Sinkアプリケーションを追加して、メッセージの受信先を動的に増やしましょう。 4 | 5 | ### プロジェクト作成 6 | 7 | 以下のコマンドを実行すると、`tweet-viewer`フォルダに雛形プロジェクトが生成されます 8 | 9 | 10 | ``` 11 | curl https://start.spring.io/starter.tgz \ 12 | -d artifactId=tweet-viewer \ 13 | -d baseDir=tweet-viewer \ 14 | -d packageName=com.example \ 15 | -d dependencies=web,actuator,cloud-stream,amqp \ 16 | -d type=maven-project \ 17 | -d applicationName=TweetViewerApplication | tar -xzvf - 18 | ``` 19 | 20 | `src/main/java/com/example/TweetViewerApplication.java`を次のように記述してください。 21 | 22 | 23 | ``` java 24 | package com.example; 25 | 26 | import java.util.List; 27 | import java.util.concurrent.CopyOnWriteArrayList; 28 | import java.util.function.Consumer; 29 | 30 | import org.springframework.boot.SpringApplication; 31 | import org.springframework.boot.autoconfigure.SpringBootApplication; 32 | import org.springframework.context.annotation.Bean; 33 | import org.springframework.web.bind.annotation.GetMapping; 34 | import org.springframework.web.bind.annotation.RestController; 35 | 36 | @SpringBootApplication 37 | @RestController 38 | public class TweetViewerApplication { 39 | private final List tweets = new CopyOnWriteArrayList<>(); 40 | 41 | @GetMapping(path = "/") 42 | public List viewTweets() { 43 | return this.tweets; 44 | } 45 | 46 | @Bean 47 | public Consumer tweetCollector() { 48 | return tweet -> this.tweets.add(tweet); 49 | } 50 | 51 | public static void main(String[] args) { 52 | SpringApplication.run(TweetViewerApplication.class, args); 53 | } 54 | 55 | record Tweet(String text) { 56 | } 57 | } 58 | ``` 59 | 60 | このSinkアプリケーションでは受信したメッセージが`List`に追加され、HTTPのGETで確認できます。 61 | channel名`input`に対するdestination名とConsumerGroup名を`application.properties`に次にように設定してください。 62 | 63 | 64 | ``` properties 65 | spring.cloud.stream.function.bindings.tweetCollector-in-0=input 66 | spring.cloud.stream.bindings.input.destination=hello 67 | spring.cloud.stream.bindings.input.group=viewer 68 | ``` 69 | 70 | はじめに作成したSourceからメッセージを受信できるようにdestination名は同じにする必要があります。しかし、先に作成したSinkとは別にメッセージを受信したいのでConsumerGroupは別にします。ここでは`viewer`という名前にしました。 71 | 72 | 次のコマンドでこのSourceアプリケーションのjarファイルを作成してください。 73 | 74 | ``` 75 | ./mvnw clean package -DskipTests=true 76 | ``` 77 | 78 | `target`ディレクトリにtweet-viewer-0.0.1-SNAPSHOT.jarができていることを確認してください。 79 | 80 | ``` 81 | $ ls -lh target/ 82 | total 56840 83 | drwxr-xr-x 4 toshiaki staff 128B Nov 2 02:43 classes 84 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:43 generated-sources 85 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:43 generated-test-sources 86 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:43 maven-archiver 87 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:43 maven-status 88 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:43 test-classes 89 | -rw-r--r-- 1 toshiaki staff 28M Nov 2 02:43 tweet-viewer-0.0.1-SNAPSHOT.jar 90 | -rw-r--r-- 1 toshiaki staff 3.7K Nov 2 02:43 tweet-viewer-0.0.1-SNAPSHOT.jar.original 91 | ``` 92 | 93 | 94 | ### ローカル環境で実行 95 | 96 | 次のコマンドでアプリケーションを起動してください。 97 | 98 | ``` 99 | java -jar target/tweet-viewer-0.0.1-SNAPSHOT.jar --server.port=8085 100 | ``` 101 | 102 | 管理コンソール([http://localhost:15672](http://localhost:15672))にアクセスして、Queuesタブを選択してください。`hello.viewe` Queueが追加されていることが確認できます。 103 | 104 | 105 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/3934e335-14c8-84ae-05b0-a36e804adf39.png) 106 | 107 | 108 | Sourceにリクエストを送りましょう。 109 | 110 | ``` 111 | curl -v localhost:8080 -d '{"text":"Hello1"}' -H 'Content-Type: application/json' 112 | curl -v localhost:8080 -d '{"text":"Hello2"}' -H 'Content-Type: application/json' 113 | curl -v localhost:8080 -d '{"text":"Hello3"}' -H 'Content-Type: application/json' 114 | curl -v localhost:8080 -d '{"text":"Hello4"}' -H 'Content-Type: application/json' 115 | curl -v localhost:8080 -d '{"text":"Hello5"}' -H 'Content-Type: application/json' 116 | ``` 117 | 118 | 一つ目のSinkにはいつも通りのログが取得されます(出力結果はSinkの数によって異なります)。 119 | 120 | ``` 121 | Received Hello1 122 | Received Hello2 123 | Received Hello3 124 | Received Hello4 125 | Received Hello5 126 | ``` 127 | 128 | その一方で、今回作成したSinkにもメッセージが届いていることを次のように確認してください。 129 | 130 | ``` 131 | $ curl http://localhost:8085/ 132 | [{"text":"Hello1"},{"text":"Hello2"},{"text":"Hello3"},{"text":"Hello4"},{"text":"Hello5"}] 133 | ``` 134 | 135 | ---- 136 | 137 | Consumer Groupの仕組みを利用して、メッセージを受信するSinkを後から追加できることを確認しました。 138 | 139 | 連携するサービスが後から増えても、サービス呼び出し側(メッセージ送信側)のコードを変更することなくサービスを追加することができます。 140 | 141 | このような手法を[Choreography Style](https://www.thoughtworks.com/insights/blog/scaling-microservices-event-stream)と呼び、マイクロサービスアーキテクチャを実現する上で重要なパターンとなります。 142 | 143 | Spring Cloud Streamを利用することでChoreography Styleを簡単に実現できます。 144 | -------------------------------------------------------------------------------- /cloudamqp.md: -------------------------------------------------------------------------------- 1 | ## [補足資料] CloudAMQPの利用 2 | 3 | ローカル環境にRabbitMQがない場合、FREEプランのあるRabbitMQのSaaSである[CloudAMQP](https://www.cloudamqp.com)を使えます。 4 | 5 | アカウントを作成するか、GitHubまたはGoogleアカウントでログインしてください。 6 | 7 | image 8 | 9 | インスタンス作成画面でNameを入力します。Planは"Little Lemur (Free)"を選択してください。 10 | 11 | image 12 | 13 | RegionとData Centerは"Amazon Web Services"の"AP_NorthEast-1 (Tokyo)"を選択し、インスタンスを作成してください。 14 | 15 | image 16 | 17 | 18 | インスタンス一覧画面から作成したインスタンス名をクリックしてください。 19 | 20 | image 21 | 22 | インスタンスの接続情報が表示されます。 23 | 24 | image 25 | 26 | 27 | このRabbitMQインスタンスにローカル環境から接続したい場合は、"URL"の文字列をコピーして、`application.properties`に次のように設定してください。 28 | 29 | 30 | ``` properties 31 | spring.rabbitmq.addresses=amqps://fmzkajiy:2g6hsLdxgPBLtdaSm19C1byLc-chHIZM@cougar.rmq.cloudamqp.com/fmzkajiy 32 | ``` 33 | 34 | > `spring.rabbitmq.host`, `spring.rabbitmq.port`, `spring.rabbitmq.username`, `spring.rabbitmq.password`, `spring.rabbitmq.addresses`を設定する方法もありますが、
35 | > `spring.rabbitmq.addresses` であれば1プロパティで済みます。また、TLSの設定(`spring.rabbitmq.ssl.enabled=true`)も`amqps://...`から自動で判断して設定されます。
36 | > 詳しくは https://docs.spring.io/spring-boot/docs/current/reference/html/messaging.html#messaging.amqp を参照 37 | 38 | インスタンス一覧画面から"RabbitMQ Manager"をクリックすると管理コンソールにアクセスできます。 39 | 40 | -------------------------------------------------------------------------------- /scale-out.md: -------------------------------------------------------------------------------- 1 | ## Sinkのスケールアウト 2 | 3 | 次にSinkをスケールアウトしましょう。 4 | 5 | ### ローカル環境でスケールアウト 6 | 7 | 現在8082番ポートで起動しているSinkの他に2インスタンスSinkを起動します。 8 | 9 | ``` 10 | java -jar target/hello-sink-0.0.1-SNAPSHOT.jar --server.port=8083 11 | ``` 12 | 13 | 14 | ``` 15 | java -jar target/hello-sink-0.0.1-SNAPSHOT.jar --server.port=8084 16 | ``` 17 | 18 | この状態で、Sourceにリクエストを5回送ります。 19 | 20 | 21 | ``` 22 | curl -v localhost:8080 -d '{"text":"Hello1"}' -H 'Content-Type: application/json' 23 | curl -v localhost:8080 -d '{"text":"Hello2"}' -H 'Content-Type: application/json' 24 | curl -v localhost:8080 -d '{"text":"Hello3"}' -H 'Content-Type: application/json' 25 | curl -v localhost:8080 -d '{"text":"Hello4"}' -H 'Content-Type: application/json' 26 | curl -v localhost:8080 -d '{"text":"Hello5"}' -H 'Content-Type: application/json' 27 | ``` 28 | 29 | おそらく、3つのSinkそれぞれが順番にメッセージを受信し、に次のようなログが出力されていると思います。 30 | 31 | * Sink1 32 | 33 | ``` 34 | Received Hello1 35 | Received Hello4 36 | ``` 37 | 38 | * Sink2 39 | 40 | ``` 41 | Received Hello2 42 | Received Hello5 43 | ``` 44 | 45 | * Sink3 46 | 47 | ``` 48 | Received Hello3 49 | ``` 50 | 51 | 52 | > 【ノート】 Consumer Groupの設定がない場合 53 | > 54 | > Sourceからのメッセージの受信が分散されていることを確認できました。これはSpring Cloud Streamでは同じConsumerGroupを持つConsumerのうち最低1つメッセージが届くことが保証されるという特徴があるためです。 55 | > ではConsumer Groupを設定せずに実行するとどうなるでしょうか。 56 | > 57 | > この場合、Exchangeに対して、Sinkごとに匿名のQueueが出来上がります。 58 | > ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/be80d8f4-03f0-2c79-8cd9-acc17d016c9e.png) 59 | > 60 | > その結果、全てのSinkに次のログが出力されます。 61 | > 62 | > ``` 63 | > Received Hello1 64 | > Received Hello2 65 | > Received Hello3 66 | > Received Hello4 67 | > Received Hello5 68 | > ``` -------------------------------------------------------------------------------- /sink.md: -------------------------------------------------------------------------------- 1 | ## Sinkの作成 2 | 3 | 次にSinkを作成します。 4 | 5 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/07c43f53-22ab-6ae1-26a4-948809767831.png) 6 | 7 | ### プロジェクト作成 8 | 9 | 以下のコマンドを実行すると、`hello-sink`フォルダに雛形プロジェクトが生成されます 10 | 11 | ``` 12 | curl https://start.spring.io/starter.tgz \ 13 | -d artifactId=hello-sink \ 14 | -d baseDir=hello-sink \ 15 | -d packageName=com.example \ 16 | -d dependencies=web,actuator,cloud-stream,amqp \ 17 | -d type=maven-project \ 18 | -d applicationName=HelloSinkApplication | tar -xzvf - 19 | ``` 20 | 21 | > **【ノート】 Web UIからのプロジェクト作成** 22 | > 23 | > `curl`によるプロジェクト作成がうまくいかない場合は[Spring Initializr](https://start.spring.io)にアクセスして、 24 | > 次の項目を入力し、"Generate Project"ボタンをクリックしてください。`hello-sink.zip`がダウンロードされるので、これを展開してください。 25 | > 26 | > * Artifact: hello-sink 27 | > * Search for dependecies: Web, Actuator, Cloud Stream, RabbitMQ 28 | > 29 | > ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/1ebaf1ab-824b-1637-ebee-94c24c596aee.png) 30 | 31 | 32 | 33 | Spring Cloud Streamが作成する`MessageChannel`からTweetを受信するSinkクラスを作成します。 34 | 35 | `Tweet`を受信する処理は`java.util.function.Consumer`で実装できます。 36 | 37 | `src/main/java/com/example/HelloSinkApplication.java`を次のように記述してください。 38 | 39 | ``` java 40 | package com.example; 41 | 42 | import java.util.function.Consumer; 43 | 44 | import org.springframework.boot.SpringApplication; 45 | import org.springframework.boot.autoconfigure.SpringBootApplication; 46 | import org.springframework.context.annotation.Bean; 47 | 48 | @SpringBootApplication 49 | public class HelloSinkApplication { 50 | 51 | public static void main(String[] args) { 52 | SpringApplication.run(HelloSinkApplication.class, args); 53 | } 54 | 55 | @Bean 56 | public Consumer tweetPrinter() { 57 | return tweet -> System.out.println("Received " + tweet.text()); 58 | } 59 | 60 | record Tweet(String text) { 61 | } 62 | } 63 | ``` 64 | 65 | channel名`input`に対するdestination名とConsumerGroup名を`application.properties`に次にように設定してください。 66 | 67 | ``` properties 68 | spring.cloud.stream.function.bindings.tweetPrinter-in-0=input 69 | spring.cloud.stream.bindings.input.destination=hello 70 | spring.cloud.stream.bindings.input.group=printer 71 | ``` 72 | 73 | > **【ノート】 ConsumerGroupの設定** 74 | > 75 | > `spring.cloud.stream.bindings.input.group`にConsumerGroup名を指定しました。Spring Cloud Streamでは同じConsumerGroupを持つConsumerのうち最低1つメッセージが届くことが保証されます。ConsumerGroupを指定しないと同じConsumerアプリケーションでも全てのインスタンスにメッセージが届きます。特別な理由がなければConsumerアプリケーションごとに同じConsumerGroup名を指定するするのが良いです。 76 | 77 | 次のコマンドでこのSourceアプリケーションのjarファイルを作成してください。 78 | 79 | 80 | 81 | ``` 82 | ./mvnw clean package -DskipTests=true 83 | ``` 84 | 85 | `target`ディレクトリに`hello-sink-0.0.1-SNAPSHOT.jar`が 86 | 87 | 88 | ``` 89 | $ ls -lh target/ 90 | total 56840 91 | drwxr-xr-x 4 toshiaki staff 128B Nov 2 02:06 classes 92 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:06 generated-sources 93 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:06 generated-test-sources 94 | -rw-r--r-- 1 toshiaki staff 28M Nov 2 02:06 hello-sink-0.0.1-SNAPSHOT.jar 95 | -rw-r--r-- 1 toshiaki staff 3.7K Nov 2 02:06 hello-sink-0.0.1-SNAPSHOT.jar.original 96 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:06 maven-archiver 97 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:06 maven-status 98 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 02:06 test-classes 99 | ``` 100 | 101 | 102 | ### ローカル環境で実行 103 | 104 | 105 | 次のコマンドでアプリケーションを起動してください。 106 | 107 | ``` 108 | java -jar target/hello-sink-0.0.1-SNAPSHOT.jar --server.port=8082 109 | ``` 110 | 111 | RabbitMQに関する設定は[Sourceの作成#ローカル環境で実行](source.md#ローカル環境で実行)を参照して下さい。 112 | 113 | 以下のコマンドで、Source側にメッセージを送信してください。 114 | 115 | ``` 116 | curl -v localhost:8080 -d '{"text":"Hello"}' -H 'Content-Type: application/json' 117 | ``` 118 | 119 | Sink側のログを見て 120 | 121 | ``` 122 | Received Hello 123 | ``` 124 | 125 | が出力されていることを確認してください。 126 | 127 | 管理コンソール([http://localhost:15672](http://localhost:15672))にアクセスして、Queuesタブを選択してください。`hello.printer` Queueができていることが確認できます。Queue名は`Destination名.ConsumerGroup名`になります。 128 | 129 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/dd5fb790-b348-ee1e-44aa-040cd4ff78e6.png) 130 | 131 | `hello.printer` Queueをクリックして、Overviewを開いてください。この状態で再度リクエストを送ると、MessageがこのQueueに送られていることが見て取れます。またBindingsを見ると`hello`ExchangeにこのQueueがバインドされていることもわかります。 132 | 133 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/17375bd3-fcad-3c2e-4f6f-1e662e288d19.png) 134 | 135 | `hello` Exchangeを再度確認し、Bindingsを見るとこのExchangeに`hello.printer` Queueがバインドされていることがわかります。 136 | 137 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/6c223200-ad11-ca8f-73f2-95df8eb9826d.png) 138 | 139 | RabbitMQのExchangeとQueueが結びつくことで、SourceとSinkがつながりました。 140 | 141 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/c3d7b31b-6a81-bd9d-db06-3abdae4ffd94.png) 142 | 143 | Sink側のアプリケーションをCtrl+Cで止めてください。その後、Sourceに次のメッセージを送信してください。 144 | 145 | ``` 146 | curl -v localhost:8080 -d '{"text":"Hello2"}' -H 'Content-Type: application/json' 147 | ``` 148 | 149 | 次のコマンドで再度Sinkアプリケーションを起動してください。 150 | 151 | ``` 152 | java -jar target/hello-sink-0.0.1-SNAPSHOT.jar --server.port=8082 153 | ``` 154 | 155 | 起動するや否や、次のログが出力されることを確認できるでしょう。 156 | 157 | ``` 158 | Received Hello2 159 | ``` 160 | 161 | Sinkが一度でも接続されていれば、Sourceにメッセージを書き込んだ段階でSinkがダウンしていてもSinkが復帰したタイミングでメッセージを受信することができます。 162 | これにより、書き込みに対するシステムの耐障害性を高くすることができます。 163 | 164 | ### Unit Testの作成 165 | 166 | Spring Cloud Streamにはテスト支援機能も用意されています。テスト時はMessage Binderのインメモリ実装が使われるようになります。これにより、開発中はMessage Binder(RabbitMQ)を用意しなくてもテストを進めることができます。 167 | 168 | `src/test/java/com/example/HelloSinkApplicationTests.java`に次の内容を記述してください。 169 | 170 | ``` java 171 | package com.example; 172 | 173 | import com.example.HelloSinkApplication.Tweet; 174 | import org.junit.jupiter.api.Test; 175 | import org.junit.jupiter.api.extension.ExtendWith; 176 | 177 | import org.springframework.beans.factory.annotation.Autowired; 178 | import org.springframework.boot.test.context.SpringBootTest; 179 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 180 | import org.springframework.boot.test.system.CapturedOutput; 181 | import org.springframework.boot.test.system.OutputCaptureExtension; 182 | import org.springframework.cloud.stream.binder.test.InputDestination; 183 | import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; 184 | import org.springframework.context.annotation.Import; 185 | import org.springframework.messaging.Message; 186 | import org.springframework.messaging.support.MessageBuilder; 187 | 188 | import static org.assertj.core.api.Assertions.assertThat; 189 | 190 | @SpringBootTest(webEnvironment = WebEnvironment.NONE) 191 | @Import(TestChannelBinderConfiguration.class) 192 | @ExtendWith(OutputCaptureExtension.class) 193 | class HelloSinkApplicationTests { 194 | @Autowired 195 | InputDestination destination; 196 | 197 | @Test 198 | void contextLoads(CapturedOutput capture) { 199 | final Tweet tweet = new Tweet("Hello"); 200 | final Message message = MessageBuilder.withPayload(tweet).build(); 201 | this.destination.send(message, "hello"); 202 | assertThat(capture.toString()).contains("Received Hello" + System.lineSeparator()); 203 | } 204 | 205 | } 206 | ``` 207 | 208 | 次のようにテストが通ればOKです。 209 | 210 | 211 | ``` console 212 | $ ./mvnw clean test 213 | [INFO] Scanning for projects... 214 | [INFO] 215 | [INFO] -----------------------< com.example:hello-sink >----------------------- 216 | [INFO] Building demo 0.0.1-SNAPSHOT 217 | [INFO] --------------------------------[ jar ]--------------------------------- 218 | [INFO] 219 | [INFO] --- maven-clean-plugin:3.2.0:clean (default-clean) @ hello-sink --- 220 | [INFO] Deleting /Users/toshiaki/git/hello-sink/target 221 | [INFO] 222 | [INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ hello-sink --- 223 | [INFO] Copying 1 resource from src/main/resources to target/classes 224 | [INFO] Copying 0 resource from src/main/resources to target/classes 225 | [INFO] 226 | [INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) @ hello-sink --- 227 | [INFO] Changes detected - recompiling the module! 228 | [INFO] Compiling 1 source file to /Users/toshiaki/git/hello-sink/target/classes 229 | [INFO] 230 | [INFO] --- maven-resources-plugin:3.3.1:testResources (default-testResources) @ hello-sink --- 231 | [INFO] skip non existing resourceDirectory /Users/toshiaki/git/hello-sink/src/test/resources 232 | [INFO] 233 | [INFO] --- maven-compiler-plugin:3.10.1:testCompile (default-testCompile) @ hello-sink --- 234 | [INFO] Changes detected - recompiling the module! 235 | [INFO] Compiling 1 source file to /Users/toshiaki/git/hello-sink/target/test-classes 236 | [INFO] 237 | [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ hello-sink --- 238 | [INFO] 239 | [INFO] ------------------------------------------------------- 240 | [INFO] T E S T S 241 | [INFO] ------------------------------------------------------- 242 | [INFO] Running com.example.HelloSinkApplicationTests 243 | 21:34:10.794 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Neither @ContextConfiguration nor @ContextHierarchy found for test class [HelloSinkApplicationTests]: using SpringBootContextLoader 244 | 21:34:10.797 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader -- Could not detect default resource locations for test class [com.example.HelloSinkApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}. 245 | 21:34:10.798 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.example.HelloSinkApplicationTests]: HelloSinkApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 246 | 21:34:10.823 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Using ContextCustomizers for test class [HelloSinkApplicationTests]: [ImportsContextCustomizer, ExcludeFilterContextCustomizer, DuplicateJsonObjectContextCustomizer, MockitoContextCustomizer, TestRestTemplateContextCustomizer, WebTestClientContextCustomizer, DisableObservabilityContextCustomizer, PropertyMappingContextCustomizer, Customizer] 247 | 21:34:10.895 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider -- Identified candidate component class: file [/Users/toshiaki/git/hello-sink/target/classes/com/example/HelloSinkApplication.class] 248 | 21:34:10.896 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.example.HelloSinkApplication for test class com.example.HelloSinkApplicationTests 249 | 21:34:11.023 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Using TestExecutionListeners for test class [HelloSinkApplicationTests]: [ServletTestExecutionListener, DirtiesContextBeforeModesTestExecutionListener, ApplicationEventsTestExecutionListener, MockitoTestExecutionListener, DependencyInjectionTestExecutionListener, DirtiesContextTestExecutionListener, TransactionalTestExecutionListener, SqlScriptsTestExecutionListener, EventPublishingTestExecutionListener, ResetMocksTestExecutionListener, RestDocsTestExecutionListener, MockRestServiceServerResetTestExecutionListener, MockMvcPrintOnlyOnFailureTestExecutionListener, WebDriverTestExecutionListener, MockWebServiceServerTestExecutionListener] 250 | 21:34:11.024 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener -- Before test class: class [HelloSinkApplicationTests], class annotated with @DirtiesContext [false] with mode [null] 251 | 21:34:11.040 [main] DEBUG org.springframework.test.context.support.DependencyInjectionTestExecutionListener -- Performing dependency injection for test class com.example.HelloSinkApplicationTests 252 | 253 | . ____ _ __ _ _ 254 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 255 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 256 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 257 | ' |____| .__|_| |_|_| |_\__, | / / / / 258 | =========|_|==============|___/=/_/_/_/ 259 | :: Spring Boot :: (v3.0.6) 260 | 261 | 2023-05-16T21:34:11.362+09:00 INFO 6899 --- [ main] com.example.HelloSinkApplicationTests : Starting HelloSinkApplicationTests using Java 17.0.5 with PID 6899 (started by toshiaki in /Users/toshiaki/git/hello-sink) 262 | 2023-05-16T21:34:11.365+09:00 INFO 6899 --- [ main] com.example.HelloSinkApplicationTests : No active profile set, falling back to 1 default profile: "default" 263 | 2023-05-16T21:34:12.212+09:00 INFO 6899 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'errorChannel' has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created. 264 | 2023-05-16T21:34:12.220+09:00 INFO 6899 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'integrationHeaderChannelRegistry' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created. 265 | 2023-05-16T21:34:13.218+09:00 INFO 6899 --- [ main] o.s.c.s.m.DirectWithAttributesChannel : Channel 'application.input' has 1 subscriber(s). 266 | 2023-05-16T21:34:13.497+09:00 INFO 6899 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel 267 | 2023-05-16T21:34:13.497+09:00 INFO 6899 --- [ main] o.s.i.channel.PublishSubscribeChannel : Channel 'application.errorChannel' has 1 subscriber(s). 268 | 2023-05-16T21:34:13.497+09:00 INFO 6899 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean '_org.springframework.integration.errorLogger' 269 | 2023-05-16T21:34:13.507+09:00 INFO 6899 --- [ main] o.s.c.stream.binder.BinderErrorChannel : Channel '22249027.input.errors' has 1 subscriber(s). 270 | 2023-05-16T21:34:13.508+09:00 INFO 6899 --- [ main] o.s.c.stream.binder.BinderErrorChannel : Channel '22249027.input.errors' has 2 subscriber(s). 271 | 2023-05-16T21:34:13.509+09:00 INFO 6899 --- [ main] o.s.i.channel.PublishSubscribeChannel : Channel 'hello.destination' has 1 subscriber(s). 272 | 2023-05-16T21:34:13.510+09:00 INFO 6899 --- [ main] r$IntegrationBinderInboundChannelAdapter : started org.springframework.cloud.stream.binder.test.TestChannelBinder$IntegrationBinderInboundChannelAdapter@77896335 273 | 2023-05-16T21:34:13.526+09:00 INFO 6899 --- [ main] com.example.HelloSinkApplicationTests : Started HelloSinkApplicationTests in 2.457 seconds (process running for 3.333) 274 | Received Hello 275 | [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.306 s - in com.example.HelloSinkApplicationTests 276 | 2023-05-16T21:34:13.997+09:00 INFO 6899 --- [ionShutdownHook] r$IntegrationBinderInboundChannelAdapter : stopped org.springframework.cloud.stream.binder.test.TestChannelBinder$IntegrationBinderInboundChannelAdapter@77896335 277 | 2023-05-16T21:34:14.002+09:00 INFO 6899 --- [ionShutdownHook] o.s.c.stream.binder.BinderErrorChannel : Channel 'application.22249027.input.errors' has 1 subscriber(s). 278 | 2023-05-16T21:34:14.006+09:00 INFO 6899 --- [ionShutdownHook] o.s.i.endpoint.EventDrivenConsumer : Removing {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel 279 | 2023-05-16T21:34:14.007+09:00 INFO 6899 --- [ionShutdownHook] o.s.i.channel.PublishSubscribeChannel : Channel 'application.errorChannel' has 0 subscriber(s). 280 | 2023-05-16T21:34:14.008+09:00 INFO 6899 --- [ionShutdownHook] o.s.i.endpoint.EventDrivenConsumer : stopped bean '_org.springframework.integration.errorLogger' 281 | [INFO] 282 | [INFO] Results: 283 | [INFO] 284 | [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 285 | [INFO] 286 | [INFO] ------------------------------------------------------------------------ 287 | [INFO] BUILD SUCCESS 288 | [INFO] ------------------------------------------------------------------------ 289 | [INFO] Total time: 6.723 s 290 | [INFO] Finished at: 2023-05-16T21:34:14+09:00 291 | [INFO] ------------------------------------------------------------------------ 292 | ``` 293 | -------------------------------------------------------------------------------- /source.md: -------------------------------------------------------------------------------- 1 | ## Sourceの作成 2 | 3 | まずはSourceから作成します。 4 | 5 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/bcd1fc24-2966-7765-f4f4-56132dbc61b8.png) 6 | 7 | 8 | ### プロジェクト作成 9 | 10 | 以下のコマンドを実行すると、`hello-source`フォルダに雛形プロジェクトが生成されます 11 | 12 | ``` 13 | curl https://start.spring.io/starter.tgz \ 14 | -d artifactId=hello-source \ 15 | -d baseDir=hello-source \ 16 | -d packageName=com.example \ 17 | -d dependencies=web,actuator,cloud-stream,amqp \ 18 | -d type=maven-project \ 19 | -d applicationName=HelloSourceApplication | tar -xzvf - 20 | ``` 21 | 22 | > **【ノート】 Web UIからのプロジェクト作成** 23 | > 24 | > `curl`によるプロジェクト作成がうまくいかない場合は[Spring Initializr](https://start.spring.io)にアクセスして、 25 | > 次の項目を入力し、"Generate Project"ボタンをクリックしてください。`hello-source.zip`がダウンロードされるので、これを展開してください。 26 | > 27 | > * Artifact: hello-source 28 | > * Search for dependecies: Web, Actuator, Cloud Stream, RabbitMQ 29 | > 30 | > ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/acd4935f-4e50-0004-55f4-2d142d8a65a2.png) 31 | 32 | 33 | ### Sourceの実装 34 | 35 | Spring Cloud Streamが作成する`MessageChannel`にTweetを送信するSourceクラスを作成します。ここではHTTPでJSONを受け付けて、`Tweet`インスタンスを作成し、`StreamBridge`を利用して`Tweet`を送信しています。 36 | 37 | `src/main/java/com/example/HelloSourceApplication.java`を次のように記述してください。 38 | 39 | ``` java 40 | package com.example; 41 | 42 | import org.springframework.boot.SpringApplication; 43 | import org.springframework.boot.autoconfigure.SpringBootApplication; 44 | import org.springframework.cloud.stream.function.StreamBridge; 45 | import org.springframework.web.bind.annotation.PostMapping; 46 | import org.springframework.web.bind.annotation.RequestBody; 47 | import org.springframework.web.bind.annotation.RestController; 48 | 49 | @SpringBootApplication 50 | @RestController 51 | public class HelloSourceApplication { 52 | 53 | private final StreamBridge streamBridge; 54 | 55 | public HelloSourceApplication(StreamBridge streamBridge) { 56 | this.streamBridge = streamBridge; 57 | } 58 | 59 | @PostMapping(path = "/") 60 | public void tweet(@RequestBody Tweet tweet) { 61 | this.streamBridge.send("output", tweet); 62 | } 63 | 64 | public static void main(String[] args) { 65 | SpringApplication.run(HelloSourceApplication.class, args); 66 | } 67 | 68 | record Tweet(String text) { 69 | } 70 | } 71 | ``` 72 | 73 | binding名`output`に対するdestination名を`application.properties`に次にように設定してください。 74 | 75 | ``` properties 76 | spring.cloud.stream.bindings.output.destination=hello 77 | ``` 78 | 79 | 80 | 次のコマンドでこのSourceアプリケーションのjarファイルを作成してください。 81 | 82 | 83 | 84 | ``` 85 | ./mvnw clean package -DskipTests=true 86 | ``` 87 | 88 | `target`ディレクトリに`hello-source-0.0.1-SNAPSHOT.jar`ができていることを確認してください。 89 | 90 | ``` 91 | $ ls -lh target/ 92 | total 56840 93 | drwxr-xr-x 4 toshiaki staff 128B Nov 2 01:36 classes 94 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 01:36 generated-sources 95 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 01:36 generated-test-sources 96 | -rw-r--r-- 1 toshiaki staff 28M Nov 2 01:36 hello-source-0.0.1-SNAPSHOT.jar 97 | -rw-r--r-- 1 toshiaki staff 3.4K Nov 2 01:36 hello-source-0.0.1-SNAPSHOT.jar.original 98 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 01:36 maven-archiver 99 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 01:36 maven-status 100 | drwxr-xr-x 3 toshiaki staff 96B Nov 2 01:36 test-classes 101 | ``` 102 | 103 | ### ローカル環境で実行 104 | 105 | ローカル環境でこのアプリケーションを実行する場合はRabbitMQのインストールが必要です。 106 | 107 | Mac(brew)の場合は、次のコマンドでインストールしてください。 108 | 109 | ``` 110 | brew install rabbitmq 111 | brew services start rabbitmq 112 | ``` 113 | Dockerを使う場合は、次のコマンドを実行してください。 114 | 115 | ``` 116 | docker run -p 15672:15672 -p 5672:5672 rabbitmq:3-management 117 | ``` 118 | 119 | 次のコマンドでアプリケーションを起動してください。 120 | 121 | ``` 122 | java -jar target/hello-source-0.0.1-SNAPSHOT.jar 123 | ``` 124 | 125 | RabbitMQが別のサーバー上にいる場合は次のように`spring.rabbitmq.host`を指定してください。 126 | 127 | ``` 128 | java -jar target/hello-source-0.0.1-SNAPSHOT.jar --spring.rabbitmq.host=192.168.99.100 129 | ``` 130 | 131 | ユーザー名、パスワードが必要な場合は`spring.rabbitmq.username`、`spring.rabbitmq.password`を指定してください。 132 | 133 | ``` 134 | java -jar target/hello-source-0.0.1-SNAPSHOT.jar --spring.rabbitmq.host=192.168.99.100 --spring.rabbitmq.username=user --spring.rabbitmq.password=pass 135 | ``` 136 | 137 | Virtual Hostを設定している場合は`spring.rabbitmq.virtual-host`を指定してください。 138 | ``` 139 | java -jar target/hello-source-0.0.1-SNAPSHOT.jar --spring.rabbitmq.host=192.168.99.100 --spring.rabbitmq.username=user --spring.rabbitmq.password=pass --spring.rabbitmq.virtual-host=/ 140 | ``` 141 | 142 | 143 | > **【ノート】 `application.properties`にRabbitMQの接続情報を設定** 144 | > 145 | > RabbitMQの接続情報は起動時に指定するだけでなく、`application.properties`に設定しておくこともできます。この場合は再度jarファイルをビルドしてください。 146 | > 147 | > ``` properties 148 | > spring.rabbitmq.host=192.168.99.100 149 | > spring.rabbitmq.username=user 150 | > spring.rabbitmq.password=pass 151 | > spring.rabbitmq.virtual-host=/ # if needed 152 | > ``` 153 | > 154 | > CloudAMQPを利用する場合は「[[補足資料] CloudAMQPの利用](cloudamqp.md)」を参照してください。 155 | 156 | 157 | 以下のコマンドで、メッセージを送信してください。 158 | ``` 159 | curl -v localhost:8080 -d '{"text":"Hello"}' -H 'Content-Type: application/json' 160 | ``` 161 | 162 | 管理コンソール([http://localhost:15672](http://localhost:15672))にアクセスして、Exchangesタブを選択してください。`hello`Exchangeができていることが確認できます。 163 | 164 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/589d9326-1640-1375-2bf5-c09f92e0c220.png) 165 | 166 | `hello`Exchangeをクリックして、Overviewを開いてください。この状態で再度リクエストを送ると、MessageがこのExchangeに送られていることが見て取れます。 167 | 168 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/fb35c19b-4cb8-075d-e193-26896b76095a.png) 169 | 170 | ただし、この時点ではこのExchangeにはQueueがバインドされておらず、メッセージはそのまま流れていきます。 171 | 172 | ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/13173095-8ce9-c504-d734-69b809dc2937.png) 173 | 174 | > **【ノート】 接続しているRabbitMQのヘルスチェック** 175 | > 176 | > Spring Boot Actuatorを追加しているため、`/actuator/health`エンドポイントでヘルスチェック結果を見ることができます。 177 | > 178 | > ``` console 179 | > $ curl localhost:8080/actuator/health 180 | > {"status":"UP"} 181 | > ``` 182 | > 183 | > `management.endpoint.health.show-details`プロパティを`always`にすればヘルスチェックの詳細も表示されます。 184 | > 185 | > ``` 186 | > java -jar target/hello-source-0.0.1-SNAPSHOT.jar --management.endpoint.health.show-details=always 187 | > ``` 188 | > 189 | > ``` console 190 | > $ curl localhost:8080/actuator/health 191 | > {"status":"UP","components":{"diskSpace":{"status":"UP","details":{"total":1000240963584,"free":174634520576,"threshold":10485760,"exists":true}},"ping":{"status":"UP"},"rabbit":{"status":"UP","details":{"version":"3.9.5"}}}} 192 | > ``` 193 | 194 | ### Unit Testの作成 195 | 196 | Spring Cloud Streamにはテスト支援機能も用意されています。テスト時はMessage Binderのインメモリ実装が使われるようになります。これにより、開発中はMessage Binder(RabbitMQ)を用意しなくてもテストを進めることができます。 197 | 198 | 199 | `src/test/java/com/example/HelloSourceApplicationTests.java`に次の内容を記述してください。 200 | 201 | ``` java 202 | package com.example; 203 | 204 | import com.example.HelloSourceApplication.Tweet; 205 | import org.junit.jupiter.api.Test; 206 | 207 | import org.springframework.beans.factory.annotation.Autowired; 208 | import org.springframework.boot.test.context.SpringBootTest; 209 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 210 | import org.springframework.cloud.stream.binder.test.OutputDestination; 211 | import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; 212 | import org.springframework.context.annotation.Import; 213 | import org.springframework.messaging.Message; 214 | import org.springframework.messaging.converter.CompositeMessageConverter; 215 | 216 | import static org.assertj.core.api.Assertions.assertThat; 217 | 218 | @SpringBootTest(webEnvironment = WebEnvironment.NONE) 219 | @Import(TestChannelBinderConfiguration.class) 220 | class HelloSourceApplicationTests { 221 | @Autowired 222 | HelloSourceApplication app; 223 | 224 | @Autowired 225 | OutputDestination destination; 226 | 227 | @Autowired 228 | CompositeMessageConverter messageConverter; 229 | 230 | @Test 231 | void contextLoads() { 232 | final Tweet tweet = new Tweet("hello!"); 233 | this.app.tweet(tweet); 234 | final Message message = this.destination.receive(3, "hello"); 235 | final Tweet output = (Tweet) this.messageConverter.fromMessage(message, Tweet.class); 236 | assertThat(output).isNotNull(); 237 | assertThat(output.text()).isEqualTo("hello!"); 238 | } 239 | 240 | } 241 | ``` 242 | 243 | `Message`のpayloadがJSON文字列になっていることに注意してください。 244 | 245 | 246 | 次のようにテストが通ればOKです。 247 | 248 | ``` console 249 | $ ./mvnw clean test 250 | [INFO] Scanning for projects... 251 | [INFO] 252 | [INFO] ----------------------< com.example:hello-source >---------------------- 253 | [INFO] Building demo 0.0.1-SNAPSHOT 254 | [INFO] --------------------------------[ jar ]--------------------------------- 255 | [INFO] 256 | [INFO] --- maven-clean-plugin:3.2.0:clean (default-clean) @ hello-source --- 257 | [INFO] Deleting /Users/toshiaki/git/hello-source/target 258 | [INFO] 259 | [INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ hello-source --- 260 | [INFO] Copying 1 resource from src/main/resources to target/classes 261 | [INFO] Copying 0 resource from src/main/resources to target/classes 262 | [INFO] 263 | [INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) @ hello-source --- 264 | [INFO] Changes detected - recompiling the module! 265 | [INFO] Compiling 1 source file to /Users/toshiaki/git/hello-source/target/classes 266 | [INFO] 267 | [INFO] --- maven-resources-plugin:3.3.1:testResources (default-testResources) @ hello-source --- 268 | [INFO] skip non existing resourceDirectory /Users/toshiaki/git/hello-source/src/test/resources 269 | [INFO] 270 | [INFO] --- maven-compiler-plugin:3.10.1:testCompile (default-testCompile) @ hello-source --- 271 | [INFO] Changes detected - recompiling the module! 272 | [INFO] Compiling 1 source file to /Users/toshiaki/git/hello-source/target/test-classes 273 | [INFO] 274 | [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ hello-source --- 275 | [INFO] 276 | [INFO] ------------------------------------------------------- 277 | [INFO] T E S T S 278 | [INFO] ------------------------------------------------------- 279 | [INFO] Running com.example.HelloSourceApplicationTests 280 | 20:28:41.594 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Neither @ContextConfiguration nor @ContextHierarchy found for test class [HelloSourceApplicationTests]: using SpringBootContextLoader 281 | 20:28:41.599 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader -- Could not detect default resource locations for test class [com.example.HelloSourceApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}. 282 | 20:28:41.601 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.example.HelloSourceApplicationTests]: HelloSourceApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 283 | 20:28:41.640 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Using ContextCustomizers for test class [HelloSourceApplicationTests]: [ImportsContextCustomizer, ExcludeFilterContextCustomizer, DuplicateJsonObjectContextCustomizer, MockitoContextCustomizer, TestRestTemplateContextCustomizer, WebTestClientContextCustomizer, DisableObservabilityContextCustomizer, PropertyMappingContextCustomizer, Customizer] 284 | 20:28:41.751 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider -- Identified candidate component class: file [/Users/toshiaki/git/hello-source/target/classes/com/example/HelloSourceApplication.class] 285 | 20:28:41.753 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.example.HelloSourceApplication for test class com.example.HelloSourceApplicationTests 286 | 20:28:41.908 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Using TestExecutionListeners for test class [HelloSourceApplicationTests]: [ServletTestExecutionListener, DirtiesContextBeforeModesTestExecutionListener, ApplicationEventsTestExecutionListener, MockitoTestExecutionListener, DependencyInjectionTestExecutionListener, DirtiesContextTestExecutionListener, TransactionalTestExecutionListener, SqlScriptsTestExecutionListener, EventPublishingTestExecutionListener, ResetMocksTestExecutionListener, RestDocsTestExecutionListener, MockRestServiceServerResetTestExecutionListener, MockMvcPrintOnlyOnFailureTestExecutionListener, WebDriverTestExecutionListener, MockWebServiceServerTestExecutionListener] 287 | 20:28:41.910 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener -- Before test class: class [HelloSourceApplicationTests], class annotated with @DirtiesContext [false] with mode [null] 288 | 20:28:41.926 [main] DEBUG org.springframework.test.context.support.DependencyInjectionTestExecutionListener -- Performing dependency injection for test class com.example.HelloSourceApplicationTests 289 | 290 | . ____ _ __ _ _ 291 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 292 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 293 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 294 | ' |____| .__|_| |_|_| |_\__, | / / / / 295 | =========|_|==============|___/=/_/_/_/ 296 | :: Spring Boot :: (v3.0.6) 297 | 298 | 2023-05-16T20:28:42.247+09:00 INFO 6684 --- [ main] c.example.HelloSourceApplicationTests : Starting HelloSourceApplicationTests using Java 17.0.5 with PID 6684 (started by toshiaki in /Users/toshiaki/git/hello-source) 299 | 2023-05-16T20:28:42.250+09:00 INFO 6684 --- [ main] c.example.HelloSourceApplicationTests : No active profile set, falling back to 1 default profile: "default" 300 | 2023-05-16T20:28:43.329+09:00 INFO 6684 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'errorChannel' has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created. 301 | 2023-05-16T20:28:43.338+09:00 INFO 6684 --- [ main] faultConfiguringBeanFactoryPostProcessor : No bean named 'integrationHeaderChannelRegistry' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created. 302 | 2023-05-16T20:28:44.593+09:00 INFO 6684 --- [ main] o.s.i.endpoint.EventDrivenConsumer : Adding {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel 303 | 2023-05-16T20:28:44.594+09:00 INFO 6684 --- [ main] o.s.i.channel.PublishSubscribeChannel : Channel 'application.errorChannel' has 1 subscriber(s). 304 | 2023-05-16T20:28:44.595+09:00 INFO 6684 --- [ main] o.s.i.endpoint.EventDrivenConsumer : started bean '_org.springframework.integration.errorLogger' 305 | 2023-05-16T20:28:44.626+09:00 INFO 6684 --- [ main] c.example.HelloSourceApplicationTests : Started HelloSourceApplicationTests in 2.673 seconds (process running for 4.041) 306 | 2023-05-16T20:28:45.205+09:00 INFO 6684 --- [ main] o.s.i.channel.PublishSubscribeChannel : Channel 'hello.destination' has 1 subscriber(s). 307 | 2023-05-16T20:28:45.206+09:00 INFO 6684 --- [ main] o.s.c.s.m.DirectWithAttributesChannel : Channel 'unknown.channel.name' has 1 subscriber(s). 308 | [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.97 s - in com.example.HelloSourceApplicationTests 309 | 2023-05-16T20:28:45.391+09:00 INFO 6684 --- [ionShutdownHook] o.s.i.endpoint.EventDrivenConsumer : Removing {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel 310 | 2023-05-16T20:28:45.391+09:00 INFO 6684 --- [ionShutdownHook] o.s.i.channel.PublishSubscribeChannel : Channel 'application.errorChannel' has 0 subscriber(s). 311 | 2023-05-16T20:28:45.392+09:00 INFO 6684 --- [ionShutdownHook] o.s.i.endpoint.EventDrivenConsumer : stopped bean '_org.springframework.integration.errorLogger' 312 | [INFO] 313 | [INFO] Results: 314 | [INFO] 315 | [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 316 | [INFO] 317 | [INFO] ------------------------------------------------------------------------ 318 | [INFO] BUILD SUCCESS 319 | [INFO] ------------------------------------------------------------------------ 320 | [INFO] Total time: 8.092 s 321 | [INFO] Finished at: 2023-05-16T20:28:45+09:00 322 | [INFO] ------------------------------------------------------------------------ 323 | ``` 324 | --------------------------------------------------------------------------------