この記事は「エムスリー Advent Calendar 2015」の 2 日目の記事です。
Seasar Conference 2015
先日 9/26 に数年ぶりに Seasar Conference が開催されました。
Seasar2 に関心をお持ちの方であればすでにご存知の通り、一年後に Seasar2 プロジェクトの現行コアチーム体制でのサポートを終了するというアナウンスがありました。OSS なので引き継ぐメンテナがいれば何らかの形でプロジェクトが継続していく可能性はありますが、とりあえずは Seasar2 の一時代の区切りということにはなるかと思います。メンテナの皆様、大変お疲れ様でした。
かつて m3commons-s2 という社内ライブラリがあった
このエントリでは、私と Seasar2 の思い出(5 年以上前なのでもう思い出と言ってよさそうです)について語ってみようかと思います。
まずはこの GitHub リポジトリをご覧ください。
https://github.com/m3dev/m3commons-s2
これはエムスリーの社内リポジトリで 5 年前まで開発されていた社内共通ライブラリです。Apache Commons になぞらえる形で m3commons-*** という名前の Java 共通ライブラリがいくつかあるのですが、そのうちの一つです。今回、この記事を書くにあたり、公開することにしました。
当時、私自身は主に SAStruts + S2JDBC の Web サービスを開発するチームで日々様々な Java アプリケーションの開発をしていました。そのときに共通化できる処理や効率化のためにつくったモジュールをまとめたものです。主な開発者は SAStruts のコミッターでもあった k-dewa さんと私でした。
コミット履歴は公開にあたり引き継ぎませんでしたが、最後にコードに変更を加えられたのは 2010 年 11 月 18 日でした。2010 年以降は Seasar2 が積極的に新規採用されることはなくなり、Java プロジェクトの場合は Spring がメインになり、サーバサイドは Java に限らず Ruby on Rails や Scala での開発が増えていきました。
それでは(誰得感はありますが)早速、この m3commons-s2 の持つ機能について紹介していきたいと思います。
sample プロジェクト
まず、ライブラリ本体の公開だけでなく、今回機能を紹介しやすくするために公式の tutorial アプリをベースに sample プロジェクトをつくっておきました。
こちらは Maven だけで動かせるようになっているので(その際 Hot reloading は期待しないでください)、興味がある方は実際に動かしてみてください。
それでは m3commons-s2 の主要な機能について紹介していきます。
ActionExecuteHookInterceptor
これは SAStruts アプリのために Ruby on Rails の before_action
/after_action
フィルターのような機能を実装したものです。AOP のインターセプタでアノテーションをスキャンして @Execute
がついた SAStruts の Action メソッド実行の前後をフックします。
@PreExecute
が指定されている Action メソッドはその Action クラスが読み込んだ @PreExecuteMethod
アノテーション付きのメソッドを Action メソッドの実行よりも前に全て実行します。これは共通化された親クラスに定義されている @PreExecutionMethod
つきのメソッドも同様で、親に定義されているものが先に実行されます。これは Rails でいう before_action
に相当するものです。
public class IndexAction {
@PreExecuteMethod
public void sayHello() {
System.out.println("Hello!");
}
@PreExecute
@Execute(validator = false)
public String index() {
return "index.jsp";
}
}
@PreExecute
には before_action
の :only
/:except
指定と同等の設定を渡せるようになっています。includeMethods
と excludeMethods
に String[] 型でメソッド名を複数渡す感じです。これによってこの Action メソッドだけはこの事前処理をスキップしたい、というケースにも簡単に対応できるようになっています。
sample プロジェクトではこの Action クラスで実際に動作していることを確認できます。
@PostExecute
についても同様で、これは Rails の after_action
に相当するものです。説明は割愛します。
SAStruts の標準の機能だと Servlet Filter やインターセプタでこういった処理を実装するのですが、実行するかどうかを request path なりの何らかの条件で分岐する必要があったり、Action クラスが Filter やインターセプタによって request attribute に設定されているはずの値を期待する処理になったりと、何かと見通しが悪くなりがちでした。また SAStruts では Action クラスの public フィールドにリクエストパラメータが自動的にバインドされるのですが、これをいじる用途には対応できませんでした。
この before_action
/after_action
的な仕組みをつくることで、上記のような問題を解決でき、メンテもしやすくなりました。
AbstractService
次に紹介するのは S2JDBC の S2AbstractService
を少しだけ拡張した AbstractService
です。これはあまりたいしたものでもないので手短に。
これは見ていただけると分かる通り find 系のよく使うメソッドが多く生えている親クラスという感じです。PK のカラム名は id 決め打ちだったりしますが、新規でつくるテーブルはそのように統一されていたのでだいたいそんな感じになっていました。
例として sample プロジェクトの EmployeeService
を見てみます。
department と join している findAllWithDepartment
以外のメソッドは定義していませんが
一通りの簡単な CRUD 処理はできるようになっています。更新系は元々 S2AbstractService
にあるものですが。
@Resource
DepartmentService departmentService;
@Resource
EmployeeService service;
Department dept = new Department();
dept.name = "Engineering Group";
departmentService.insert(dept);
Employee alice = new Employee();
alice.name = "Alice";
alice.departmentId = dept.id;
service.insert(alice);
List<Employee> employees = service.findAll();
Employee employee = service.findById(alice.id);
Employee employee = service.findByIdForUpdate(alice.id);
service.delete(alice);
service.deleteIfExist(alice);
long count = service.countAll();
複数件取得するメソッド名は今見るとちょっと違和感あるものもありますが、簡単な用途は Service クラスを定義するだけで済んだので楽ができたように思います。
PostgreDialectQueryAnalyzer / TraceServiceQueryInterceptor
次は S2JDBC のクエリアナライザーの紹介です。エムスリーの本番サービスの RDBMS は基本的には PostgreSQL を使っているので PostgreSQL をターゲットにしたものをつくりました。
- https://github.com/m3dev/m3commons-s2/blob/master/src/main/java/com/m3/m3commons/s2/s2jdbc/analyzer/impl/PostgreDialectQueryAnalyzer.java
- https://github.com/m3dev/m3commons-s2/blob/master/src/main/java/com/m3/m3commons/s2/s2jdbc/interceptor/TraceServiceQueryInterceptor.java
これは開発中に Service クラスから出ている DB へのクエリを常に explain して sequential scan を見つけたら警告するという開発補助ツールです。デフォルトの挙動では例外を投げるようになっていますが、データ量が非常に少ない場合など必ずしも seq scan が無条件に NG というわけでもないので、基本的には loggingOnly のモードで使っていました。
実装としては S2 の SqlLogRegistry
で憶えておいてくれている実際に出たクエリを取り出してきて explain した結果に「Seq Scan」という文字列がいないか見ているというものです。
これによって開発中にインデックス作成忘れに気付きやすくなりましたし、debug ログを有効にすればユニットテストを実行するだけで簡単に実行計画を見ることができて重宝しました。
ちなみにこのアイデア自体は S2JDBC に限定されません。今年やったプロジェクトでは Scala のアプリケーションでも同じようなコードを書いたのですが、非常に有用でした。
SimpleS2TestCase
ここからは 3 つほどテスト関連です。まずは親となる SimpleS2TestCase
から。simple という命名はアンチパターン感がありますが、これは結構その名の通り普通に S2TestCase
を使うよりはシンプルにやれるように出来ていたのでは?と思います。
ハマリどころやお決まりのところを設定したり、 S2TestCaseがサポートしていないものを追加してより簡単にテストできるようにするのが目的です。
とコメントにありますが、ポイントとしては
- 先ほど紹介した QueryAnalyzer の自動実行をデフォルトでサポートする
-
Tx
をメソッド名につけなくてもデフォルトで全てのユニットテストで行った DB の更新処理を自動 rollback する(usingAutoRollback = false
で無効化も可能、予めTx
が付いているときは邪魔しない) - S2 コンテナから DI できなかった場合にモックライブラリを使って mock object にしておいてくれる(デフォルトは JMock2、必要に応じて JMock2 か Mockito を選択できる)
- テストクラスに
@Resource
つきで定義されているメンバ変数は S2 コンテナ初期化後に DI を試みて、失敗したら mock object にする -
S2TestCase
を拡張している関係上、JUnit 3 系に依存している
といったところが挙げられるかと思います。
基本的な動作として S2 コンテナから DI に失敗したオブジェクトを mock 化してあげることで、テストを書ける状態に手っ取り早く持っていけるので、テストを書くのが非常に楽になっています。
具体例として次の項で説明する SAStrutsActionTestCase
の例を挙げることにします。これは SimpleS2TestCase
を継承している基底クラスです。Action クラスのテストを書くとき、しばしば HttpServletRequest
が絡んだ処理のテストを書きたいケースがあります。もし、これを手動で都度 mock object にするようなことをやろうとするとそれなりに手間がかかりますし、そういう小さな手間はテストを書くことを億劫にさせる原因になりがちです。しかし、とりあえず自動で mock object にしておいてくれるなら、あとはこれの挙動を書くだけでよいのでずいぶん楽になります。
public class ProtectAction {
@Resource
protected HttpServletRequest request;
@Execute(validator = false)
public String index() {
// request を使って何かする
return "index.jsp";
}
}
この Action クラスのテストコードはこのように request
が mock object になっていることがわかります。
public class ProtectActionTest extends SAStrutsActionTestCase<ProtectAction> {
public void testMockingRequest() throws Exception {
assertNotNull(action.request);
}
}
また、このようなテストのサポートモジュールだけでなく、私がいたチームでは拙作の Eclipse プラグイン「JUnit Helper」でテストの自動生成もしていたので、それなりにテストを多く書いていたチームだったと思います。
SAStruts + S2JDBC の組み合わせの場合だと、やはり Action や Service のテストが大半にはなりますが、この SimpleS2TestCase
は任意の S2 コンポーネントをテストできる仕組みです。
例えば、当時チームでは非同期処理やちょっとしたバッチ処理を実行するために Runnable
インタフェースを実装した worker をつくり、それを Task
という名前で終わる命名することで S2 コンポーネントにして Service を DI させて実装するということをよくやっていましたが、この Task クラスをテストするとき、この SimpleS2TestCase
を継承するだけですぐにテストが書けるのは有用でした。
SAStrutsActionTestCase
次はその名の通り SAStruts の Action クラスのテストを簡単に書くための基底クラスです。SimpleS2TestCase
を継承しています。
このクラスは一言で言えば SAStrutsActionTestCase
のジェネリクスに指定した Action クラスを S2 コンポーネント解決済の状態でインスタンス化し action
というメンバ変数に設定するものです。
- https://github.com/m3dev/m3commons-s2/blob/master/sample/src/main/java/tutorial/action/EmployeesAction.java
- https://github.com/m3dev/m3commons-s2/blob/master/sample/src/test/java/tutorial/action/EmployeesActionTest.java
こんな感じで Action メソッド呼び出しをすぐにテストすることができます。
public class EmployeesActionTest extends SAStrutsActionTestCase<EmployeesAction> {
@Resource
EmployeeService service;
public void testIndex() throws Exception {
// service を使ってデータを事前準備しておいてから Action メソッド実行するなど..
String result = action.index();
assertEquals("index.jsp", result);
}
// テスト終了後、DB 操作は自動で rollback される
}
ジェネリクスから Action クラスを取得してインスタンス化するあたりは Seasar2 の本体にある GenericUtil
を利用することで簡単に実装できました。Action クラスをインスタンス化したら SimpleS2TestCase
のノリで @Resource
での自動 DI、setter injection を予め解決しておいてくれます。
先ほど説明したように HttpServletRequest
などもとりあえず mock object になっておいてくれるので、基本的にはすぐにテストが書き始められます。ただし @PreExecuteMethod
などは実行されないので、その動作確認は @PreExecuteMethod
をテストコード内で実行させるか E2E テストを書くという感じでした。
実際には Action クラスのテストは Hot reloading もあるので、ブラウザからアクセスして動作確認することが多かったですが、お金に絡むところなどはリグレッションテストが書きたいケースもあり、その場合には mock object も簡単に使いつつ Action クラスの実行時の状況をシミュレートできることは有用でした。限られた時間の中でテストを書く敷居を下げられたことは非常に良かったと思います。
S2JDBCServiceTestCase
次は主に S2JDBC で DB アクセス処理を行う Service クラスのテスト基底クラスです。
これもジェネリクスに指定された Service クラスを S2 コンポーネント解決済の状態でインスタンス化し service
というメンバ変数に設定しておいてくれます。
public class EmployeeServiceTest extends S2JDBCServiceTestCase<EmployeeService> {
@Resource
DepartmentService departmentService;
public void testExample() throws Exception {
long beforeCount = service.countAll();
Department dept = new Department();
dept.name = "Engineering Group";
departmentService.insert(dept);
Employee alice = new Employee();
alice.name = "Alice";
alice.departmentId = dept.id;
service.insert(alice);
assertEquals(beforeCount + 1, service.findAll().size());
}
}
利点はこれまでの繰り返しになるので割愛します。
SAStrutsSessionStateManagerImpl
最後に少しマイナーな機能ですが S2 の DB セッションリプリケーションが hot deploy 時にエラーになる挙動だったのを対処したものも軽く紹介します。
- http://s2container.seasar.org/2.4/ja/dbsession.html
- https://github.com/seasarorg/seasar2/blob/master/seasar2/s2-extension/src/main/java/org/seasar/extension/httpsession/impl/DbSessionStateManagerImpl.java
これは SerializedObjectHolder
のオブジェクトだったときに例外を投げてしまう問題だったのでそこを対処しています。今思えば本家に contribute すべき修正な気がしますが、この dbsession の機能はごく一部の管理画面のみで使っていたので、社内で解決して終わりにしていました。
まとめ
以上 m3commons-s2 の機能をご紹介しました。どうだったでしょうか?このテキスト量から私の Seasar2 への思い入れを感じていただけたはずですw 個人的には、久しぶりにこのコードを読むと色々と当時の情景を思い出したりして、感慨深いところもありました。
最後に繰り返しとなりますが、Seasar2 をつくってくださった皆様、本当にお疲れ様でした。