JBoss Fuseを使い倒す その3:デザインパターン詳細編

はじめに
JBoss Fuseの魅力と使い方を紹介する本連載の締めくくりとして、前回と今回の2回にわたって、エンタープライズ統合パターン(Enterprise Integration Patterns:EIP)を紹介しています。EIPはJBoss Fuseの基盤をなすシステム間統合のデザインパターンです。そのためEIPを理解し、活用できるようになることは、JBoss Fuseを使いこなし、システム間統合のソリューションを効果的に構築する上で非常に重要です。
前回は、EIPの全体像とパターンの一覧を紹介しました。今回は、実際にJBoss Fuse上でEIPを使ってみます。この記事では、Camelで最も頻繁に使われるルーティングのパターンから、代表的な以下の4パターンを取り上げます。
- Content-Based Routerパターン
- Recipient Listパターン
- Aggregatorパターン
- Scatter-Gatherパターン
シームレスに設計と実装をつなぐJBoss Fuse
本題に入る前に、いま一度、JBoss Fuseの特徴を強調しておきたいと思います。
JBoss Fuse(より具体的にはその主要コンポーネントであるApache Camel)の特徴は、なんと言ってもDSL(Domain Specific Language、ドメイン固有言語)によるEIPのサポートです。多くのEIPがCamelルート上にほぼそのままDSLとしてマッピングされるため、たいへん容易にEIPを実装できます。さらに、システム間統合上の抽象度の高い設計判断がDSLで直接的に表現されているため、実装されたCamelルートの理解や保守という観点でも優れています。もちろん、XML DSLで実装されていれば、EIPを組み合わせた複雑なルートであってもJBoss Developer Studio(JBDS)上でグラフィカルにCamelルートの表示・編集が可能となり、その理解や保守の容易性はさらに高まります。
このように、EIPを通して設計と実装とが明示的にマッピングされるJBoss Fuseだからこそ、品質の高いシステム間統合ソリューションを構築できると言えます。例えば、EIPをサポートしないESBミドルウェアを想像してみてください。その上で構築されたソリューションは、設計時に考えた数々のEIPの組み合わせや工夫も、そのプログラミング言語中にクラスやメソッドの名前やコメントとして設計意図を残し、注意深く保守していかない限り、次第に実装と乖離していくことでしょう。さらに、もし残念なことに開発者が詳細なドキュメントを残してくれなかったとしたら、どうでしょう。その独自に実装されたソリューションから、当初のEIPをベースとした詳細な設計判断をくまなく読み取ることは果たしてできるでしょうか。
この特徴を念頭においた上で、これから実際に手を動かしてみましょう。EIPがJBoss Fuse上でどれだけ簡単に実装できるか、JBDSのグラフィカルなエディタを通して高度な設計がいかにシームレスに実装にマッピングされるかを体感してください。
Camel組み込みのEIPサポート
まずは、CamelのルートDSLに直接組み込まれているEIPから始めましょう。
Content-Based Router パターン
| パターン | 概要 | |
|---|---|---|
![]() | Content-Based Router コンテンツベースルータ | メッセージをその内容(コンテンツ)に基づいて、異なる宛先に振り分ける |
メッセージのルーティングを、その中身のデータに基づいて条件分岐するという、いわばプログラミングのif文に相当するようなルーティングの基本構成要素です。少しでも複雑なルーティングをしようとするなら、ほぼ必ず登場するパターンと言えます。これまでの記事でも、すでに何度か登場したのを覚えているでしょう。
Content-Based Routerは、JBDSのCamelルートエディタ上で、PaletteペインのRoutingカテゴリにあるChoice、WhenおよびOtherwiseの各ノードを組み合わせて実装します。
Choiceノードがルート上にContent-Based Routerによる条件分岐があることを表し、WhenとOtherwiseのノードがその中で各条件に基づくそれぞれのルートの分岐を定義します。プログラミング言語のif文と同様に、到着したメッセージに対してまず一番上のWhenノードから順に条件式が評価され、条件式に最初にマッチしたルートへメッセージが流されます。Otherwiseノードはif文のelse句に相当する部分で、メッセージがどの条件式にもマッチしなかった場合に、最終的にこのルートが選択されます。
Whenノードにおける分岐の条件式は、以下のプロパティで設定します。
| プロパティ | 説明 | 例 |
|---|---|---|
| Language | 条件式の記述に用いる式言語 | xpath |
| Expression | 条件式が真だった場合にこの分岐が選ばれる | //type = 'aaa' |
Whenノードの条件式は、メッセージ自身が持つ何らかのコンテンツ(ヘッダまたは本文)に対しての条件式にするのがポイントです。これがContent-Based Routerパターンを正しく理解し、実装する上で重要な点です。メッセージ中に存在しないデータ、例えばデータベースのレコードや環境変数などに基づいてメッセージをルーティングすることも可能です。しかし、その場合はコンテンツベースのルーティングとは言えず、実質的にDynamic Routerなどの別のパターンになることに注意してください。
XML DSLによる定義は、以下のようになります。式言語にXPathを指定した場合の例です。
<from uri="direct:in" />
...
<choice>
<when>
<xpath>//type = 'aaa'</xpath>
...
<to uri="direct:aaa" />
</when>
<when>
<xpath>//type = 'bbb'</xpath>
...
<to uri="direct:bbb" />
</when>
<otherwise>
...
<to uri="direct:invalid" />
</otherwise>
</choice>
Camelがサポートする式言語
ここで、Camelの式言語(Expression Language)について触れておきましょう。
Content-Based Routerと同様、あらゆるEIPで何らかの式(expression)を記述するのに式言語が用いられます。例えば、Content-Based Routerではルーティングで分岐する条件式に用いられていました。他にもMessage Filterパターンではメッセージをフィルタリングする際の条件式、次に取り上げるRecipient Listパターンでは宛先リストの情報がどこに格納されているかを示す式などで使われます。
CamelのDSLがサポートしている式言語は、以下の通りです。
| 言語 | 説明 | 例 |
|---|---|---|
| Constant | 定数 | |
| EL | JSPとJSFで使われる統一式言語(JSR-245) | |
| Groovy | スクリプト言語Groovyによる式 | |
| Header | メッセージヘッダの参照 | |
| JavaScript | スクリプト言語JavaScriptによる式 | |
| JXPath | インメモリJavaオブジェクトへのXPathクエリ | |
| MVEL | 式言語MVEL | |
| OGNL | 式言語OGNL | |
| PHP | スクリプト言語PHPによる式 | |
| Property | Exchangeプロパティの参照 | |
| Python | スクリプト言語Pythonによる式 | |
| Ref | レジストリの参照 | myExpression |
| Ruby | スクリプト言語Rubyによる式 | $request.headers['type'] == 'aaa' |
| Simple | Camelの式言語 | |
| SpEL | Spring Frameworkの式言語 | |
| SQL | インメモリJavaオブジェクトへのSQLクエリ | |
| XPath | XPath言語 | |
| XQuery | XQueryクエリ言語 |
詳細は、以下のドキュメントを参照してください。
Apache Camel Development Guide - Part II. Routing Expression and Predicate Languages
Recipient List パターン
| パターン | 概要 | |
|---|---|---|
![]() | Recipient List 受信者リスト | メッセージを複数の宛先に一斉に送信する。宛先のリストは動的に変更できる |
Content-Based Routerは、いくつかあるルートの選択肢のうち1つを、メッセージのコンテンツに基づいて選択的に振り分けるパターンでした。しかし、複数あるルートのうちどれか1つではなく、全てのルートにメッセージを送りたい場合にはどうすればいいでしょうか。
EIPでは、このようないわゆるメッセージのブロードキャストを実現するのに2つのアプローチがあります。1つはメッセージチャネルを活用するアプローチで、Publish-Subscribe Channelパターンを使って実現します。具体的には、JMSのトピックなどを使って複数のコンシューマにメッセージを配信します。そしてもう1つがルーティングを活用するアプローチで、Recipient Listパターンを使って実現します。
当然ながら2つのアプローチにはそれぞれ長所・短所がありますが、ここでは触れません。詳細が気になる方は、ぜひ書籍のEIPを読んでみてください。
Recipient Listは、PaletteペインのRoutingカテゴリにあるRecipientListノードを使って実装します。
Recipient Listのアイデアは、メッセージ中のどこかに宛先を指定した受信者リストを埋め込んでおき、そのリストに基づいてRecipientListノードに全宛先にメッセージをコピーして配信させるというものです。Publish-Subscribe Channelを用いた場合のアプローチとの大きな違いは、受信者リストに指定される可能性のあるエンドポイントを、あらかじめ全て定義しておく必要がある点です。
RecipientListノードでは、受信者リストを読み込む先を以下のプロパティで設定します。
| プロパティ | 説明 | 例 |
|---|---|---|
| Language | 式の記述に用いる言語 | header |
| Expression | 受信者リストを指定する式 | addresses |
| Delimiter | (オプション)受信者リストの区切り文字を指定する。デフォルトはカンマ(,) | |
| ; |
XML DSLによる定義は、以下のようになります。ここでは、式言語にヘッダを指定した場合の例を示します。
<from uri="direct:in" /> ... <recipientList> <header>addresses</header> </recipientList>
受信者リストの値は、以下のいずれかのデータ型で表現できます。
- カンマ区切りの文字列
- 配列
- java.util.Collection
- java.util.Iterator
- org.w3c.dom.NodeList
例えば文字列で受信者リストを表現すると、以下のようになります。
"direct:aaa, direct:bbb, direct:ccc"
Aggregator パターン
| パターン | 概要 | |
|---|---|---|
| Aggregator 集約器 | 複数のメッセージを1つに集約する |
1つのメッセージを複数の宛先へブロードキャストするのがRecipient Listパターンでしたが、今度は、その逆に複数のメッセージを1つに集約するパターンを考えてみましょう。それがAggregatorパターンです。
「複数のメッセージを1つに集約する」とだけ聞くと一見簡単そうですが、 Aggregatorはなかなか奥の深いパターンです。なぜならAggregatorは、他の多くのルーティングパターンと異なり、「ステートフル」なパターンだからです。例えばA、B、Cという3つのメッセージを集約するAggregatorを考えたとき、そのコンポーネントがそれらを1つのメッセージに集約するには、「A」「A、B」「B、C」など、A、B、Cのうちいくつまで集められているかという状態を常に保持しないといけないからです。
またパターンがステートフルであるということは、クラスタリングとの親和性が低いことを意味します。他の「ステートレス」なルーティングパターンの場合、コンポーネントが状態を持たないので、クラスタノードにそのまま分散させることで、容易にスケールアウトできます。しかしステートフルなパターンの場合は、正しい動作を得るためにはコンポーネントをクラスタ内にシングルトンサービスとしてデプロイする必要があり、そこが常にボトルネックとなります。ステートフルなパターンは、Aggregatorの他にもResequencerなどがあります。
Aggregatorは、PaletteペインのRoutingカテゴリにあるAggregateノードを使って実装します。
Aggregatorパターンの奥深いポイントのもう1つは、メッセージをどうやって集約するかの設定にあります。一口にメッセージを集約すると言っても、それを実際にシステムに実装しようとすると、どのメッセージを1つのグループにまとめるのか、集約に必要なメッセージが集まったかどうかをどのように判定するのか、そして集まったメッセージをどうやって1つにまとめるのか、最低でもこの3点を考える必要があります。
つまり、Aggregatorパターンを実装するには、以下の3つのポリシーを設計する必要があります。
- 集約するメッセージの相関性(Correlation)
- メッセージ集約の完了条件(Completeness Condition)
- 複数のメッセージをどのように1つに集約するかの戦略(Aggregation Algorithm)
CamelのAggregateノードでは、主に以下のプロパティを設定することで、この3つのポリシーを実装します。
| プロパティ | 説明 |
|---|---|
| Correlation Expression | メッセージの相関を指定する式。式の値が同じメッセージが1つに集約される |
| Completion Size Expression | 集約の完了条件。一定数のメッセージが集まったら集約する |
| Completion Timeout Expression | 集約の完了条件。一定の待機時間が過ぎたらメッセージを集約する |
| Completion Interval | 集約の完了条件。一定時間間隔で強制的にメッセージを集約する(Completion Timeoutと同時には使えない) |
| Completion Predicate | 集約の完了条件。条件式がtrueを返す場合にメッセージを集約する |
| Strategy Ref | メッセージの集約戦略。AggregationStrategyインターフェース実装への参照。 |
メッセージ集約の完了条件には複数のプロパティが用意されていますが、Completion TimeoutとCompletion Intervalが併用不可である以外は、複数のプロパティを同時に指定可能です。複数条件を指定した場合、どれか1つでも条件を満たせば集約が完了したことになります。
Strategy Refプロパティには、集約戦略オブジェクトへの参照を指定するだけですが、この集約戦略オブジェクト自体はJavaクラスとして自分自身で実装しなくてはいけません。
XML DSLによる定義は、以下のようになります。
<from uri="direct:in" />
...
<aggregate strategyRef="aggregationStrategy">
<correlationExpression>
<header>aggregateId</header>
</correlationExpression>
<completionPredicate>
<simple>${header.forceComplete} == 'true'</simple>
</completionPredicate>
<completionTimeout>
<constant>1000</constant>
</completionTimeout>
<completionSize>
<header>totalSize</header>
</completionSize>
...
<to uri="direct:out" />
</aggregate>
...
<bean id="aggregationStrategy"
class="com.redhat.examples.fuse.eip.ListAggregationStrategy" />
![[fig-a]](/sites/default/files/fig-a_0.gif)


![[fig-b]](/sites/default/files/fig-b_0.gif)




