Apache Camelを使用したシステム間連携ロジックの集約

この記事は GMOインターネットグループ Advent Calendar 2024 12日目の記事です。

GMOペイメントゲートウェイの羽鳥です。主にクレジットカード決済システムや、カード会社様向けシステムの開発にプログラマーとして携わっています。

今回お話しするのは、今携わっているプロジェクトで、システム間連携のロジックをApache Camelを使用して一か所に集約したときのお話です。

プロジェクトについて

現在携わっているプロジェクトは、クレジットカードの発行を行うお客様向けのシステムです。システムの内部は、

  • 入会審査を行うシステム
  • 売上情報を処理するシステム
  • 債権を扱うシステム

に分かれています。これらのシステムは互いにファイルでデータをやり取りします。

課題について

全てのシステムが同じファイル送受信の方式を採用していれば簡単なのですが、実際はそれぞれHULFT、SFTPといった具合にそれぞれ異なっていました。

このため、以下の課題がありました。

  • 各システムの中にファイル送受信をするサーバは複数存在するため、相手方のサーバの接続するためのロジックの重複が発生
  • 接続方式が変更になると、全てのサーバに変更が必要

これでは非効率なため、代理でファイル送受信を行うサーバを構築する必要がありました。ここで、代理でファイル送受信を行うアプリケーションを構築するのに最適だったのがApache Camelです。

Apache Camelとは

Apache Camelは、統合パターン (Enterprise Integration Patterns: EIP) を実装するためのオープンソースフレームワークです。分散システム間のデータ連携やプロトコル変換を効率的に行うためのツールであり、多数のコンポーネントを使用して、ファイル、メッセージング、HTTPなど、さまざまなシステム間での通信を簡単に構築できます。

Camelの主要な特徴は次の通りです。

  • EIPに基づく柔軟なルーティングと処理:シンプルなDSL(Domain Specific Language)を使用し、データ処理の流れをルートとして記述可能。
  • 豊富なコンポーネント:FTP、SFTP、JMS、HTTPなどのプロトコル、さらにAWS、Azureといった外部サービスの連携用コンポーネントを提供。
  • Producer、Consumer、Processorの概念:データの送信、受信、加工をシンプルにモデル化。

サンプルコード

from("sftp://[email protected]:22/source?password=sourceSecret") // Consumer: SFTPからデータを取得
                    .process(exchange -> { // Processor: データ内容を加工
                        String body = exchange.getIn().getBody(String.class);
                        body = body.replace("oldValue", "newValue"); // 例: 文字列を置換
                        exchange.getMessage().setBody(body);
                    })
                    .to("sftp://[email protected]:22/target?password=targetSecret"); // Producer: SFTPにデータを送信

上記はDSLを使用したApache Camelのルート定義の例です。分解して説明します。

Consumer:ファイルを取得

from("sftp://[email protected]:22/source?password=sourceSecret")

Consumerはデータの入力元を定義します。RouteBuilder.fromメソッドの引数にコンポーネント(sftp)とそのパラメータを指定することで、ファイルの取得方法を定義できます。

Processor:ファイルの内容を加工

.process(exchange -> { // Processor: データ内容を加工
                        String body = exchange.getIn().getBody(String.class);
                        body = body.replace("oldValue", "newValue"); // 例: 文字列を置換
                        exchange.getMessage().setBody(body);
                    })

ProcessorはConsumerが取得したデータを加工します。データの内容や、取得したファイル名といった情報を格納するのがExchangeクラスですが、このクラスを引数に受け取るラムダ式として定義します。処理した結果は、再びExchangeに設定することで、続く処理で受け取ることが可能となります。

Producer:ファイルを送信

.to("sftp://[email protected]:22/target?password=targetSecret");

Producerはファイルの出力先を定義します。ProcessorDefinition.toメソッドの引数にコンポーネント(sftp)とそのパラメータを指定することで定義します。

なお、Consumer、Producerはコンポーネントを変更することで、容易にプロトコルの変更が可能です。こうした柔軟性もCamelの大きな特徴と言えます。

適用例

あるシステム内のサーバが、他のシステムにファイルを送信するときは、同一の方式で受け取り、送信先に応じてそれぞれの方式に応じて送信できるようにしました。ポイントは以下となります。

  • HTTPリクエストで送信要求を受ける
  • 代理サーバから送信するファイルを取得する
  • 送信先に応じて方式を変えて送信

これにより、

  • サーバ接続ロジックの集約を実現
  • 接続方式の変更があっても、一か所の修正で完結

とすることができました。さらに、

  • 代理サーバからファイルを取得することで、ログインするための認証情報を代理サーバ側で一元管理
  • HTTPリクエストで送信要求を受け取ることで、送信側はシンプルなHTTPリクエストの送信処理だけで実装が完結

とすることで、サーバ間を疎結合にできました。
以下は送信要求を受信する箇所のコードです。

        from("jetty:http://0.0.0.0:8080/transfer/{routeType}") // Jettyのリスナーアドレスを指定
                .process(
                        exchange -> {
                            String path =
                                    exchange.getIn().getHeader(Exchange.HTTP_PATH, String.class);
                            String[] paths = path.split("/");
                            // CamelHttpPathは"/transfer/hulft"のようになるため、splitすると{"","transfer",hulft"}になる
                            exchange.getIn().setHeader(HeaderKeys.TRANS_ROUTE, paths[1]);
                            exchange.getIn().setHeader(HeaderKeys.ROUTE_TYPE, paths[2]);
                        })
                .toD(
                        "direct:${header."
                                + HeaderKeys.TRANS_ROUTE
                                + "}-${header."
                                + HeaderKeys.ROUTE_TYPE
                                + "}")
                .end();

また、リクエストパラメータとして送信先の情報を受け取り、それぞれのファイル送信用ルートに転送するように定義しました。これにより、ファイル受信用ルート1つに対して複数の送信用ルートを定義でき、コードの重複を避けることを実現しました。
以下は送信処理のコードです。directコンポーネントを使用することで、指定したIDのルートに転送することが出来ます。

        from("direct:transfer-" + this.routeId)
                .process(
                        exchange -> {
                            // SFTPでアクセスするユーザをプロパティから取得
                            String srcHost =
                                    exchange.getIn().getHeader(HeaderKeys.SRC_HOST, String.class);
                            String accessUserKey =
                                    String.format(PropertyKeys.SSH_ACCESS_USER_KEY_FORMAT, srcHost);
                            exchange.getIn()
                                    .setHeader(
                                            HeaderKeys.ACCESS_USER,
                                            exchange.getContext()
                                                    .resolvePropertyPlaceholders(accessUserKey));
                            exchange.getIn().setHeader(HeaderKeys.SEND_DIR, this.sendDir);
                        })
                .process(new SftpRetrieveProcessor()) //ファイルを取得するためのSFTPコマンドを設定
                .to("exec:command?useStderrOnEmptyStdout=true&exitValues=0") //ExecモジュールでSFTP実行
                .to("direct:" + this.sendRouteName) //送信用ルートへ転送
                .end();

まとめ

システム間連携を代理するサーバは、ファイル送受信、HTTPリクエストの送受信等の似たような処理の、微妙な違いを吸収するために構築します。このため処理が重複しがちなのですが、Apache Camelを使うことで重複を省きつつ、異なる箇所だけ実装をすることができました。また、ルートの追加にもアドホックに対応できるため、今後の拡張性にも期待できます。
私たちが扱うシステムは様々なシステムと連携することが多いので、今後は必ずApache Camelを導入するように啓蒙していきたいなと思いました。

ブログの著者欄

羽鳥 樹

GMOペイメントゲートウェイ株式会社

GMOペイメントゲートウェイ所属のプログラマー。クレジットカード決済システムや、カード会社様向けシステムの開発に主に従事。社内向けに技術関連の発信をときどきやっています。

採用情報

関連記事

KEYWORD

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック