意気あいあい

ちょっと前まで蛯原友里ちゃんが出演していたドラマ「サラリーマン金太郎4」.
あまり出番は多くなかったのですが,社長の秘書という役所でした.
このドラマには会長の秘書役として曲山えりが出ていたのですが,TBSの番組のページには「曲山えりの秘書の一日」というコーナーがあって,いくつかの動画を見ることができます.
その中の「LESSON No.04 先輩秘書から後輩秘書へ」には,友里ちゃんがゲストとして出ていて,二人で対談をしています.
その中で面白いのが,

えり:どうですか? 「サラリーマン金太郎4」撮影の現場は?
友里:すごい,なんか皆さん,意気,意気あいあい,意気あいあいって言うんですか?
えり:和気あいあい.
友里:和気あいあいですよねっ.和気あいあいとしてすごい楽しい現場ですよねっ!

あはは,どうやら意気投合と和気あいあいが混ざっちゃったみたいです.(^^;
いやぁ,友里ちゃんかわいいなぁ*1

*1:と書いている自分は相当バカですネ.心より恥じる.

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

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

Spring Framework 入門記 JDBCその2 JdbcTemplate#query

まずは落ち穂拾いから.
前回の内容に間違いがありました.無念だ.
JdbcTemplateのお手軽問い合わせメソッドについてですが,これらは戻り値がObjectのものとそれ以外でメソッドのシグネチャが異なります.まずObject以外については,

    • queryForXxx(String sql)
    • queryForXxx(String sql, Object[] args)

の2種類があり,Xxxの部分はIntLongListの3種類があります.
一方Objectを返すメソッドとしては,

    • queryForObject(String sql, Class requiredType)
    • queryForObject(String sql, Object[] args, Class requiredType)

の2種類があります.
ちょっと混乱していました.心より恥じる.


気を取り直して,ここから今日の学習です.
今回もJdbcTemplateの学習を続けます.まだ見ていないことが結構あるようなので.
前回書いたように,JdbcTemplateは豊富なメソッドを持っています.publicな問い合わせメソッドだけでも16個もあります.これらをもう少し詳しく見ていくことにします.
前回は,queryForInt(String)のような使いやすそうなメソッドを見たわけですが,それではすまない場合も多いでしょう.そのような場合に備えて,JdbcTemplateの問い合わせメソッドは様々にカスタマイズできるようになっています.


まずは,ResultSetの扱いをカスタマイズする方法を見てみましょう.それには,

  • RowCallbackHandler

というinterfaceimplementsしたclassを用意して,JdbcTemplate

    • List query(String sql, RowCallbackHandler rch)

などに渡します.
このRowCallbackHandlerResultSetを扱うためのもので,結果セットの行ごとにJdbcTemplateからコールバックされる,次のメソッドを持っています.

    • void processRow(ResultSet rs)

引数でResultSetを渡されますが,そのnext()を呼び出すのはJdbcTemplateの役割です.
ところで,このメソッドはvoidなんですね.行を処理した結果はどうするの?
このあたり,ちょっとイマイチな感じなのですが,実はRowCallbackHandlerの実装クラスが

  • ResultReader

というinterfaceimplementsしていれば,そのメソッドである

    • List getResults()

を通じてListを返すことができます.ということは,自分でListを用意するのですね.てっきり,行ごとのインスタンスを返せば,それをJdbcTemplateListに加えてくれるのだと思っちゃいました.無念だ.
なお,RowCallbackHandlerの実装クラスがResultReaderimplementsしていなければ,query(String, RowCallbackHandler)などのメソッドはnullを返します.
ということで,事実上RowCallbackHandlerResultReaderは同時にimplementsする必要がありますね.


これで十分とは限りません.結果をList以外で取得したい場合もあるでしょう.そんな場合でも,JdbcTemplateを利用することができます.それには,

  • ResultSetExtractor

というinterfaceimplementsしたclassを用意して,JdbcTemplate

    • Object query(String sql, ResultSetExtractor rse)

などに渡します.
このResultSetExtractorは,文字通りResultSetを展開するもので,次のメソッドを持っています.

    • Object extractData(ResultSet rs)

このメソッドは,RowCallbackHandler#processRow(ResultSet)と異なり,問い合わせごとに一回だけ呼び出されます.ResultSet#next()を呼び出すのもこのメソッドの役割になります.


ということで,ResultSetをどう扱うかで,JdbcTemplateのメソッドを次のように使い分ければいいようです.

List queryForList(String sql)
問い合わせ結果をMapListで欲しい場合に使う.
List query(String sql, RowCallbackHandler)
問い合わせ結果を任意の型のListで欲しい場合に使う.
Object query(String sql, ResultSetExtractor
問い合わせ結果を任意の型で欲しい場合に使う.

ということになります.


さて,カスタマイズしたくなるのはResultSetの扱いだけとは限りません.もしかすると,PreparedStatementの扱い方もカスタマイズしたくなるかもしれません.
例えばObjectの配列を作りたくない場合.そんな場合は,

  • PreparedStatementSetter

というinterfaceimplementsしたclassを用意して,JdbcTemplate

    • Object query(String sql, PreparedStatementSetter pss, RowCallbackHandler)

などに渡します.
このinterfaceは次のメソッドを持っています.

    • void setValues(PreparedStatement ps)

ここで,好きなようにPreparedStatementにパラメータを設定できます.
それでも十分ではない場合もあるかもしれません.例えばPreparedStatementをキャッシュしたりとか.そんな場合は,

  • PreparedStatementCreator

というinterfaceimplementsしたclassを用意して,JdbcTemplate

    • Object query(PreparedStatementCreator psc, RowCallbackHandler)

などに渡します.
このinterfaceは次のメソッドを持っています.

    • PreparedStatement createPreparedStatement(Connection conn)

ここで,好きなようにPreparedStatementを用意して返せばいいわけですね.


ということで,PreparedStatementをどう扱うかで,JdbcTemplateのメソッドを次のように使い分ければいいようです.

List query(String sql, Object[] args, RowCallbackHandler rch)
パラメータをObjectの配列で渡せる場合に使う.
List query(String sql, PreparedStatementSetter pss, RowCallbackHandler rch)
パラメータをObjectの配列で渡したくない場合に使う.
List query(PreparedStatementCreator psc, RowCallbackHandler rch)
PreparedStatementの取得方法をカスタマイズしたい場合に使う.

なお,これらのメソッドの第3引数は,ResultSetExtractorを受け取るものも用意されています.
ということで,最強(?)の問い合わせメソッドは

    • Object query(PreparedStatementCreator psc, ResultSetExtractor rse)

です.自分でPreparedStatementを用意して,自分でResultSetを処理します.
うーむ,それでもJdbcTemplateを使うメリットって?
おぉ,そうだ! 例外がSQLExceptionじゃなくなるんだ! 後は... そうだ! ResultSet#close()を自分で呼び出さなくてもいいんだ!


さて,こんな感じでいろいろなメソッドがあるわけですから,一通り使うべきだという気持ちもないわけではないのですが,あまりに細かいのばかりなので,メソッド一つだけ使うことで済ませたいと思います.心より恥じる.
前回のサンプルで使っているテーブルは,keyvalueという文字列型のカラムを持っています.ということで,このテーブルの問い合わせ結果をPropertiesにして返すということをやってみましょう.
まずはいつものFoo.例によってメタデータによる宣言的トランザクションを使います.

package study;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;

public class Foo {
    private DataSource dataSource;
    public DataSource getDataSource() {
        return dataSource;
    }
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute()
     */
    public void createTable() {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        jt.execute("create table pair (key varchar, value varchar, primary key(key))");
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute(readOnly=true)
     */
    public Properties query() {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        return (Properties) jt.query("select * from pair", new ResultSetExtractor() {
            public Object extractData(ResultSet rs) throws SQLException {
                Properties props = new Properties();
                while (rs.next()) {
                    props.put(rs.getString("key"), rs.getString("value"));
                }
                return props;
            }
        }
    );
    }
    /**
     * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute(readOnly=true)
     */
    public void insert(String key, String value) {
        JdbcTemplate jt = new JdbcTemplate(dataSource);
        jt.update("insert into pair (key, value) values(?, ?)", new Object[] { key, value });
    }
}

これをAttribute Compilerで処理します.私はいつもこれを忘れて痛い目に遭います.無念だ.
定義ファイルは前回と同じです.
後は実行用クラス.

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.createTable();
            foo.insert("Yuri", "Ebihara");
            foo.insert("Akiko", "Yada");
            System.out.println(foo.query());

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

これを実行!!

 BEGIN study.Foo#createTable()
 - Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
 - Creating shared instance of singleton bean 'DB2'
 - Creating shared instance of singleton bean 'HSQL'
 - Creating shared instance of singleton bean 'MS-SQL'
 - Creating shared instance of singleton bean 'MySQL'
 - Creating shared instance of singleton bean 'Oracle'
 - Creating shared instance of singleton bean 'Informix'
 - Creating shared instance of singleton bean 'PostgreSQL'
 - SQLErrorCodes loaded: [HSQL Database Engine, Oracle, Microsoft SQL Server, Informix Dynamic Server, PostgreSQL, MySQL, DB2]
 - Looking up default SQLErrorCodes for DataSource
 - Database Product Name is HSQL Database Engine
 - Driver Version is 1.7.1
 END study.Foo#createTable() : null
 - Initiating transaction commit
 BEGIN study.Foo#insert(Yuri, Ebihara)
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {534353}. Name is HSQL Database Engine
 END study.Foo#insert(Yuri, Ebihara) : null
 - Initiating transaction commit
 BEGIN study.Foo#insert(Akiko, Yada)
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {534353}. Name is HSQL Database Engine
 END study.Foo#insert(Akiko, Yada) : null
 - Initiating transaction commit
 BEGIN study.Foo#query()
 - Looking up default SQLErrorCodes for DataSource
 - Database product name found in cache {534353}. Name is HSQL Database Engine
 END study.Foo#query() : {Yuri=Ebihara, Akiko=Yada}
 - Initiating transaction commit
 {Yuri=Ebihara, Akiko=Yada}

こんな感じということで.
次はJdbcTemplate#execute()シリーズかなぁ.