2.3. 例外処理
概要
Apache Camel はいくつかの異なるメカニズムを提供しており、異なるレベルの粒度で例外を処理することができます。まず、doTry、doCatch、および doFinally を使用してルート内で例外を処理できます。また、onException を使用して、各例外型に対して実行するアクションを指定し、RouteBuilder 内のすべてのルートにそのルールを適用することもできます。または、errorHandler を使用して、すべての 例外型に対して実行するアクションを指定し、そのルールを RouteBuilder 内のすべてのルートに適用することもできます。
例外処理の詳細は、「Dead Letter Channel」 を参照してください。
2.3.1. onException 句
概要
onException 句は、1 つ以上のルートで発生する例外をトラップするための強力なメカニズムです。これは型固有のもので、異なる例外型を処理するための個別のアクションを定義することができます。基本的にルートと同じ (実際には、若干拡張された) 構文でアクションを定義できるため、例外を処理する方法にかなりの柔軟性が得られます。また、トラップモデルをベースにしていることにより、1つの onException 句で任意のルート内の任意のノードで発生した例外を処理できます。
onException を使用した例外のトラップ
onException 句は、例外をキャッチするのではなく、トラップ するメカニズムです。つまり、一度 onException 句を定義すると、ルート内の任意の地点で発生する例外がトラップされます。これは、特定のコードフラグメントが try ブロックで 明示的 に囲まれている場合にのみ例外がキャッチされる、Java の try/catch メカニズムとは対照的です。
onException 句を定義すると、Apache Camel ランタイムが各ルートノードを暗黙的に try ブロックで囲んでしまいます。このため、onException 句はルートの任意の地点で例外をトラップすることができます。ただし、このラッピングは自動的に行われ、ルート定義には表示されません。
Java DSL の例
以下の Java DSL の例では、onException 句は RouteBuilder クラスで定義されているすべてのルートに適用されます。いずれかのルート (from("seda:inputA") または from("seda:inputB")) の処理中に ValidationException 例外が発生すると、onException 句はその例外をトラップし、現在のエクスチェンジを validationFailed JMS キュー (デッドレターキューとして機能する) にリダイレクトします。
// Java
public class MyRouteBuilder extends RouteBuilder {
public void configure() {
onException(ValidationException.class)
.to("activemq:validationFailed");
from("seda:inputA")
.to("validation:foo/bar.xsd", "activemq:someQueue");
from("seda:inputB").to("direct:foo")
.to("rnc:mySchema.rnc", "activemq:anotherQueue");
}
}XML DSL の例
上記の例は、exception 句を定義する onException 要素を使用して、以下のように XML DSL で表すこともできます。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:camel="http://camel.apache.org/schema/spring"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd">
<camelContext xmlns="http://camel.apache.org/schema/spring">
<onException>
<exception>com.mycompany.ValidationException</exception>
<to uri="activemq:validationFailed"/>
</onException>
<route>
<from uri="seda:inputA"/>
<to uri="validation:foo/bar.xsd"/>
<to uri="activemq:someQueue"/>
</route>
<route>
<from uri="seda:inputB"/>
<to uri="rnc:mySchema.rnc"/>
<to uri="activemq:anotherQueue"/>
</route>
</camelContext>
</beans>複数の例外のトラップ
複数の onException 句を定義して、RouteBuilder スコープ内で例外をトラップすることができます。これにより、例外に応じて異なるアクションを実行できます。たとえば、以下の Java DSL で定義された一連の onException 句は、ValidationException、IOException、および Exception の異なるデッドレター宛先を定義します。
onException(ValidationException.class).to("activemq:validationFailed");
onException(java.io.IOException.class).to("activemq:ioExceptions");
onException(Exception.class).to("activemq:exceptions");
以下のように、XML DSL で同じ一連の onException 句を定義することができます。
<onException>
<exception>com.mycompany.ValidationException</exception>
<to uri="activemq:validationFailed"/>
</onException>
<onException>
<exception>java.io.IOException</exception>
<to uri="activemq:ioExceptions"/>
</onException>
<onException>
<exception>java.lang.Exception</exception>
<to uri="activemq:exceptions"/>
</onException>
また、複数の例外をグループ化して、同じ onException 句でトラップすることもできます。Java DSL では、以下のように複数の例外をグループ化できます。
onException(ValidationException.class, BuesinessException.class)
.to("activemq:validationFailed");
XML DSL では、以下のように onException 要素内に複数の exception 要素を定義することで、複数の例外をグループ化できます。
<onException>
<exception>com.mycompany.ValidationException</exception>
<exception>com.mycompany.BuesinessException</exception>
<to uri="activemq:validationFailed"/>
</onException>
複数の例外をトラップする場合、onException 句の順序は重要です。Apache Camel はまず、発生した例外を最初の句に対して一致しようと試みます。最初の句が一致しない場合、次の onException 句が試行され、一致するものが見つかるまで続きます。各々の一致するかどうかの試行は、以下のアルゴリズムで制御されます。
発生する例外が チェーン例外 (例外がキャッチされて別の例外としてスローされたもの) である場合、最もネストされた例外型が最初に一致の基準となります。この例外は、以下のようにテストされます。
-
テスト対象例外が正確に
onException句で指定された型を持っている場合 (instanceofによってテストされる) は、一致が起こります。 -
テスト対象例外が
onException句で指定された型のサブタイプである場合、一致が起こります。
-
テスト対象例外が正確に
- 最もネストされた例外が一致しなかった場合、チェーンの次の例外 (ラップしている例外) がテストされます。このテストは、一致が起こるかチェーンの最後に到達するまで継続します。
throwException EIP を使用すると、Simple 言語の式から新しい例外インスタンスを生成できます。現在のエクスチェンジから利用可能な情報に基づいて、動的に生成することができます。以下に例を示します。
<throwException exceptionType="java.lang.IllegalArgumentException" message="${body}"/>デッドレターチャネル
これまでの基本的な onException の使用例は、すべて デッドレターチャネル パターンを利用していました。つまり、onException 句が例外をトラップすると、現在のエクスチェンジは特別な宛先 (デッドレターチャネル) にルーティングされます。デッドレターチャネルは、処理されて いない 失敗したメッセージの保持領域として機能します。管理者は後でメッセージを検査し、どのようなアクションを取る必要があるかを決定できます。
チャネルパターンの詳細は、「Dead Letter Channel」 を参照してください。
元のメッセージの使用
ルートの途中で例外が発生した時点では、エクスチェンジ内のメッセージが大幅に変更されている可能性があります (人間には判読できなくなっている場合もあります) 。多くの場合、管理者にとっては、デッドレターキューに表示されるメッセージがルートの開始時に受信したままの 元 のメッセージであれば、どのような修正アクションをとるべきか決定するのが簡単になります。useOriginalMessage オプションはデフォルトでは false に設定されますが、エラーハンドラーに設定されている場合には自動的に有効になります。
useOriginalMessage オプションは、メッセージを複数のエンドポイントに送信する Camel ルートに適用したり、メッセージを分割したりすると、予期せぬ動作をすることがあります。中間処理ステップが元のメッセージを変更する Multicast、Splitter、または RecipientList のルートでは、元のメッセージは保持されない場合があります。
Java DSL では、エクスチェンジのメッセージを元のメッセージで置き換えることができます。setAllowUseOriginalMessage() を true に設定し、以下のように useOriginalMessage() DSL コマンドを使用します。
onException(ValidationException.class)
.useOriginalMessage()
.to("activemq:validationFailed");
XML DSL では、以下のように onException 要素の useOriginalMessage 属性を設定することで、元のメッセージを取得できます。
<onException useOriginalMessage="true">
<exception>com.mycompany.ValidationException</exception>
<to uri="activemq:validationFailed"/>
</onException>
setAllowUseOriginalMessage() オプションが true に設定されている場合、Camel はルートの開始時に元のメッセージのコピーを作成します。これにより、useOriginalMessage() の呼び出し時に元のメッセージが利用できることを保証します。しかし、setAllowUseOriginalMessage() オプションが Camel コンテキストで false (デフォルト) に設定されている場合、元のメッセージにはアクセス できず、useOriginalMessage() を呼び出すことができません。
デフォルトの動作がこうなっている理由は、大きなメッセージを処理する際にパフォーマンスを最適化するためです。
2.18 より前の Camel バージョンでは、allowUseOriginalMessage のデフォルト設定は true です。
再配信ポリシー
例外が発生したらすぐにメッセージの処理を中断して諦める代わりに、Apache Camel では例外が発生した時点でメッセージを 再送 するオプションを利用できます。タイムアウトが発生したり、一時的な障害が発生したりするネットワークシステムでは、元の例外が発生してからすぐに再送することで、失敗したメッセージが正常に処理されることがよくあります。
Apache Camel の再配信は、例外の発生後にメッセージを再送するさまざまなストラテジーをサポートします。再配信を設定する際に最も重要なオプションには、以下のものがあります。
maximumRedeliveries()-
再配信を試行できる最大回数を指定します (デフォルトは
0)。負の値は、再配信がいつまでも試行されることを意味します (無限の値と同等です) 。 retryWhile()Apache Camel が再配信を続行すべきかどうかを決定する述語 (
Predicate型) を指定します。述語が現在のエクスチェンジ上でtrueと評価されると、再配信が試行されます。そうでない場合は再配信が停止され、それ以上の再配信の試みは行われません。このオプションは
maximumRedeliveries()オプションよりも優先されます。
Java DSL では、再配信ポリシーのオプションは、onException 句内の DSL コマンドを使用して指定します。たとえば、以下のように、エクスチェンジが validationFailed デッドレターキューに送信される前に、最大 6 回の再配信を指定できます。
onException(ValidationException.class)
.maximumRedeliveries(6)
.retryAttemptedLogLevel(org.apache.camel.LogginLevel.WARN)
.to("activemq:validationFailed");
XML DSL では、redeliveryPolicy 要素に属性を設定することで再配信ポリシーオプションを指定します。たとえば、上記のルートは以下のように XML DSL で表現できます。
<onException useOriginalMessage="true">
<exception>com.mycompany.ValidationException</exception>
<redeliveryPolicy maximumRedeliveries="6"/>
<to uri="activemq:validationFailed"/>
</onException>再配信オプションが設定された後のルートの後半部分は、最後の再配信の試みが失敗するまで処理されません。すべての再配信オプションの詳細については、「Dead Letter Channel」 を参照してください。
もう1つの方法として、redeliveryPolicyProfile インスタンスで再配信ポリシーオプションを指定することもできます。その後、onException 要素の redeliverPolicyRef 属性を使用して、redeliveryPolicyProfile インスタンスを参照できます。たとえば、上記のルートは以下のように表現できます。
<redeliveryPolicyProfile id="redelivPolicy" maximumRedeliveries="6" retryAttemptedLogLevel="WARN"/>
<onException useOriginalMessage="true" redeliveryPolicyRef="redelivPolicy">
<exception>com.mycompany.ValidationException</exception>
<to uri="activemq:validationFailed"/>
</onException>
複数の onException 句で同じ再配信ポリシーを再利用したい場合は、redeliveryPolicyProfile を使用するアプローチが便利です。
条件付きトラップ
onWhen オプションを指定することで、onException による例外のトラップを条件付きにすることができます。onException 句で onWhen オプションを指定すると、発生した例外が句と一致し、かつ、onWhen 述語が現在のエクスチェンジで true に評価された場合にのみ一致が起こります。
たとえば、以下の Java DSL フラグメントでは、発生する例外が MyUserException に一致し、user ヘッダーが現在のエクスチェンジで null でない場合にのみ、最初の onException 句が実行されます。
// Java
// Here we define onException() to catch MyUserException when
// there is a header[user] on the exchange that is not null
onException(MyUserException.class)
.onWhen(header("user").isNotNull())
.maximumRedeliveries(2)
.to(ERROR_USER_QUEUE);
// Here we define onException to catch MyUserException as a kind
// of fallback when the above did not match.
// Noitce: The order how we have defined these onException is
// important as Camel will resolve in the same order as they
// have been defined
onException(MyUserException.class)
.maximumRedeliveries(2)
.to(ERROR_QUEUE);
上記の onException 句は、以下のように XML DSL で表現できます。
<redeliveryPolicyProfile id="twoRedeliveries" maximumRedeliveries="2"/>
<onException redeliveryPolicyRef="twoRedeliveries">
<exception>com.mycompany.MyUserException</exception>
<onWhen>
<simple>${header.user} != null</simple>
</onWhen>
<to uri="activemq:error_user_queue"/>
</onException>
<onException redeliveryPolicyRef="twoRedeliveries">
<exception>com.mycompany.MyUserException</exception>
<to uri="activemq:error_queue"/>
</onException>例外の処理
デフォルトでは、ルートの途中で例外が発生すると、現在のエクスチェンジの処理が中断され、発生した例外がルート先頭のコンシューマーエンドポイントに伝播されます。onException 句がトリガーされても、発生した例外が伝播される前に onException 句がいくつかの処理を実行することを除き、この動作は基本的に同じです。
しかし、このデフォルトの動作が例外を処理する唯一の方法ではありません。以下のように、onException には例外処理の動作を変更するさまざまなオプションが用意されています。
-
例外再スローの抑制 -
onException句が完了した後に、再スローされた例外を抑制するオプションがあります。つまり、この場合、例外はルート先頭のコンシューマーエンドポイントまで伝播しません。 - 継続的な処理 - 例外が発生した時点からエクスチェンジの通常の処理を再開するオプションがあります。このアプローチでは、暗黙的に例外の再スローも抑制されます。
- レスポンスの送信 - ルート先頭にあるコンシューマーエンドポイントがリプライを期待する (つまり InOut MEP を持つ) 特別なケースでは、例外をコンシューマーエンドポイントに伝播するのではなく、カスタムのフォールトリプライメッセージを作成したい場合があります。
新しい onExceptionOccurred オプションを使用して例外がスローされた直後に、カスタムプロセッサーである Camel Exception Clause および Error Handler get が呼び出されます。
例外再スローの抑制
現在の例外が再スローされ、コンシューマーエンドポイントに伝播されないようにするには、以下のように Java DSL で handled() オプションを true に設定します。
onException(ValidationException.class)
.handled(true)
.to("activemq:validationFailed");
Java DSL では、handled() オプションの引数はブール型、Predicate 型、または Expression 型のいずれかを取ります (非ブール型の式は、それが非 null 値として評価された場合には true と解釈されます)。
以下のように handled 要素を使用して、XML DSL で同じルートを設定して再スローした例外を抑制できます。
<onException>
<exception>com.mycompany.ValidationException</exception>
<handled>
<constant>true</constant>
</handled>
<to uri="activemq:validationFailed"/>
</onException>処理の継続
例外が最初に発生したルート内のポイントから現在のメッセージの処理を続行するには、以下のように Java DSL で continued オプションを true に設定します。
onException(ValidationException.class) .continued(true);
Java DSL では、continued() オプションの引数はブール型、Predicate 型、または Expression 型のいずれかを取ります (非ブール型の式は、それが非 null 値として評価された場合には true と解釈されます)。
以下のように continued 要素を使用して、XML DSL で同じルートを設定できます。
<onException>
<exception>com.mycompany.ValidationException</exception>
<continued>
<constant>true</constant>
</continued>
</onException>レスポンスの送信
ルートを開始するコンシューマーエンドポイントがリプライを期待している場合、単に発生した例外をコンシューマーに伝播するのではなく、カスタムのフォールトリプライメッセージを作成したい場合があります。この場合、2つのステップが必要になります。まず、handled オプションを使用して再スロー例外を抑制し、次にエクスチェンジの Out メッセージスロットにカスタムのフォールトメッセージを設定します。
たとえば、以下の Java DSL フラグメントは、MyFunctionalException 例外が発生するたびに、テキスト文字列 Sorry を含むリプライメッセージを送信する方法を示しています。
// we catch MyFunctionalException and want to mark it as handled (= no failure returned to client)
// but we want to return a fixed text response, so we transform OUT body as Sorry.
onException(MyFunctionalException.class)
.handled(true)
.transform().constant("Sorry");
クライアントにフォールトレスポンスを送信する場合、例外メッセージのテキストをレスポンスに組み込みたいことがよくあります。exceptionMessage() ビルダーメソッドを使用して、現在の例外メッセージのテキストにアクセスできます。たとえば、以下のように MyFunctionalException 例外が発生するたびに、例外メッセージのテキストのみを含むリプライを送信できます。
// we catch MyFunctionalException and want to mark it as handled (= no failure returned to client)
// but we want to return a fixed text response, so we transform OUT body and return the exception message
onException(MyFunctionalException.class)
.handled(true)
.transform(exceptionMessage());
例外メッセージのテキストは、Simple 言語からも exception.message 変数を介してアクセスできます。たとえば、以下のように現在の例外のテキストをリプライメッセージに埋め込むことができます。
// we catch MyFunctionalException and want to mark it as handled (= no failure returned to client)
// but we want to return a fixed text response, so we transform OUT body and return a nice message
// using the simple language where we want insert the exception message
onException(MyFunctionalException.class)
.handled(true)
.transform().simple("Error reported: ${exception.message} - cannot process this message.");
上記の onException 句は、以下のように XML DSL で表現できます。
<onException>
<exception>com.mycompany.MyFunctionalException</exception>
<handled>
<constant>true</constant>
</handled>
<transform>
<simple>Error reported: ${exception.message} - cannot process this message.</simple>
</transform>
</onException>例外処理中に発生した例外
既存の例外の処理中に発生した例外 (つまり、onException 句の処理中に発生した例外) は、特別な方法で処理されます。このような例外は、特別なフォールバック例外ハンドラーによって処理されます。例外は以下のように処理されます。
- 既存の例外ハンドラーはすべて無視され、処理は直ちに失敗します。
- 新しい例外がログに記録されます。
- 新しい例外がエクスチェンジオブジェクトに設定されます。
このシンプルな戦略は、onException 句が無限ループに閉じ込められるような複雑な障害のシナリオを回避します。
スコープ
OnException 句は、以下のスコープのいずれかで有効になります。
RouteBuilder scope:
RouteBuilder.configure()メソッド内で独立した文として定義されたonException句は、そのRouteBuilderインスタンスで定義されたすべてのルートに影響します。一方、これらのonException句は他のRouteBuilderインスタンス内で定義されたルートに対する 影響はありません。onException句は、ルート定義の前に表示する 必要があります。この時点までのすべての例は、
RouteBuilderスコープを使用して定義されます。-
Route スコープ -
onException句をルート内に直接埋め込むこともできます。onException 句は、それらが定義されているルートに のみ 影響します。
Route スコープ
ルート定義内のどこにでも onException 句を埋め込むことができますが、end() DSL コマンドを使用して埋め込んだ onException 句を終了する必要があります。
たとえば、以下のように Java DSL で埋め込み onException 句を定義できます。
// Java
from("direct:start")
.onException(OrderFailedException.class)
.maximumRedeliveries(1)
.handled(true)
.beanRef("orderService", "orderFailed")
.to("mock:error")
.end()
.beanRef("orderService", "handleOrder")
.to("mock:result");
XML DSL では、埋め込み onException 句を以下のように定義できます。
<route errorHandlerRef="deadLetter">
<from uri="direct:start"/>
<onException>
<exception>com.mycompany.OrderFailedException</exception>
<redeliveryPolicy maximumRedeliveries="1"/>
<handled>
<constant>true</constant>
</handled>
<bean ref="orderService" method="orderFailed"/>
<to uri="mock:error"/>
</onException>
<bean ref="orderService" method="handleOrder"/>
<to uri="mock:result"/>
</route>