意気あいあい
ちょっと前まで蛯原友里ちゃんが出演していたドラマ「サラリーマン金太郎4」.
あまり出番は多くなかったのですが,社長の秘書という役所でした.
このドラマには会長の秘書役として曲山えりが出ていたのですが,TBSの番組のページには「曲山えりの秘書の一日」というコーナーがあって,いくつかの動画を見ることができます.
その中の「LESSON No.04 先輩秘書から後輩秘書へ」には,友里ちゃんがゲストとして出ていて,二人で対談をしています.
その中で面白いのが,
えり:どうですか? 「サラリーマン金太郎4」撮影の現場は?
友里:すごい,なんか皆さん,意気,意気あいあい,意気あいあいって言うんですか?
えり:和気あいあい.
友里:和気あいあいですよねっ.和気あいあいとしてすごい楽しい現場ですよねっ!
あはは,どうやら意気投合と和気あいあいが混ざっちゃったみたいです.(^^;
いやぁ,友里ちゃんかわいいなぁ*1.
*1:と書いている自分は相当バカですネ.心より恥じる.
アサーションをアスペクト&メタデータで実装
Seasar2でアサーションアスペクトを作るべくyushi_さんがアサーションをアスペクトで実装し、アサーションの定義を
コードを読み始めたのです。
Selで書くということをなさってます。
AOPってアイディア次第ですね。
アサーションをアスペクトで実装するというのは私も以前から考えていたのですが,定義ファイルにアサーションを書くのはちょっと違う感じがしていました.
アサーションはやっぱりソースに書きたい!
以前,JavaDocコメントにアサーションを書くことのできるツール(プリプロセッサ?)を雑誌か書籍で見たことがあるのですが,あのような感じがいいなぁと思うわけです.
と,よく考えたらあるじゃないですか,メタデータが.
というわけで,メタデータで記述したアサーションをアスペクトで実装するということをSpringでやってみました.
ついでに,アサーションの記述にはGroovyを使ってみました.てへっ.
メタデータを使うには,その属性値を保持するクラスを作るところから始めます.今回はもちろん,Pre
とPost
で((しまった,Require
とEnsure
にすべきだったか?)),それぞれ事前条件・事後条件を表します.どちらも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
の部分はInt
・Long
・List
の3種類があります.
一方Object
を返すメソッドとしては,
-
queryForObject(String sql, Class requiredType)
queryForObject(String sql, Object[] args, Class requiredType)
の2種類があります.
ちょっと混乱していました.心より恥じる.
気を取り直して,ここから今日の学習です.
今回もJdbcTemplate
の学習を続けます.まだ見ていないことが結構あるようなので.
前回書いたように,JdbcTemplate
は豊富なメソッドを持っています.publicな問い合わせメソッドだけでも16個もあります.これらをもう少し詳しく見ていくことにします.
前回は,queryForInt(String)
のような使いやすそうなメソッドを見たわけですが,それではすまない場合も多いでしょう.そのような場合に備えて,JdbcTemplate
の問い合わせメソッドは様々にカスタマイズできるようになっています.
まずは,ResultSet
の扱いをカスタマイズする方法を見てみましょう.それには,
RowCallbackHandler
というinterface
をimplements
したclass
を用意して,JdbcTemplate
の
-
List query(String sql, RowCallbackHandler rch)
などに渡します.
このRowCallbackHandler
はResultSet
を扱うためのもので,結果セットの行ごとにJdbcTemplate
からコールバックされる,次のメソッドを持っています.
-
void processRow(ResultSet rs)
引数でResultSet
を渡されますが,そのnext()
を呼び出すのはJdbcTemplate
の役割です.
ところで,このメソッドはvoidなんですね.行を処理した結果はどうするの?
このあたり,ちょっとイマイチな感じなのですが,実はRowCallbackHandler
の実装クラスが
ResultReader
というinterface
もimplements
していれば,そのメソッドである
-
List getResults()
を通じてList
を返すことができます.ということは,自分でList
を用意するのですね.てっきり,行ごとのインスタンスを返せば,それをJdbcTemplate
がList
に加えてくれるのだと思っちゃいました.無念だ.
なお,RowCallbackHandler
の実装クラスがResultReader
をimplements
していなければ,query(String, RowCallbackHandler)
などのメソッドはnull
を返します.
ということで,事実上RowCallbackHandler
とResultReader
は同時にimplements
する必要がありますね.
これで十分とは限りません.結果をList
以外で取得したい場合もあるでしょう.そんな場合でも,JdbcTemplate
を利用することができます.それには,
ResultSetExtractor
というinterface
をimplements
した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)
- 問い合わせ結果を
Map
のList
で欲しい場合に使う. List query(String sql, RowCallbackHandler)
- 問い合わせ結果を任意の型の
List
で欲しい場合に使う. Object query(String sql, ResultSetExtractor
- 問い合わせ結果を任意の型で欲しい場合に使う.
ということになります.
さて,カスタマイズしたくなるのはResultSet
の扱いだけとは限りません.もしかすると,PreparedStatement
の扱い方もカスタマイズしたくなるかもしれません.
例えばObject
の配列を作りたくない場合.そんな場合は,
PreparedStatementSetter
というinterface
をimplements
したclass
を用意して,JdbcTemplate
の
-
Object query(String sql, PreparedStatementSetter pss, RowCallbackHandler)
などに渡します.
このinterface
は次のメソッドを持っています.
-
void setValues(PreparedStatement ps)
ここで,好きなようにPreparedStatement
にパラメータを設定できます.
それでも十分ではない場合もあるかもしれません.例えばPreparedStatement
をキャッシュしたりとか.そんな場合は,
PreparedStatementCreator
というinterface
をimplements
した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()
を自分で呼び出さなくてもいいんだ!
さて,こんな感じでいろいろなメソッドがあるわけですから,一通り使うべきだという気持ちもないわけではないのですが,あまりに細かいのばかりなので,メソッド一つだけ使うことで済ませたいと思います.心より恥じる.
前回のサンプルで使っているテーブルは,key
とvalue
という文字列型のカラムを持っています.ということで,このテーブルの問い合わせ結果を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()
シリーズかなぁ.