Spring Framework 入門記 のTransaction編を復習
入門記の連載?当時、何を隠そうAOPの最初で脱落していて、Transactionのところもちゃんと読みきれていなかったのですが、今回読み直してみました。復習として次のことをやってみます。入門記ですでに行われたことばかりですが。
- jtaTransactionManagerを使う
- AutoProxyCreatorを使う
- 任意の例外でcommit/rollbackを制御
入門記をそのままなぞっても仕方ないのでほんのちょっとだけ違うことしてみます。
- JTAにはJOTMを使う。SpringにJotmFactoryBeanが用意されているのでこれを使ってみます。
- ワイルドカードを使って引っかかるクラス名のコンポーネントに対してだけトランザクションをインターセプトする。(入門記との微妙な違い)。
- JdbcTemplateをDIする。DataSourceを直接コンポーネントが扱うのは危険(DataSourceUtil#getConnection(DataSource)を使わないといけない)なので使わない方向で。当然データアクセスはSpringに依存しちゃいますけど。
- 任意の例外でcommit/rollbackを制御のところは入門記とほとんど同じです。
FactoryBean、AutoProxyCreator、JdbcTemplate。うーんSpringっぽい。
tx.xml : 設定ファイル。BeanPostProcessorを使っているのでApplicationContext系のBeanFactoryで読み込まなければいけないことに注意。登録されているbeanの名前が「Logic」で終わる場合にAspectをかける。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"><!-- xapoolのXADataSource --> <!-- xapoolのコネクションプーリング --> org.hsqldb.jdbcDriver jdbc:hsqldb:hsql://localhost:9001 sa <!-- JOTM(UserTransaction兼TransactionManager)生成FactoryBean --> <!-- SpringのTransactionManager --> <!-- データアクセスに使われるSpringのJdbcTemplate --> <!-- Transaction境界としたいクラスを引っ掛けるBeanPostProcessor --> <!-- トランザクション属性とcommit/rollbackを行う例外の設定(メソッドの名前ごとに指定) --> *Logic txInterceptor <!-- Transactionの開始や終了を行うインターセプター --> PROPAGATION_REQUIRED, +java.lang.UnsupportedOperationException, -java.io.IOException PROPAGATION_REQUIRED <!-- Transaction境界となるビジネスロジックのオブジェクト(実質的にはDAOだけど) -->
carol.properties : JOTM使うのに必要らしい。ないとぬるぽがでて動かない。
# lmi stands for Local Method Invocation carol.protocols=lmi # do not use CAROL JNDI wrapper carol.start.jndi=false # do not start a name server carol.start.ns=false
Address.java : DBのTableに対応するクラス
package nt.spring.tx; public class Address { private String name; private int tel; // コンストラクタやアクセッサーは省略 }
AddressLogic.java : トランザクションインタセプターが扱うクラスとなる。トランザクション境界になるということでビジネスロジックな名前にしたけれども、やっていることはデータアクセス。JdbcTemplateがDIされる。
package nt.spring.tx; import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; public class AddressLogic { private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public List getAllAddress() { ResultSetExtractor rse = new ResultSetExtractor() { public Object extractData(ResultSet rs) throws SQLException { List list = new ArrayList(); while (rs.next()) { Address address = new Address(); address.setName(rs.getString("name")); address.setTel(rs.getInt("tel")); list.add(address); } return list; } }; return (List)jdbcTemplate.query("select * from ADDRESS", rse); } public int deleteAll() { return jdbcTemplate.update("delete from ADDRESS"); } public int insert1(Address address) { jdbcTemplate.update("insert into ADDRESS values(?, ?)", new Object{address.getName(), new Integer(address.getTel())}); throw new UnsupportedOperationException(); } public int insert2(Address address) throws IOException { jdbcTemplate.update("insert into ADDRESS values(?, ?)", new Object{address.getName(), new Integer(address.getTel())}); throw new IOException(); } }
AddressClient.java : BeanFactoryからAddressLogicを取得して処理を行う。処理が最後まで流れても裏で動くThreadが残ってしまうのでSystem.exit(1)。ほかに対応の仕方知りません。
package nt.spring.tx; import java.io.IOException; import java.util.Iterator; import java.util.List; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AddressClient { private static final String PATH = "nt/spring/tx/tx.xml"; public static void main(String[] args) { ClassPathXmlApplicationContext factory = new ClassPathXmlApplicationContext(PATH); try { // BeanFactoryからコンポーネント取得 AddressLogic logic = (AddressLogic)factory.getBean("addressLogic"); // クリアー logic.deleteAll(); // 更新実行前 print("Before Insert", logic.getAllAddress()); try { logic.insert1(new Address("taro", 11111)); } catch(UnsupportedOperationException e) { System.out.println(e); } try { logic.insert2(new Address("hanako", 99999)); } catch(IOException e) { System.out.println(e); } // 更新実行後 print("After Insert", logic.getAllAddress()); } catch(Exception e) { System.out.println(e); } finally { factory.close(); // 裏で動くJOnASのスレッドの止め方がわからないので強制終了 System.exit(1); } } private static void print(String msg, List list) { System.out.println(msg + " " + list.size() + "件"); for (Iterator it = list.iterator(); it.hasNext(); ) { System.out.println(it.next()); } } }
結果 : 指定したとおりUnsupportedOperationExceptionが発生したときはコミットが行われてIOExceptionが起きたときはロールバックが行われている。taroさんのデータだけがinsertされているのでOK。
Before Insert 0件 java.lang.UnsupportedOperationException java.io.IOException After Insert 1件 name=taro, tel=11111
感想など
- BeanPostProcessorが設定ファイルに定義されていてもXmlBeanFactoryでは無視される。またしてもはまりました、しかも数時間...。BeanPostProcessorを使うときはApplicationContext系のBeanFactoryを使いましょう。
- Springのトランザクションの仕組みで直接はつかわないけれども重要な位置を占めているのはTransactionSynchronizationManagerだと思います。これがThreadLocalを2つ持っているのですが、使われ方がややこしい。JtaTransactionManagerとDataSourceTransactionManagerを使った場合でTransactionSynchronizationManagerへのアクセスの仕方がことなるので注意。完全に把握し切れていませんが大雑把に言うと多分こんな感じだと思います。
- DataSourceUtil#doGetConnection() : Connectionの取得とTransactionSynchronizationManagerへのbind
- DataSourceTransactionManager#doCleanupAfterCompletion() : DataSourceTransactionManagerをつかった場合はここでConnection相当をTransactionSynchronizationManagerからunbind
- ConnectionSynchronization#beforeCompletion() : JtaTransactionManagerをつかった場合はここでConnection相当をTransactionSynchronizationManagerからunbind
- DataSourceUtil#doCloseConnectionIfNecessary() : TransactionSynchronizationManagerのunbindが行われていたらConnectionのclose
- JtaTransactionManagerを使った場合、commitの前にConnectionのcloseが呼ばれますが、これは物理的には閉じていないはずです。物理的なcloseは設定ファイルに指定したxapoolのコネクションプーリングがJOTMのTransactionManagerと連携してよろしくやってくれるはず。この前疑問に思ったS2SessionFactoryImplでおこなわているcloseの処理と一緒だと思います。
- DefaultコンストラクタをもってないとAspectかけれないって話が前回のwithout EJB読書会でありましたが、コンストラクタ持ってると確かにorg.aopalliance.aop.AspectExceptionが起きますね。なぜかは未調査。