李です。
クラウドサービスとの連携に限らず、通信が発生するシステムにおいては常に通信周りの例外処理が課題でしたが、これまでシステム連携は多くの場合自社のデータセンターにある別サーバーや、VPNによって結ばれた連携先システムなど、"信頼できる"システムが殆どでした。
しかしSaaSなどの登場によって、遠隔地にある外国のサービスとの連携など今までと比べて複雑で、変化が早い通信経路を辿って、直接のやり取りが難しい相手のシステムと頻繁にデータの交換を行うパターンが増えました。
"このサービスは夜間不安定でリトライ回数を調整したい" "このサービスは時々セッションが切れるので、この条件下では自動で再ログインがしたい"などの通信周りの複雑な要件は、従来の設計ではなかなか対応し辛いものです。
さて、VMforceの発表やGoogleとVMwareとの連携の発表などで、それまであまりパッとしないSpring Rooが注目されるようになりました。
Spring Rooというプロダクト自体の紹介は詳しい方に任せるとして、このプロダクトの特徴はなんといっても変更を自動で検知しコード生成をサポートするエンジンと、それによって生成されるAspectJコードによるAOP(Aspect-Oriented Programming)の実装です。
これによってAOPが再評価される意見も多いので、今回はそれに便乗して冒頭で述べた問題をAOPで解決する簡単な例をお見せしたいと思います。
例えば下記のような、リモートサービスと通信し、ひたすらレスポンスを表示するだけのプログラムがあります:
約10回に1回は例外になる、悪いヤツです。
では通信モジュールに例外処理のコードを書くか、例外処理用のラッパーを作るにしても、今度はそれを呼ぶ側の事情によっては都合が悪かったりと柔軟性に欠けます。
そこで、AOPの出番です。(ここではAspectJのコードを使いますが、とてもこのブログに収まる話ではないので詳しい説明は省きます。)
アノテーションが使える最近のAspectJなら、下記のようなコードを入れるだけで、CloudServiceConnect内にあるすべてのget..という名前と一致するメソッドに例外処理を編みこんでくれます。
その場合、下記のコードを追加するだけで対応できます。(対応を分けるということで、上記のexceptionRecoveryAdviceのexecutionをget*ServiceAに変更する必要があります)
命名規則などを固めてある前提ですが、このパターンを適用すればソースコードを汚さずに、確実な例外処理を簡単に実装できることが伝われば幸いです。
クラウドサービスとの連携に限らず、通信が発生するシステムにおいては常に通信周りの例外処理が課題でしたが、これまでシステム連携は多くの場合自社のデータセンターにある別サーバーや、VPNによって結ばれた連携先システムなど、"信頼できる"システムが殆どでした。
しかしSaaSなどの登場によって、遠隔地にある外国のサービスとの連携など今までと比べて複雑で、変化が早い通信経路を辿って、直接のやり取りが難しい相手のシステムと頻繁にデータの交換を行うパターンが増えました。
"このサービスは夜間不安定でリトライ回数を調整したい" "このサービスは時々セッションが切れるので、この条件下では自動で再ログインがしたい"などの通信周りの複雑な要件は、従来の設計ではなかなか対応し辛いものです。
さて、VMforceの発表やGoogleとVMwareとの連携の発表などで、それまであまりパッとしないSpring Rooが注目されるようになりました。
Spring Rooというプロダクト自体の紹介は詳しい方に任せるとして、このプロダクトの特徴はなんといっても変更を自動で検知しコード生成をサポートするエンジンと、それによって生成されるAspectJコードによるAOP(Aspect-Oriented Programming)の実装です。
これによってAOPが再評価される意見も多いので、今回はそれに便乗して冒頭で述べた問題をAOPで解決する簡単な例をお見せしたいと思います。
例えば下記のような、リモートサービスと通信し、ひたすらレスポンスを表示するだけのプログラムがあります:
public class CloudServiceConnect {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
String response = getDataFromServiceA();
System.out.println(response);
}
}
この中で呼び出されているgetDataFromServiceA()は、リモートサービスと通信するモジュールとします。約10回に1回は例外になる、悪いヤツです。
/**
* リモートシステムからデータを取得するメソッド
*/
private static String getDataFromServiceA() {
// リモートシステムが一定の確率で通信エラーなどが原因で例外が発生することをシミュレーションするため、10回に一回RuntimeExceptionを返す
Random r = new Random();
if (r.nextInt(10) == 0) {
throw new RuntimeException("雲の向こうで何かが起きた");
}
return "good response from serviceA";
}
単純に例外処理をするだけなら簡単です、getDataFromServiceAを呼び出すところをtry-catchで囲んで、例外が発生したとメッセージを表示すればとりあえず処理は継続できますが、複雑なシステムとのやり取りなら当然インターフェースとなるメソッドは一箇所ではなくう、例外処理すべき箇所が多くてすべての呼び出しにそのようなコードを書くのはあまり楽しい仕事ではないし、なにより同様なコードが散在することでバグの原因になる可能性があります。では通信モジュールに例外処理のコードを書くか、例外処理用のラッパーを作るにしても、今度はそれを呼ぶ側の事情によっては都合が悪かったりと柔軟性に欠けます。
そこで、AOPの出番です。(ここではAspectJのコードを使いますが、とてもこのブログに収まる話ではないので詳しい説明は省きます。)
アノテーションが使える最近のAspectJなら、下記のようなコードを入れるだけで、CloudServiceConnect内にあるすべてのget..という名前と一致するメソッドに例外処理を編みこんでくれます。
@Around("execution(* test.CloudServiceConnect.get*(..))")
public Object exceptionRecoveryAdvice(final ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
try {
result = joinPoint.proceed();
}
catch (RuntimeException e) {
//なにもしないでnullを返す
}
return result;
}
これだけあまり魅力を感じないかもしれませんが、例えばServiceAの他により通信条件が悪く、しかもデータを必ず必要で、取れるまでリトライしたいServiceBがあるとします。その場合、下記のコードを追加するだけで対応できます。(対応を分けるということで、上記のexceptionRecoveryAdviceのexecutionをget*ServiceAに変更する必要があります)
@Around("execution(* test.CloudServiceConnect.get*ServiceB(..))")
public Object retryAdvice(final ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
try {
result = joinPoint.proceed();
}
catch (RuntimeException e) {
System.out.println("通信に失敗した、リトライ開始");
for (int i = 0; i < 10; i++) {
try {
result = joinPoint.proceed();
return result;
}
catch (RuntimeException e1) {
System.out.println(String.format("リトライに失敗(%d)回目",i+1));
}
}
}
return result;
}
実際の業務ではもっと複雑な例外処理が必要ですし、Spring AOP(設定ファイルベースでのAOP適用をサポート)の利用によって適用対象自体の外出しなど実務では使いたい技術は今回では紹介できませんでしたが命名規則などを固めてある前提ですが、このパターンを適用すればソースコードを汚さずに、確実な例外処理を簡単に実装できることが伝われば幸いです。
