Spring Framework 入門記 Transactionその2 Declarative Transaction using the Atuoproxy

いやいや,前回DataSourceUtils#getConnection(DataSource),予想通り不評なようで.当然ですよねぇ.あんなの使わされるのなんて,ご勘弁いただきたいってのが普通でしょう.
そのDataSourceUtils#getConnection(DataSource)ですが,トランザクションマネージャと連携すること以外に,SQLExceptionCannotGetJdbcConnectionExceptionというuncheckedな例外にするという役割もあるようです.こだわってるなぁ.プライオリティの設定を間違ってないか?
まぁ,その辺については後ほど対処法を考えることにして,今回はautoproxyを使用した宣言的トランザクションに挑みましょう.
私の場合,新しいことを1つ覚えると代わりに4つくらい忘れてしまいます.無念だ.でも,これを読んでいるあなたも仲間に違いない! autoproxyってなんだっけ? って思った人,「AOP その13 AutoProxyCreator」を読んで思い出しましょう.私も読み直しました.心より恥じる.
ま,覚えておけないから日記に書くわけですよ.そんなもんです,はい.


さて,宣言的トランザクションをautoproxyを使って実現するには,TransactionInterceptorを使います.じつは,前回使用したTransactionProxyFactoryBeanも内部ではこいつをWeavingするわけですが,表には出てこなかったんですよね.裏でこっそりとWeavingされていたのです.しかし,autoproxyを使う場合には,こいつを明示的に用意してあげなければいけないようです.うーん,あっちを立てればこっちが立たず?
そのTransactionInterceptorですが,プロパティが2つあります.

  • transactionManager
  • transactionAttributeSource

transactionManagerは,このTransactionInterceptorが使用するトランザクションマネージャです.そのまんまですね.
そしてtransactionAttributeSourceですが,これはどのメソッドにどんな性質のトランザクションを適用するかを設定するもので,次の実装クラスが用意されています.

  • MatchAlwaysTransactionAttributeSource
  • NameMatchTransactionAttributeSource
  • MethodMapTransactionAttributeSource

まずMatchAlwaysTransactionAttributeSourceですが,これはターゲットになるBeanのメソッドを片っ端からトランザクショナルにしてしまうという豪快なもので,次のプロパティを持っています.

    • transactionAttribute

このプロパティはTransactionAttributeというinterface型です.これは前回TransactionProxyFactoryBeantransactionAttributeに設定したもので,あの"PROPAGATION_REQUIRED"とか指定したやつです.
次のNameMatchTransactionAttributeSourceは,個々のメソッドをどんな性質でトランザクショナルにするかを指定できるというもので,次のプロパティを持っています.

    • properties

このプロパティはもちろんProperties型で,そのキーはメソッド名(末尾にワイルドカード'*'を使うことができます),値はトランザクションの性質を示す文字列です.これもまた,前回TransactionProxyFactoryBeantransactionAttributeに設定したものと同じです.
最後のMethodMapTransactionAttributeSourceですが,これは基本的にNameMatchTransactionAttributeSourceと同じみたいです.ただし,ProeprtyEditorが用意されているため,文字列で一気に表現できるという特徴があります.つまり TransactionInterceptor<property>要素に <bean>要素ではなく <value>要素を書くと,このMethodMapTransactionAttributeSourceが設定されるというわけです.その文字列は,method=transactionAttributeという行の並びです.methodは末尾にワイルドカード'*'を使うことができます.transactionAttributeはこれまたトランザクションの性質を示す文字列です.
さて,前回もちょっと使ったトランザクションの性質を示す文字列ですが,これの正体はTransactionAttributeというinterfaceらしいです.そして,文字列からその実装クラスのインスタンスに変換するためのPropertyEditorである,TransactionAttributeEditorが用意されています.そのJavaDocに,この文字列表現の説明がちゃんとありました.前回はそこまで見ませんでした.心より恥じる.ともかくその文字列は,次のように記述できるようです.

PROPAGATION_NAME,ISOLATION_NAME,readOnly,+Throwable,-Throwable

このうち,PROPAGATION_NAMEだけが必須で,その他は任意です.また,順序も任意らしいです.
+Throwable-Throwableは任意の数だけ記述できます.この意味は前回も書いたように,JoinpointのメソッドがそのThrowableをスローした場合に,トランザクションをコミットする('+')か,ロールバックする('-')かを指定します.
ということで,今回必要な予習はこれくらいのようです.後は使うだけなのですがその前に! DataSourceUtils問題の対処を考えましょう.


誰がどう考えたって,DataSourceUtilsをアプリで使いたいとは思わないはず.Springに依存しちゃいますからね.じゃあどうればいいか? こんな手が思いつきます.

  • DataSourceのラッパーを作ってそのgetConnectionDataSourceUtilsを使う.
  • DataSourceTransactionManagerに代わるPlatformTransactionManagerを実装する.
  • S2のJTAとコネクションプールを使う.

今回は,1番目の方法を試してみましょう.魅力的な3番目の方法は,次回挑戦しようと思います.
そのラッパーですが,こんな感じになりました.

package study;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class DataSourceWrapper implements DataSource {
    private DataSource actualDataSource;
    public Connection getConnection() throws SQLException {
        if (!TransactionSynchronizationManager.hasResource(this)) {
            return actualDataSource.getConnection();
        }
        return DataSourceUtils.getConnection(this);
    }
    public Connection getConnection(String userName, String password) throws SQLException {
        throw new UnsupportedOperationException("getConnection(String, String)");
    }
    public DataSource getActualDataSource() {
        return actualDataSource;
    }
    public void setActualDataSource(DataSource actualDataSource) {
        this.actualDataSource = actualDataSource;
    }
    public int getLoginTimeout() throws SQLException {
        return actualDataSource.getLoginTimeout();
    }
    public void setLoginTimeout(int seconds) throws SQLException {
        actualDataSource.setLoginTimeout(seconds);
    }
    public PrintWriter getLogWriter() throws SQLException {
        return actualDataSource.getLogWriter();
    }
    public void setLogWriter(PrintWriter writer) throws SQLException {
        actualDataSource.setLogWriter(writer);
    }
}

こいつのgetConnection()では,DataSourceUtilsと同じようにTransactionSynchronizationmanagerに自分が登録済みかどうかをチェックしています.登録されていなければ(DataSourceUtilsから呼び出された場合)本当のDataSourceからConnectionを取得します.登録済みのとき(アプリから呼び出された場合)は,DataSourceUtilsからConnectionを取得します.
これでアプリは普通にDataSource#getConnection()ができる! はず!


では,autoproxyによる宣言的トランザクションを実践しましょう.
まずトランザクションAspectをWeavingされるFooですが,基本的に前回と同じです.ただし,せっかく作ったDataSourceWrapperを使うために,

Statement st = DataSourceUtils.getConnection(dataSource).createStatement();

Statement st = dataSource.getConnection().createStatement();

に変更しています.
そして定義ファイル.

<bean id="actualDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName"><value>org.hsqldb.jdbcDriver</value></property>
    <property name="url"><value>jdbc:hsqldb:.</value></property>
    <property name="username"><value>sa</value></property>
    <property name="password"><value></value></property>
    <property name="minIdle"><value>1</value></property>
</bean>

<bean id="dataSource" class="study.DataSourceWrapper">
    <property name="actualDataSource"><ref bean="actualDataSource"/></property>
</bean>

<bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource"><ref bean="dataSource"/></property>
</bean>

<bean id="txAttribute" 
    class="org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">
    <property name="properties">
        <props>
            <prop key="insert">PROPAGATION_REQUIRED,+java.lang.UnsupportedOperationException</prop>
            <prop key="update">PROPAGATION_REQUIRED,-java.io.IOException</prop>
            <prop key="*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

<bean id="txInterceptor" 
    class="org.springframework.transaction.interceptor.TransactionInterceptor">
    <property name="transactionManager"><ref bean="transactionManager"/></property>
    <property name="transactionAttributeSource"><ref bean="txAttribute"/></property>
</bean>

<bean id="debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>

<bean id="autoProxyCreator" 
    class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames"><value>foo</value></property>
    <property name="interceptorNames"><value>txInterceptor,debug</value></property>
</bean>

<bean id="foo" class="study.Foo">
    <property name="dataSource"><ref bean="dataSource"/></property>
</bean>

うーん,ラクチンなはずのautoproxyを使ったら逆に複雑になったような? 無念だ.
というのもですね,以前autoproxyを学習したときには気づかなかったのですが,autoproxyを使った場合,<property>要素の中で<bean>要素を記述するとエラー(例外)になっちゃうみたいなんですね.なので,一つ一つ個別に定義しなきゃいけなくて,こんな大変な事態になってしまったというわけで.要するにSpringを使う場合,定義ファイルは優しくもなければ易しくもないというのが結論です.無念だ.
それはともかく,今回はBeanNameAutoProxyCreatorを使っています.そこでターゲットとしてfooを,AspectとしてtxInterceptordebugを指定しています.
これを前回と同じ実行用クラスで実行!!!!

 Debug interceptor: count=1 invocation=[Invocation: method=[public void study.Foo.createTable 以下略
 Debug interceptor: next returned
 - Initiating transaction commit
 Debug interceptor: count=2 invocation=[Invocation: method=[public void study.Foo.insert 以下略
 Debug interceptor: next returned
  - Initiating transaction commit
 Debug interceptor: count=3 invocation=[Invocation: method=[public java.lang.String study.Foo.query 以下略
 Debug interceptor: next returned
 - Initiating transaction commit
 Ebihara
 Debug interceptor: count=4 invocation=[Invocation: method=[public void study.Foo.insert 以下略
 - Initiating transaction commit
 Debug interceptor: count=5 invocation=[Invocation: method=[public java.lang.String study.Foo.query 以下略
 Debug interceptor: next returned
 - Initiating transaction commit
 Yada
 Debug interceptor: count=6 invocation=[Invocation: method=[public void study.Foo.update 以下略
 - Invoking rollback for transaction on method 'update' in class [study.Foo] due to throwable [java.io.IOException]
 - Initiating transaction rollback
 Debug interceptor: count=7 invocation=[Invocation: method=[public java.lang.String study.Foo.query 以下略
 Debug interceptor: next returned
 - Initiating transaction commit
 Yada

バッチリ,前回と同じ結果になりました.DataSourceUtilsを忘れてもよさそうです.
ということでautoproxyの使い方も一応マスターできましたが,たいして記述が簡潔になるわけでもないのに不明瞭さが増すだけという感じがするので,基本的にはTransactionProxyFactoryBeanを使っていきたいと思います.
さて,せっかくのTransaction編だというのに,一度もJTAを使っていないのはもぐりでしょう.どう考えたって,JTAを使わずしてTransaction編を終えるわけにはいきません.そこで,次回はJTAです.Spring的にはJOTMとか使えってことだと思われますが,はてな的には断然S2です.S2のJTAをSpring上で使うという,なかなか楽しげなことをやってみようと思います.うん,その方が素敵!(キラッ)