はじめに

Apache Camelの開発者であるClaus Ibsen氏は、Camelのことを「ツールボックス」と表現している。Camelは、プログラマの能力を一段と引き上げる洗練されたツールで、すべてのJavaプログラマが学ぶ価値のあるものだ。

いままでCamelは、大規模なシステムのメッセージングを前提としたシステム間統合のためのミドルウェアと思われがちだった。こうしたこともあってか、どうやら日本での導入はいくつかの例外を除いて一部の大企業が中心のようだ。一方、海外ではより広範囲への導入が確実に進んでいる。実はCamelはどのようなアプリケーションでも利用できる、とても汎用的で拡張可能なライブラリでもあるのだ。

Camelが提供する主な機能は、アダプタ、データ変換、ルーティング、エラーや障害時のハンドリングといったものだ。そして、これらは実際のコーディングの多くをしめ、複雑化しやすい部分でもある。初めてCamelで書かれたコードをみたら、シンプルで無駄がないことに驚くだろう。Camelでのプログラミングはコーディングであり、また設定であり、そしてそれらの調和によってプログラマの意図を直接反映させることができるものだ。

クラウド、マイクロサービス、イベントソーシング、リアクティブプログラミング、API管理、モバイル、IoTといった技術の普及により、分散したマシン間でイベントを協調させるシステムが多くなってきた。こうしたことが海外における広範囲なCamelの導入の背景となっている。私の所属しているRed Hatでもでもそうした技術を用いた各種のリファレンスアーキテクチャ[1]を提案しているが、これらの実装にはCamelを利用することが前提となっている。こうした技術の利用においてCamelは必要不可欠な薄いインフラとして確固たる地位を築き始めている。

今回はCamelの詳細には触れない。その代わりに、Camelは何ができ、そしてなぜ学ぶ必要があるのかを説明する。

Camelとは何か?

Apache Camel (http://camel.apache.org) は様々なシステムを相互に接続することができる、オープンソースのライトウェイトなライブラリだ。例えば、REST/SOAP、ファイル、データベース(JDBCやJPAなど)、各種メッセージキュー、メール、MQTT、そして当然Javaプログラムなど、様々なものと相互に接続して連携させることができる。

Camelには様々な機能があるが基本的な動きはシンプルだ。Camelでの処理は、データを自身で取りに行ったり、誰かから受け取ることで開始される。例えば、CSVファイルを読み取ったり、HTTPでJSONによるリクエストを受け取ったりした場合だ。そして、受け取ったデータに追加の情報を付加したり、変換したりして、次の接続先へとデータを送る。接続元と先は別の技術であってももちろん接続できる。例えばHTTPで受信したデータをデータベースやExcelファイルに出力したりできる。あるいはメッセージキューなどに送ることで、さらに別のサービスへと連鎖的に連携させていくこともある。

いままでCamelは、大規模なシステムのメッセージングを前提としたシステム間統合のためのミドルウェアと思われがちだった。だが実はCamelは普段のプログラミングの多くの側面に対応し、身近な問題を解決する、とても汎用的で拡張可能なライブラリでもあるのだ。それを理解するために、まずはCamelの基本的な機能を確認していこう。

Camelの基本的な機能

Camelが提供する基本的な機能は、アダプタ、型変換、データ変換、ルーティングといったものだ。 ※より高度な機能については別途後述する。

様々なものとの相互接続を可能にするアダプタ

Camelは、REST/SOAP、ファイル、データベース(JDBCやJPAなど)、各種メッセージキュー、メール、MQTT、そして当然Javaプログラムなど、様々なものと相互に接続して連携させることができる。それらを使用するために個別ライブラリを直接扱う必要はなく、そしてその詳細を知る必要もない。後述するように接続エンドポイントに対してURIによる設定を行うだけで利用できるようになる。いまではコミュニティのものを含めると200以上ものコンポーネントが存在する。

型変換

Camelは接続先に応じて自動的にデータ内容の型変換を行う。接続先ごとに好みのデータ型は異なり、例えばファイルならストリーム、データベースはResultSet、JMSならTextMessageなど数え上げればキリがない。普段のプログラミングでもこれらを連携させる場合には、地道で厄介な型変換のコーディングを行っているはずだ。しかしCamelを使えば必要な型変換を自動的に行ってくれる。そのためにCamelは内部的に型変換のための拡張可能な型変換ルールのリポジトリを持っている。

データ変換/フォーマット変換

Camelはデータの種類を選ばない。JSON、XML、CSV、Javaオブジェクトなど、そのままCamel内で扱える。そしてその内容を書き換えたり相互に変換できる。変換にはXPATHやXStreamやJAXBなど様々な方法が可能だ。また各種テンプレートエンジンを利用してメールなどへの定型的な文面を作成することもできる。そしてもちろん直接自分でJavaを利用してコーディングしてしまうことも可能だ。こうしたデータ変換処理にあまり馴染みがないと感じる方がいるかもしれないが、DBから取り出したデータをJSPによるテンプレートよってHTMLに変換する処理なども立派なデータ変換処理といえる。普段のプログラミングでよく行われている作業の1つだ。

ルーティング

受け取ったデータは、分岐やフィルタリングさせたり、分割して並列処理させたり、それらを集約したりすることができる。Camelでは普段コーディングしているif文のようにこれらを指定できる。

これらは普段のプログラミングで行っていること

これらの機能を振り返ってみれば、実はCamelは普段のプログラミングの多くの側面に対応していることがわかる。これは普段のプログラミングにも相互接続のためのインテグレーションの要素が数多く含まれているからだ。このようにCamelは私達の身近な問題を解決している。Camelは簡単な処理から複雑な問題まで対応することができ、誤解を恐れずに言えば、Camelはあらゆるアプリケーションで利用可能だとさえいえる。特にJEEアプリケーションサーバやフレームワークなどによる包括的なプログラミングへのサポートが期待できなかったり、それでは不足している機能があればCamelの利用を検討するべきだ。

ダイレクトに意図を表現できるCamel

Camelの強力さを実感するために、早速実際のコード例を見てみよう。この例はJSONを受け付け、次のような処理を行うRESTサービスを公開する場合のサンプルコードだ。

  • ユーザ登録用のJSONリクエストをRESTサービスで受けつける
  • DBにユーザデータを登録する
  • テンプレートエンジンでメールの文面を作成して、ユーザにメールを送信する(別スレッドを用いて非同期で処理)
  • ユーザの追加を他システムに通知するためのイベント(XML)をキューに登録する
  • エラーハンドリング
    • 10秒以内に終了しなかったら、タイムアウトエラーにする
    • 何らかのエラーが15秒以内に2回以上発生したら本サービスを120秒間停止(サーキットブレーカーをOpen)する
    • サービスがクローズ状態の間は、リクエストに対して即座にエラー応答を返す
// usersのパスにpostが送信されたら処理を開始
// 次のようなリクエストを想定 {name:"username_1", email:"xxxx@redhat.com"}
rest("/users")
  .post()
    .route()
        .hystrix()
          // サーキットブレーカーの設定
          // 10秒以内に処理が終了しなかったらタイムアウトエラーにする
          // 何らかのエラーが15秒以内に2回以上発生したら本サービスを120秒間停止(サーキットブレーカーをOpen)する
          .hystrixConfiguration()
            .executionTimeoutInMilliseconds(10000)
            .metricsRollingPercentileWindowInMilliseconds(15000)
            .circuitBreakerRequestVolumeThreshold(2)
            .circuitBreakerSleepWindowInMilliseconds(120000)
          .end()
          // JSONをMapオブジェクトに変換
          .unmarshal().json(JsonLibrary.Jackson, Map.class)
          // DBにUserデータを保存 
          .to("sql:insert into users(name, email) values(:#name, :#email)")
          // [サブルート呼び出し(別スレッドを用いて非同期で処理)] ユーザへメールを送信 
          .wireTap("direct:userCreatedNotificationMail").end()
          // ユーザの追加を他システムに通知するためのイベント(XML)をキューに登録する
          .transform(new Map2XML("user-created")).to("jms:UserCreated")
          // 成功を応答する
          .transform().constant("{result:\"true\"}")
        // エラーやサービスがクローズされている場合には、失敗を応答する
        .onFallback()
          .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(503))
          .transform().constant("{result:\"false\"}")
        .end();

// [サブルート] メールの文面をテンプレートを用いて作成し、ユーザへメールを送信
from("direct:userCreatedNotificationMail")
  .setHeader("to", simple("${body[email]}"))
  .to("velocity:template/velocity/createdUserNotification.vm")
  .to("smtp://smtpserver?password=&username=");
  

流れるようにプログラミングが行われており、コーディングとも設定ともつかない。ここでその詳細は説明しないが、それでも何が行われるのかは想像できると思う。これはやりたいことがダイレクトに表現できているということだ。

これだけのことを各種の個別ライブラリを直接扱いながら実装するのは大変な労力が必要だし、そもそも同じだけのことを実装できるプログラマはプロジェクト内に何人もいないだろう。Camelなら接続先に応じた個別の実装や、データ型の調整、高度なエラーハンドリングなどの様々な詳細を隠蔽して、設定を中心とした宣言的なプログラミングを行うことができる。

特に、マイクロサービスのようなダイナミックに成長していくシステムにおいては、このように使い捨てできるレベルの高い生産性でプログラミングできなければ、勝負の土俵にすら上がれない。

Camelのアーキテクチャ(簡易版)

Camelには高度な機能がたくさんあるが、それら機能をよりよく理解するためには、いくらかCamelのアーキテクチャについて知らなけばならない。ここでは簡単ながら説明する。

// Consumer
from("jetty:http://localhost:8080/orders?httpMethodRestrict=POST") 
    .process().body(this::process1)) // Processor 1
    .process().body(this::process2)) // Processor 2
    .to("jms:queue:order?timeToLive=86400000"); // Producer

Camelの汎用性は、シンプルだが強力な構造によって支えられている。データを運ぶ袋としての「メッセージとエクスチェンジ」、メッセージの加工やルーティングを行う「プロセッサ」、外側とをつなぐアダプタとしての「エンドポイント」、そしてエンドポイントとプロセッサを組み合わせた一連のフローである「ルート」だ。上の図はそれを模式化したのもので、そしてサンプルコードはCamelで記述した場合の例だ。Camelのアプリケーションは、このルートを単位として組み合わせていくことで、全体としてサービスやシステムを形作っていく。この構造は、システム間統合の叡智を集約したEnterprise Integration Patterns(EIP)[2]に基いている。

エンドポイントは、REST/SOAP、ファイル、データベース(JDBCやJPAなど)、各種メッセージキューといった接続対象とルートを結びつけるためのアダプタだ。なお本来EIPはメッセージングを対象としたパターンであり、エンドポイントをメッセージブローカーとの接点としてのみ扱っていた。その考えをCamelでは拡張して、ファイルやREST/SOAPといったあらゆる接続対象をエンドポイントとして取り扱い、またデータをすべてメッセージとして扱えるようにしている。これによりCamelは、メッセージングに限らず、あらゆる接続先とデータを統一的に取り扱いながら相互に接続させることを実現している。

さらにエンドポイント内では、メッセージのデータに対して接続先に応じた型変換が自動的に行われ、プログラマが行うべき地味な作業の多くを肩代わりしてくれる。この自動的な型変換を実現するためにCamelは内部に拡張可能な型変換ルールのリポジトリを保持している。

また、エンドポイントは単にデータをメッセージに変換するだけでなく、接続先に応じた高度な機能を提供する。例えばファイル用のエンドポイントなら、タイマーを使って定期的にファイルの存在チェックを行いながら、新しいファイルだけをルートに転送するといった具合だ。エンドポイントへの設定はURI形式になっており、こうした設定を与えれば、本来は接続対象ごとに異なるはずのAPIやその詳細をうまく隠蔽しながら、接続先特有の機能やそれを簡便に利用するための機能を提供してくれる。

なお、エンドポイントは、データを受け取る「コンシューマ」と、データを送信する「プロデューサ」として区別されることもある。

プロセッサは、型変換やデータ変換といったメッセージの加工や、バリデーション、メッセージの送信先を決定するルーティングを行う。変換にはXPATHやXStreamやJAXBそしてラムダ式など様々な方法が可能で、ルーティングもCamelの提供する機能によりまるでif文を書くように簡単に行える。

ルート内を流れるメッセージは、ヘッダーとボディーから構成されており、本体となるデータはこのボディーに含まれている。ヘッダーはメッセージに関連した任意の情報を、名前と値のペアで保持できる。実は本体であるボティーとは別に、こうしたメタデータを付加できるヘッダーが存在するととても便利だ。例えば、ファイル読み込み用のエンドポイントから送られてきたメッセージには、自動的にファイルパスやファイル名がヘッダーに挿入されおり、後から自由に利用することができる。

実はCamelのルート内は、正確にはメッセージそのものではなく、入力用と出力用のメッセージを保持したエクスチェンジというデータ構造が流れている。CamelはHTTP通信などの応答が必要な要求を受け付けるので、入力用と、応答に向けた出力用のメッセージをまとめて保持できると都合が良いのだ。またエクスチェンジには、メッセージにおけるヘッダーと似た、プロパティを持っており、ルーティングのために必要な任意のデータを保持できる。これはルート内におけるローカル変数のようなものと思っていい。

EIPに基づくCamalは、メッセージやエクスチェンジに必要な情報を詰め込むことによって、ランタイム側にはステートを持たないよう緻密な設計がなされている。これはステートを持たなければどのCamelランタイムからでも処理することができ、これが簡潔なシステム構成やシステム設計上の柔軟性、そしてスケーラビリティへとつながっていくからだ。とはいえステートを持たなければ実現できない機能(アグリゲータパターンや冪等性チェックなど)もあるため、EIPを参考にしながらバランスの良い設計判断をくださなければならないときもある。ところでメッセージやエクスチェンジのような付加的な入れ物自体のオーバーヘッドを気にするかもしれないが、これは軽量なオブジェクトなので十分に無視できる。

このようにCamelはEIPに基づいた普遍的な構造によって、多くのアプリケーションのニーズに対応できる強力な柔軟性を持っている。また新しい技術が誕生しても、それに対応したエンドポイントを提供することで、この普遍的な構造の中でもCamelは常に進化を続けている。

Camelの高度な機能

簡単にCamelのアーキテクチャを見たところで、高度なCamelの機能を説明する。これらの機能のすべてを理解するのは尻込みしてしまうが、必要な機能から少しづつ利用して学んでいけばいい。実際のところコンポーネントが多すぎて、すべての機能を把握できる人はいない。すべての機能を知らなくてもCamelを利用できるし、十分に恩恵を受けることができる。

高度なルーティング

受け取ったメッセージは、分岐やフィルタリングしたり、分割して並列処理させたり、それらを集約したりすることができる。またHTTP通信のような同期処理から、その応答を待たずに処理を続ける非同期処理へと切り替えることも簡単だ。さらには単体のマシン内だけでなく、マシンをまたぎ複数のキューを経由させながら、応答メッセージを受け取るようなダイナミックで高度なルーティングも実現できる。やや特殊な用途としてはラウンドロビンで宛先を切り替えて簡易なロードバランサーを実現したり、一度に流れるデータの流量を調整することも可能だ。

エラーや障害時のハンドリング

ローカルマシンを考えただけでもメモリやディスクなど様々なエラーが考えられるが、ネットワークを超えて分散する様々なサービスを統合するなら、ネットワーク不調や無応答サービスなどの、より複雑な状況を含めてハンドリングできなければならない。Camelは一時的なエラー時のリトライはもちろんのこと、中長期にわたる障害がシステム全体に波及していくことを防ぐための、サーキットブレーカーやバルクヘッドといったより高度なハンドリングも簡単な設定で利用できる。これは近年人気の高いHystrix[3]で実現されている。また、エラーの滞留を防ぐために専用のキュー(デッドレターキュー)にメッセージを移動させる機能もある。これらの機能はクラウドやマイクロサービスといった分散したシステムで信頼性や可用性を維持するためには欠かせない。

トランザクション

Camelはトランザクションマネージャと連携してトランザクションを提供できる。連携先のリソースがXAに対応していれば複数リソースを横断したトランザクションを実現することも可能だ。さらにファイルなどのトランザクションが提供されないリソースに対しては、エラー発生時に実行すべき補償トランザクションを定義することもできる。

トランザクションに関連して、冪等性(Idempotency)をサポートするIdempotent Consumer機能もある。例えばKafkaやAWS Kinesisなどのイベントソーシングに特化したメッセージブローカーなどでは同じデータが複数回送られることがあるが、この機能で補完すれば、同じデータが複数回送信されたことを検出して、ただ1回のみの処理を実現することができる。

ノンブロッキング対応

Camelはノンブロッキングによる非同期処理に対応する。例えば背後のWEBサービスの処理に時間がかかる場合、その応答までスレッドをブロックながら待ち続ける(C10K問題)のは無駄なことだ。Camelはこうしたスレッドを有効に再利用することで高いスケーラビリティを発揮する。エンドポイントのコンポーネントが対応しているかなどのいくつかの条件が揃えば、Camelのエンジンは自動的に非同期処理を選択するため、何もせずともメリットだけを享受できる。

セキュリティ

Camelは、データそれ自身を暗号化させるためのコンポーネントを提供し、また多くのエンドポイントのコンポーネントが経路上の暗号化に対応している。また特定のルートに対して、ロールによる認証を要求することもできるし、パスワードなどの機密性の高い設定の暗号化も可能だ。

管理/監視

CamelはJMXを通して、各種の統計情報やCamelやそのアプリケーションのライフサイクルを管理するための機能を提供している。またJMXはREST APIを用いたアクセスも可能だ。また管理用のWebアプリ(Hawtio)も用意されており、それらの情報やライフサイクル管理のための機能をブラウザから利用できる。

またREST APIに関しては、Swaggerを組み込むことが可能だ。常に最新のAPIドキュメントを維持し、またAPIに対する簡易なテストを行うこともできる。

さらにCamel内部の各種イベントを購読できる仕組みも用意され、それこそCamelの持つ連携のための機能を組み合わせれば、監視専用サーバへの連携も簡単に実現できる。

開発ツールの提供

JBossからEclipseベースのグラフィカルエディタが提供されている。これを利用すればグラフィカルにデータマッピングが行え、タイプセーフエンドポイントエディタによってエンドポイントのURIの記述をガイドしてくれる。また、デバッガとの統合も行われている。

なお、Webアプリ(Hawtio)からもライブトレースやデバッグが行える。

Camelの特徴

Camelの持つ機能を一通り理解したところで、その特徴について説明していく。

軽量なコアと拡張可能なコンポーネントライブラリ

Camelは大規模なシステム間統合用のミドルウェアと思われがちだが、実際には約5M程度のライトウェイトなライブラリだ。コアに対して、様々なコンポーネントを追加することで拡張することができるプラガブルなアーキテクチャになっており、必要なコンポーネントを選択して利用することができる。今現在200を超えるコンポーネントが存在しており、豊富なドキュメントも存在する。

通常のJavaプログラムへの組み込みが容易

今までも見てきたように、Camelでは設定を中心とした宣言的なプログラミングを行うことができ、これで完結させることもできるが、通常のJavaプログラムと連携させることも容易だ。このため必要な部分でだけCamelを利用することができる。連携するJavaのコードはPOJOやラムダ式を利用でき、CamelのAPIに依存させないままにコーディングが可能だ。また、RxJavaなどのReactive Streams APIを使って相互に連携することもできる。

様々な表現でルートを定義できる

今回紹介したのは、Javaコードの中にルートを定義する、JavaDSLを利用したものだ。その他にも、XML DSLやScala DSLやGroovy DSLにも対応している。

様々な環境で動作する

Camel自体はライブラリであるので様々な環境で動作する。各種のWEBサーバ、アプリケーションサーバ、KarafなどのOSGi環境でも動作させられるし、Camelをスタンドアロンで動作させることも可能だ。

EIPを実現したCamel

これまで何度か触れているようにCamelはEIPに基づきそのパターンを実現したものだ。しかし残念ながらEIPは邦訳されておらず、ページ数も多いため、忙しいエンジニアはなかなか学習する機会が得られないと思う。Camelを使えば、自然とEIPに基づくアーキテクチャにそった構成でプログラミングを行うことになり、明に暗にEIPから多くの恩恵を受け、そしてその知識も少しづつ身につけることができる。初めのうちはCamelを使い、必要に応じてEIPを学んでいくというスタンスで良いのではないかと思う。EIP自体の知識がなくても多くのCamelを機能を利用して恩恵をうけることができる。もちろんEIPにはシステム間統合の設計する上でとても有用な情報が詰め込まれているので時間があれば勉強したいところだ。

テストキットの充実

Camelはテストのしやすさが考慮されている。エンドポイントは各種のアサーション機能を持ったモックに入れ替えることができる。またメッセージのインターセプトやルートの一部置換も可能だ。これらを使えばあらゆる箇所を様々な状況でテストできる。またCitrusやArquillianなどのシステムテストを対象としたツールと組み合わせることもできる。

[PR] 日本でのCamelコミュニティ ~ Camel コミュニティ関係者より ~

Camelの人気は、海外と比べると日本では残念ながら今ひとつという感触です。これにはEIPの学習ハードルの高さや、いままでファイル連携や共有データベースによって連携を行ってきた実績に対する強い信頼感があるのかもしれません。しかし、日本でもクラウドやマイクロサービスの採用が進み、またファイル連携や共有データベースも、ActiveMQやKafkaなどによるメッセージブローカーへ置き換わってきています。Camelの出番です。

この度、少しでもCamelのよさを知ってもらうために、Red Hat社の有志社員を中心として、Camelのユーザグループを作成する運びとなりました。(Red Hat社としての活動ではありません。参加者全員の自主的な活動により成り立っています。)ゆくゆくは日本でのCamelのオープンソースコミュニティを成熟させて、利用者を増やすことはもちろんのこと、機能追加の提案やプルリクエストなどの貢献が行なえるようなメンバーがでてくることも目指しています。Camelはコーディング規約やドキュメントも用意されているため開発に参加するハードルも低く、今後も新技術に対応したコンポーネント作りが求められていくはずです。実際にコンポーネント作りを通してCamelのコミッターになった方もいます。さらに幸運なことにCamelの開発に携わる日本人技術者さえいます。日本におけるCamelコミュニティの場から、お互いの貢献を通して、そしてお互いに還元できればと思っています。

Camelに興味を持った方は、「Camal In Action」の読書会を定期的に実施しておりますので、是非ご参加ください。 -https://jcug-oss.github.io/

まとめ

Camelが提供する機能や特徴を見てきた。現在の新技術にあわせてApache Camelは今も進化が続いている。特にクラウド、マイクロサービスといった技術を用いたシステム開発の浸透につれ、これらに必須の様々な機能を提供しているCamelは、必要不可欠な薄いインフラとして確固たる地位を築き始めている。

Camelは普段のプログラミングの多くの側面に対応し、簡単な処理から複雑な問題にまで対応した、とても汎用的で拡張可能なライブラリだ。これはEIPに基づいたシンプルで強力な構造に支えられている。Camelでのプログラミングは驚くほどにシンプルで無駄がない。Camelでのプログラミングはコーディングであり、また設定であり、そしてそれらの調和によってプログラマの意図を直接反映させることができるものだ。

Apache Camelの開発者であるClaus Ibsen氏は、Camelのことを「ツールボックス」と表現しているが、この意図するところがいくらかでも伝わったなら幸いだ。Camelは、プログラマの能力を一段と引き上げる洗練されたツールで、すべてのJavaプログラマが学ぶ価値がある。