Spring Framework 入門記 AOPその14 Metadata-Driven Autoproxying

前回に引き続きautoproxyです.今回は「5.9.2. Using metadata-driven autoproxying」に進みます.
Metadataですよ,メタデータC#の,あれ.J2SE1.5(Tiger)で導入される,あれ.あれを先取りしてAOPです.カックイー!
ということで,Core Concernのソースにメタデータを記述してWeavingするAspectを指定するっていうのが今回の学習内容です.個人的には,Crosscutting Concernの適用をCore Concernのソースで指定するのには抵抗を感じないではありません.よく,@remoteみたいなメタデータの例を見かけますが,こんなのをCore Concernで指定するのはちょっと違うと思うわけです.でも,それはたぶん@remoteなんていうのでは抽象度が低すぎるからで,もっと抽象度の高い表現ならいいのかもしれません.具体的にどんなのって言われても困るわけですが,例えば@serviceとかって感じ? で,そのサービスメソッドをリモートにするかしないかは,後からどうにでもすればいいわけで.
IoCAOPだけでもどう設計すればいいかが追いついていない感じの現状ですが,メタデータもまた同じような課題を増やしてくれそうですね.


能書きはこれくらいにして,Springでメタデータをどう扱うのか見ていきましょう.
Springでは今のところ, Apache Jakarta CommonsAttributes を使ってメタデータを扱うようです.ただし,直接 Commons Attributes のAPIを呼び出さなくてもいいように,抽象レイヤが用意されているみたい.


ということで,まずは Commons Attributes に寄り道します.不覚にも触ったことがないもので.ドキドキです.
Commons Attributes では,メタデータの記述は

/**
 * @@MyAttribute ("constructor arg", property="value")
 */
public class Foo {
・・・

みたいに記述するようです.'@'が二つなんですねぇ.
このようにメタデータを記述したソースを,Attribute Compiler というものにかけると,上の例だとFoo$__attributeRepositoryというクラスのソースファイルが生成されます.'$'なんてついていますが,別に内部クラスではありません.そういう名前の普通のクラスです.この生成されたクラスは,元のクラス(Foo)の中に記述されたメタデータに関する情報を返してくれるようなクラスで,次のようなメソッドを持っています.

  • Set getClassAttributes()
  • Map getFieldAttributes()
  • Map getConstructorAttributes()
  • Map getMethodAttributes()

なるほどなるほど.それで,こいつらが返してくれるSetMapの要素はどんなものかというと,上の例の場合はMyAttributeというクラスのインスタンスになります.そういう風に書いたので.MyAttributeクラスには,上の書き方だと1つのStringを引数として受け取るコンストラクタと,propertyという名前でString型のプロパティのsetterつまりsetProperty(String)が必要です.これはあくまで上のように書いた場合の話で,実際には引数の数や型は自由自在みたいです.というのも,メタデータでの記述がほとんどそのまま生成されるソースファイルにコピペされるだけなのです.ナイス.なのか?
とまぁ,そんなわけでメタデータリポジトリとなるクラスのソースが生成されますから,それをコンパイルして使えばいいみたい.使い方は,普通にインスタンスnewして前述のメソッドを呼ぶだけ.簡単そうですね.これでひとまず Commons Attributes を終了,Springに戻ります.


ということで,Springに戻ってきました.Springでは,メタデータを扱うためにAttributesというinterfaceを用意しています.こいつは,次のメソッドを持っています.

  • Collection getAttributes(Class)
  • Collection getAttributes(Field)
  • Collection getAttributes(Field, Class)
  • Collection getAttributes(Method)
  • Collection getAttributes(Method, Class)

なんとなくイメージは分かりますね.コレクションの要素が何かというのは明記されていない感じなのですが,普通に考えれば先ほどのMyAttributeみたいなのに決まってます.間違いない.二つ目の引数でClassを取るものは,その型のメタデータ情報だけを取得するためのものです.
Attributesinterfaceなのですが,それをimplementsしたclassとしてCommonsAttributesが用意されています.もちろん,Commons Attributes を使った実装です.こいつは普通にBeanとしてインスタンス化して,メソッドを呼び出せばそれでいいみたい.
それで,このAttributesをどこで使うかというと,もうおなじみの Advisor(Pointcut) で使うようです.具体的には,ClassFilter#matches(Class)とかMethodMatcher#matches(Method, Class)の中でAttributesを呼び出してあげればよさげです.
さぁ,これで材料が揃いました.メタデータは定義できるし,それを取得するべき場所もその方法もわかりました.後は実践あるのみ!


で,恥ずかしながらまたしてもDebugInterceptorを使います.しょうがないんですよ,日記としては標準出力に跡を残してくれるAspectを使うのが一番なんですから.心より恥じる.
とういわけで,メタデータ@@DebugAttribute に決定です.となると,必然的にDebugAttributeクラスを用意しなくてはなりません.でも,トランザクションAspectみたいにREQUIREDとか指定する必要はないので,超単純です.

package study;

public class DebugAttribute {
}

日記に載せるほどでもないですね.いいんです,これも大切な思い出なんです.
で,次にAspectを適用するクラス.今回はメタデータ付.

package study;

public class Foo {
    /**
     * @@DebugAttribute()
     */
    public void one() {
        System.out.println("one");
    }
    public void two() {
        System.out.println("two");
    }
    /**
     * @@DebugAttribute()
     */
    public void three() {
        System.out.println("three");
    }
}

3つの無駄なメソッドのうち二つにはメタデータデバッグ対象であることを記述しています.
これをAttribute Compilerで処理します.Antのタスクが提供されているので,次のようなbuild.xmlを用意すればOK.

<?xml version="1.0"?>
<project name="attribute-compiler" basedir="." default="build">
    <target name="build">
        <property name="spring.root" value="C:\Program Files\spring\spring-framework-1.0"/>
        <path id="attribute-compiler-classpath">
            <fileset dir="${spring.root}/lib/jakarta-commons">
                <include name="commons-attributes-compiler-SNAPSHOT.jar"/>
                <include name="commons-collections.jar"/>
            </fileset>
            <fileset dir="${spring.root}/lib/xdoclet">
                <include name="xjavadoc-1.0.jar"/>
            </fileset>
        </path>
        <taskdef resource="org/apache/commons/attributes/anttasks.properties">
            <classpath refid="attribute-compiler-classpath"/>
        </taskdef>
        <attribute-compiler destdir="${basedir}/generated">
            <fileset dir="${basedir}/sources" includes="**/*.java"/>
        </attribute-compiler>
    </target>
</project>

これをAntで実行すると,study.Foo$__attributeRepositoryが生成されます.
次にStaticPointcutなAdvisorを作成します.

package study;
import java.lang.reflect.Method;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import org.springframework.metadata.Attributes;

public class DebugAttributesAdvisor extends StaticMethodMatcherPointcutAdvisor {
    private Attributes attributes;
    public boolean matches(Method method, Class targetClass) {
        return !attributes.getAttributes(method, DebugAttribute.class).isEmpty();
    }
    public Attributes getAttributes() {
        return attributes;
    }
    public void setAttributes(Attributes attributes) {
        this.attributes = attributes;
    }
}

このクラスは,プロパティとしてAttributesを持っており,matches(Method, Class)ではそれを使って該当のメソッドにDebugAttributeが存在するかチェックしています.Attributes#getAttributes()メタデータがない場合は空のコレクションを返します.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 class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
    <bean class="org.springframework.metadata.commons.CommonsAttributes"/>
    <bean class="org.springframework.aop.interceptor.DebugInterceptor"/>

    <bean class="study.DebugAttributesAdvisor" autowire="byType"/>
    <bean id="foo" class="study.Foo"/>
</beans>

えへへ,今回はautowireを指定して,シンプルな定義ファイルに挑戦してみました.
さて,あとは実行用のクラスです.前回同様,コンテナからfooを取り出して,3つのメソッドを呼ぶだけです.
それを実行!!!

Debug interceptor: count=1 invocation=[Invocation: method=[public void study.Foo.one()] args=[Ljava.lang.Object;@289d2e] target is of class study.Foo]
one
Debug interceptor: next returned
two
Debug interceptor: count=2 invocation=[Invocation: method=[public void study.Foo.three()] args=[Ljava.lang.Object;@289d2e] target is of class study.Foo]
three
Debug interceptor: next returned

ばっちグー.


出だしで@remoteは抽象度が低いとか偉そうに書いておきながら,サンプルで使ったのが@@DebugAttribute()とは... 心より恥じる.