バグから学ぶSpring IntegrationのTcpConnectionInterceptorの仕組み

お仕事でSpring Integrationを使う機会があり、古いバージョンから新しいバージョンへアップデートを試みた際に、旧バージョンで動いていた処理が新バージョンで動かない（＝そもそも処理が呼び出されない）という事象にぶつかってしまいました。複数のマイナーバージョンを跨ぐアップデートだったのである程度破壊的な変更が含めれていることは覚悟していましたが、想定どおり動いているケースもあったので最初は理由がよくわかりませんでした。

# 謎事象が起こった仕組みは？

よくわからない事象が発生したのは、TCP通信処理に任意の処理を割り込ませることができる仕組み（`TcpConnectionInterceptor`）を使って処理を行っているところでした。

# そもそもTCP通信処理ってどんな感じなの？

`TcpConnectionInterceptor`の話をする前にめっちゃ簡単にSpring IntegrationのTCP通信処理がどんな感じでメッセージの送信と受信を行っているのかを紹介しておきます。実際には複数の仕組みが存在しますが、ここでは代表的なパターンを一つ紹介しておきます。

以下はNIOを利用した時の処理フローの概要イメージになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117313/7ab672fa-db31-52f4-231e-addfb30d175c.png)

### メッセージの送信

メッセージの送信を行う際は、`TcpSendingMessageHandler`(`TcpSender`を実装している)というコンポーネントを介して`TcpConnection`の`send`メソッドを介してNIOのAPIを使ってデータを書き込みます。

### メッセージの受信

メッセージの受信を行う際は、NIOのAPI経由で読み取ったデータを`TcpListener`の`onMessage`メソッドを介して`TcpReceivingChannelAdapter`へ渡され、その後は`TcpReceivingChannelAdapter`に紐付たチャネルへメッセージが連携されます。

# `TcpConnectionInterceptor`って何もの？

`TcpConnectionInterceptor`は、ざっくりいうと・・・

* 送信処理であれば`TcpSender`から`TcpConnection`の`send`メソッドを呼び出した際に、`TcpConnection`本体のメソッドが呼び出される前に任意の処理を割り込みすることができる

* 受信処理であれば`TcpConnection`から`TcpListener`の`onMessage`メソッドを呼び出した際に、`TcpListener`本体のメソッドが呼び出される前に任意の処理を割り込みすることができる

仕組みです。

> **NOTE:**
>
> 正確にいうと・・・`send`や`onMessage`以外のメソッドにも割り込みが可能ですが、本投稿では意図的に割愛します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117313/75fe02c8-6349-0907-d13b-37e660658189.png)


# どんな謎事象だった？

前提となる最低限の情報を共有したところで、今回発生した謎事象がどんなものだったかというと・・・・

* TCPサーバ（＝リッスンポートを立ち上げてクライアントからつないでもらうモード）で扱う`TcpConnection`に複数の`TcpConnectionInterceptor`を適用していると送信処理にて最初のインタセプタしか適用されない

というものでした。具体的には以下のような処理フローになっていました。

> **NOTE:**
>
> この事象はSpring Integrationへバグレポート済み([#8609](https://github.com/spring-projects/spring-integration/issues/8609))で、次回の計画リリースでリリースされる予定です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117313/c0e10d3a-2644-f645-6ab2-622e5a99f8a6.png)

> **NOTE:**
>
> ここでは詳細は割愛しますが、TCPクライアント（＝他のTCPサーバへつなぎにいくモード）の場合は`TcpSender`で利用する`TcpConnection`を決定する仕組みが異なるため本事象は発生しません。⇨　クライアントモード時は動いていたので・・・謎事象だと思ってしまいましたw

# 原因は？

通常はライブラリ利用者が気にする必要がない部分ですが、Spring Integrationはコネクション生成時に以下のような処理を行っています。

* 接続処理を行い`TcpConnection`本体（例：`TcpNioConnection`）の生成
* `TcpConnectionInterceptor`を生成し、オリジナルの`TcpConnection`または他のインタセプタをラップ
* 最後(=大外)の`TcpConnectionInterceptor`を介して`TcpSender`を登録 →　メソッドチェインにより各インタセプタに`TcpSender`が登録される
* 各インタセプタに`TcpSender`を登録した後に`TcpSender`で利用する`TcpConnection`を`addNewConnection`メソッドを介して（＝コールバックして）伝播する

本来であれば、`TcpSender`（`TcpSendingMessageHandler`）には「`Interceptor3`」が伝播されるべきですが、「`Interceptor1`」が伝播される仕組みになっていたために本事象が発生していました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117313/39aca04c-4547-dd3f-a994-8b876dd81f51.png)


# 修正方法は？

`TcpSender`で利用する`TcpConnection`を`addNewConnection`メソッドを介して（＝コールバックして）伝播する際に、**チェイン先の`addNewConnection`メソッドにチェイン元のインスタンスを渡す**ように修正することで、`TcpSender`（`TcpSendingMessageHandler`）に「`Interceptor3`」を伝播することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117313/3d54a98b-db24-f21a-a3ab-bf10d087d884.png)


# 本事象の影響を受けるバージョン

おそらく以下のバージョンでこの事象が発生します。

* Spring Integration 6.0.0 〜 6.0.5
* Spring Integration 5.5.9 〜 5.5.17

# 修正されるバージョン

2023/5/17リリース予定の以下のバージョンで本事象が解消される予定です。

* Spring Integration 6.1.0 
* Spring Integration 6.0.6
* Spring Integration 5.5.18

# バージョンアップせずに本事象を回避できないのか？

**バージョンアップすることを推奨しますが・・・**、修正版の正式バージョンリリース後であっても「**何かしらの理由**でSpring Integration自体のバージョンアップができない or したくない」が本事象は解消したい場合は、`TcpSender`に「最後にインタセプタが適用されたコネクションを伝播するような`TcpConnectionInterceptor`」を適用することで本事象を回避することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117313/bfc8347b-f0ad-1409-885b-eec67a2ae63a.png)

> **NOTE:**
>
> 具体的に以下のGiHubリポジトリにサンプル実装があるので参考にしてみてください。
>
> * https://github.com/kazuki43zoo/spi-gh-8609

# 参考

* https://docs.spring.io/spring-integration/reference/html/ip.html#ip
* https://docs.spring.io/spring-integration/reference/html/ip.html#ip-interceptors

# 終わりに

久々にOSS活動して疲れましたw　かなりサボっていたのでGWをきっかけにOSS活動も復活しよ〜