Spring Framework 入門記 AOPその8 Throws Advice

昨日はトラブル騒ぎで入門記を書けませんでした.無念だ.自分が引き起こしたトラブルなんですけどね.心より恥じる.
まだまだ続くSpring Framework入門記,今回はThrows Adviceです.
Throws adviceは,メソッドJoinpointから例外がスローされた場合に適用されるAdviceです.例外処理をCrosscutting Concern(横断的関心)として扱うことが出来るわけですね.
Throws adviceを実装するには,ThrowsAdviceというinterfaceimplementsします.しかしながらこのinterface,ただのマーカー(タグ)インタフェースで,メソッドは宣言されていません.その代わり,次のようなメソッドをリフレクションを使って呼び出してくれます.

  • afterThrowing(Throwable)
  • afterThrowing(Method, Object[], Object, Throwable)

Joinpointの情報が必要なら後者を,不要であれば前者を使えばいいわけですね.なお,「5.3.2.3. Throws advice」では,引数は1〜4個の範囲で自由であると読めそうなことが書いてある(たぶん)のですが,少なくとも1.0Final Releaseの実装では引数は1個か4個かのいずれかでないと,Throws Adviceとして扱われません.
上記メソッドの最後の引数であるThrowableには,Throwableのサブクラスを指定することが出来ます.そしてSpringは,Joinpointからスローされた例外がThrowableと適合する(代入可能である)場合にのみ,そのメソッドを呼び出してくれます.もし

  • afterThrowing(java.io.IOException)

というメソッドがある場合,Joinpointからスローされた例外がjava.io.IOExceptionまたはその派生クラスであれば,このメソッドが呼び出されます.それ以外の例外の場合には呼び出されません.当然ながら,例外がスローされなかった場合も呼び出されません.
1つのThrowsAdvice実装クラスには,複数のafterThrowing(...)メソッドを定義することが出来ます.その場合,Joinpointからスローされた例外に最も適合する(クラス階層で下位にあるクラスを引数とする)メソッドが呼び出されます.例えば

  • afterThrowing(java.io.IOException)
  • afterThrowing(java.io.FileNotFoundException)

という二つのメソッドがあり,JoinpointからFileNotFoundExceptionおよびその派生クラスがスローされた場合は後者が呼び出されます.それ以外のIOExceptionおよびその派生クラスがスローされた場合は前者が呼び出されます.それ以外の例外がスローされた場合にはどちらも呼び出されません.
ではでは,

  • afterThrowing(java.io.IOException)
  • afterThrowing(Method, Object[], Object, java.io.IOException)

というように同じ例外クラスを引数とするメソッドが二つあるとどうなるのか? 確認したところ,前者のみが呼び出されました.at most once らしいです.
さて,例外処理におけるAspectといえば,最近まさたかさんの日記に対するコメントで,

IOExceptionSQLExceptionなどのchecked例外の場合,unchecked例外で包んでスローするだけということが結構あると思うのですが,そういうのはAspectでやらなきゃ損ですよね.

と書いたのですが,AspectJではこういうことを「例外のソフト化(Exception Softening)」などと呼んでいるようです.
例えばFileReaderなどを使っている場合,IOExceptionをキャッチするか,throws節に宣言するか,どちらかでないとコンパイルエラーになりますが,AspectJで例外のソフト化を行うAspectをWeavingするとあら不思議,コンパイルエラーがなくなります.素晴らしい.
これは例外を扱うとても魅力的なAspectだと思っていたのですが,SpringのようにダイナミックなAOPの場合,うまく使えませんね.なんせ,コンパイルが通った後のクラスファイルに対してAspectをWeavingするわけですが,例外をソフト化する前のコードはコンパイルを通りません.無念だ.よく考えもしないで「Aspectでやらなきゃ損」だなんてコメントしてしまいました.心より恥じる.
例外のソフト化が出来ないとなると,Throws adviceそのものの魅力が大きくダウンというか,正直使い道がわからなくなってしまいました.例外のロギングをすることはできますが,そこかしこのメソッドにそのようなAspectを適用すると,例外が通過するたびに同じログを出力することになってしまいます.あまり有用な使い方ではなさそうです.
まさたかさんの日記まこたんがコメントしたような「リトライ」をするなら,Throws adviceではなく,Around adviceでやるべきではないかと思います.困りましたね.どう使おう? うーむ.いかんなー,やる気NotFoundExceptionthrowしたくなってきました.いかんいかん,やる気Injectionしなくては!!
しょうがないので,例外を握りつぶしてしまう(無視する)ようなThrows adviceでも作ってみましょう.

package study;
import org.springframework.aop.ThrowsAdvice;

public class IgnoreExceptionAdvice implements ThrowsAdvice {
    public void afterThrowing(Exception e) {
    }
}

見事に何もしていません.やる気NullなAdviceです.
次にこのAdviceを適用するクラス.

package study;

public class Foo {
    public void run() {
        System.out.println("こんな仕事やってられるか!");
        throw new RuntimeException("やる気Null");
    }
}

相当不満がたまっているようです.かわいそうに.
その不満を握りつぶすべく,定義ファイルを作成.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="foo" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target">
            <bean class="study.Foo"/>
        </property>
        <property name="interceptorNames"><value>advice</value></property>
    </bean>

    <bean id="advice" class="study.IgnoreExceptionAdvice">
    </bean>
</beans>

そして実行用のクラス.

package study;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springframework.context.ApplicationContext;
import org.springframework.context.access.ContextSingletonBeanFactoryLocator;

public class Main {
    public static void main(String[] args) {
        try {
            BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance();
            BeanFactoryReference ref = locator.useBeanFactory("context");
            ApplicationContext context = (ApplicationContext) ref.getFactory();

            Foo foo = (Foo) context.getBean("foo");
            foo.run();
            foo.run();
            foo.run();

            ref.release();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

いやな仕事を3度もやらせます.ざまーみろ.
んで実行...

こんな仕事やってられるか!
java.lang.RuntimeException: やる気Null
    at study.Foo.run(Foo.java:6)
(以下略)

ありゃりゃ? ThrowsAdviceで例外を再スローしないで無視しても,握りつぶしたことにはならないのですか!? うー,確かにそんなことはどこにも書いていません.無念だ.
ということは,例外を握りつぶすにもThrows adviceではなくAround adviceを使わないとできないということですか.困ったな,本当に何も使い道が思いつかない...
しょうがない,やっぱりこれを実行します.

throw new やる気NotFoundException("使い道ありません");

心より恥じる.