アサーションをアスペクト&メタデータで実装

Seasar2アサーションアスペクトを作るべく
コードを読み始めたのです。
yushi_さんがアサーションアスペクトで実装し、アサーションの定義を
Selで書くということをなさってます。
AOPってアイディア次第ですね。

アサーションアスペクトで実装するというのは私も以前から考えていたのですが,定義ファイルにアサーションを書くのはちょっと違う感じがしていました.
アサーションはやっぱりソースに書きたい!
以前,JavaDocコメントにアサーションを書くことのできるツール(プリプロセッサ?)を雑誌か書籍で見たことがあるのですが,あのような感じがいいなぁと思うわけです.
と,よく考えたらあるじゃないですか,メタデータが.
というわけで,メタデータで記述したアサーションアスペクトで実装するということをSpringでやってみました.
ついでに,アサーションの記述にはGroovyを使ってみました.てへっ.


メタデータを使うには,その属性値を保持するクラスを作るところから始めます.今回はもちろん,PrePostで((しまった,RequireEnsureにすべきだったか?)),それぞれ事前条件・事後条件を表します.どちらもGroovyによる条件式(スクリプト)を文字列として持つので,基底クラスを用意しました.
基底クラスはこんな感じ.

package study;

public class AssertionAttribute {
    private String condition;
    public AssertionAttribute(String condition) {
        setCondition(condition);
    }
    public String getCondition() {
        return condition;
    }
    public void setCondition(String condition) {
        this.condition = condition;
    }
}

事前条件の属性クラス.

package study;

public class Pre extends AssertionAttribute {
    public Pre(String assertion) {
        super(assertion);
    }
}

事後条件の属性クラス.

package study;

public class Post extends AssertionAttribute {
    public Post(String assertion) {
        super(assertion);
    }
}

これで,

/**
 * @@study.Pre("事前条件")
 * @@study.Post("事後条件")
 */

という具合にアサーションを記述することができます.
しかもこれ,interfaceにも記述できるんですよ.実装クラスが守らなくてはならない事前・事後条件をinterfaceに書けるというのは,かなり素敵です.
で,これを扱うためのAdvisorを用意します.これも基底クラスと事前・事後条件それぞれ用のサブクラスを作成しました.
まずは基底クラス.

package study;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.aopalliance.aop.Advice;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import org.springframework.metadata.Attributes;

public abstract class MethodAssertionAdvisor extends StaticMethodMatcherPointcutAdvisor implements Advice {
    private Attributes attributes;
    public MethodAssertionAdvisor() {
        setAdvice(this);
    }
    public Attributes getAttributes() {
        return attributes;
    }
    public void setAttributes(Attributes attributes) {
        this.attributes = attributes;
    }
    public boolean matches(Method method, Class targetClass) {
        return !getAssertions(method, targetClass).isEmpty();
    }
    public void verifyAssertions(Method method, Object target, Map variables) throws Throwable {
        Collection assertions = getAssertions(method, target.getClass());
        Binding binding = new Binding(variables);
        GroovyShell shell = new GroovyShell(binding);
        Iterator it = assertions.iterator();
        while (it.hasNext()) {
            AssertionAttribute assertion = (AssertionAttribute) it.next();
            Object result = shell.evaluate(assertion.getCondition());
            if (!((Boolean) result).booleanValue()) {
                throw new AssertionError(assertion.getCondition());
            }
        }
    }
    private Collection getAssertions(Method method, Class targetClass) {
        List assertions = new ArrayList();
        for (Class clazz = targetClass; clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Method m = clazz.getMethod(method.getName(), method.getParameterTypes());
                assertions.addAll(attributes.getAttributes(m, getAttributeClass()));
            }
            catch (NoSuchMethodException ignore) {
            }
        }

        Class[] interfaces = targetClass.getInterfaces();
        for (int i = 0; i < interfaces.length; ++i) {
            try {
                Method m = interfaces[i].getMethod(method.getName(), method.getParameterTypes());
                assertions.addAll(attributes.getAttributes(m, getAttributeClass()));
            }
            catch (NoSuchMethodException ignore) {
            }
        }
        return assertions;
    }
    protected abstract Class getAttributeClass();
}

事前条件のAdvisor.

package study;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.springframework.aop.MethodBeforeAdvice;

public class PreConditionAdvisor extends MethodAssertionAdvisor implements MethodBeforeAdvice {
    public void before(Method method, Object[] args, Object target) 
            throws Throwable {
        Map variables = new HashMap();
        variables.put("this", target);
        variables.put("args", args);
        verifyAssertions(method, target, variables);
    }
    protected Class getAttributeClass() {
        return Pre.class;
    }
}

事後条件のAdvisor.

package study;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.springframework.aop.AfterReturningAdvice;

public class PostConditionAdvisor extends MethodAssertionAdvisor implements AfterReturningAdvice {
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) 
            throws Throwable {
        Map variables = new HashMap();
        variables.put("this", target);
        variables.put("args", args);
        variables.put("result", returnValue);
        verifyAssertions(method, target, variables);
    }
    protected Class getAttributeClass() {
        return Post.class;
    }
}

事前・事後条件とも,自分のインスタンスthisで,メソッドの引数をargsという配列(これはもう一工夫したいところ)でアクセスできます.事後条件ではメソッドの戻り値をresultという変数でアクセスできます.


ということで,お試し用のクラスを用意しましょう.今回は,interfaceと実装クラスを分けてみました.
まずはinterface

package study;

public interface Hoge {
    /**
     * @@study.Pre("args[0] != null")
     * @@study.Post("result != null")
     */
    String geho(String text);
}

これをAttribute Compilerでコンパイルします.
次に実装クラス.

package study;

public class HogeImpl implements Hoge {
    public String geho(String text) {
        return text.length() == 0 ? null : text;
    }
}

引数が空文字列だったら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 id="attributes" class="org.springframework.metadata.commons.CommonsAttributes"/>

    <bean id="preAdvisor" class="study.PreConditionAdvisor">
        <property name="attributes"><ref bean="attributes"/></property>
    </bean>
    <bean id="postAdvisor" class="study.PostConditionAdvisor">
        <property name="attributes"><ref bean="attributes"/></property>
    </bean>

    <bean id="hoge" class="study.HogeImpl"/>
</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();

            Hoge hoge = (Hoge) context.getBean("hoge");
            String text = new String[] {"hoge", null, ""};
            for (int i = 0; i < text.length; ++i) {
                try {
                    hoge.geho(text[i]);
                }
                catch (Throwable e) {
                    System.out.println(e);
                }
            }

            ref.release();
        }
        catch (Throwable e) {
            e.printStackTrace();
        }
    }
    private static void doGeho(Hoge hoge, String text) {
    }
}

ここでは3回Hoge#geho(String)を呼び出します.2回目の引数はnullなので事前条件に違反します.3回目の引数は空文字列で,その場合HogeImpl#geho(String)nullを返すので事後条件に違反します.
これを実行!!!





java.lang.AssertionError: args[0] != null


java.lang.AssertionError: result != null

なんか,Groovyでスクリプトを実行するごとに,[]が2回表示されるみたいです.
で,ちゃんとアサーションの違反をはねることができました.


今回はとりあえず作っただけという感じで,メソッド呼び出しの度にメタデータを探してGroovyで評価しているのですが,このあたりはアスペクトをWeavingするときにバイトコードに変換したものをキャッシュするとかできたらいいですね.そういう,パフォーマンスをちゃんと考慮した実装をすれば,結構使える気がしてきました.
いいかも,DbC + AOP + Metadata