Aspect-oriented programming (AOP) ライブラリである AspectJ は JVM 環境においてメソッド呼び出しを横取りして任意の処理を挿入するために有用であり、フレームワークの実装としてもしばしば利用されています。
例えば Spring Framework の利用現場においても AspectJ (CGLIB proxy1) を利用して @Transactional
アノテーションによるトランザクションを実現している事例が少なからずあります 2。
広く使われている AspectJ は実際便利なのですが、それが有効なプロジェクトにおいて 「何時間も試行錯誤しても俺の @Transactional
が無視される...何故だ...!」 という悲痛な叫びを筆者はさまざまな現場で幾度となく聞きました。
Spring においては公式ドキュメント Chapter 6. Aspect Oriented Programming with Spring のチャプターを一通り読めばその背景も動作原理も取りうる選択肢も全てが分かるのですが、 「そんなことより俺の AOP を今すぐ動かしたい!!」 とお困りの方は以下をご参照ください:
あなたの AOP はどうして無視されるのか
1) Override 出来ないメソッド
以下の条件を満たす場合には AOP が効かないという症状が現れます:
- クラスまたはメソッドが
final
である -
static
メソッド
override できるようにしてあげましょう。
2) AspectJ によって生成されるラッパーのインスタンスを参照していない
ありがちなパターン:
-
this.メソッド()
で呼び出している -
return this;
したものに対してメソッド呼び出しが行われている -
new あなたのクラス()
している
なぜそうなるのか - AspectJ の動作原理
AspectJ は以下のステップによって AOP を実現しています:
- 元のインスタンスの class を継承する class を動的に生成、全てのメソッドを override する
- 上記の class を new してインスタンスを生成(元のインスタンスをラップするインスタンス)
- ラッパーのメソッドは、AOP の処理を実行した上で、元のインスタンスのメソッドを呼び出す 3
重要なのは、 AspectJ はあなたの書いた class を継承する別の class とそのインスタンス(ラッパー)を生成している という点です。
そのため、ラッパーのインスタンスに対してメソッド呼び出しをすれば AOP が働きますが、元の class のインスタンスのメソッドを呼び出してしまうと AOP は一切働きません。
対処法は、メソッドを呼び出す対象のインスタンスをフレームワーク(AspectJ をケアしている)から取得するようにすることです。
例えば、先述の 2)
のありがちなパターンの具体的対処法は以下:
-
this.メソッド()
で呼び出している- あなたの書いた class のメソッドにおける
this
はラッパーではなく、元のインスタンスです -
this
ではなく DI などで フレームワークから自分(のラッパー)のインスタンス を取得してそちらを呼び出しましょう (= AspectJ のラッパーが得られる)
- あなたの書いた class のメソッドにおける
-
return this;
したものに対してメソッド呼び出しが行われている- フレームワークから DI などで取得したインスタンスを使うようにし、
this
を露出するのはやめましょう
- フレームワークから DI などで取得したインスタンスを使うようにし、
-
new あなたのクラス()
している- インスタンスの生成をフレームワークに行わせましょう (Spring であれば Bean/Component として定義する)
まとめ
ここまでの説明を全て読んでいただいた方は既にご存知のように、 AspectJ が継承・override で生成したラッパーを経由しないでメソッドを呼び出すからダメ というシンプルな原理に帰着します。
世の中の記事などでは「あのケースでもこんなケースでもそんなケースでも AspectJ がうまく動かない」という説明・羅列の仕方がされていることがあり、それを見て「なんという複雑怪奇意味不明なライブラリなんだ...」と絶望されてしまっている人を幾度となく筆者は見たことがあるのですが、本質的にはただそれだけの問題です (ということを今後も説明することがありそうなので文章にまとめました)。
ここまでに触れた具体例そのものに直接該当しない場合も、本稿で触れました AspectJ の動作原理を思い浮かべていただけばきっと原因にたどり着けるのではないでしょうか。
おまけ: Spring の @Transactional
の rollbackFor
By default, a transaction will be rolling back on RuntimeException and Error but not on checked exceptions (business exceptions). See DefaultTransactionAttribute.rollbackOn(Throwable) for a detailed explanation.
本稿の主題とは無関係ですが、Spring の @Transactional
ではチェック例外(e.g. IOException
)がデフォルトではロールバック対象にはなっていないため、「例外が発生したのにトランザクションが commit されてしまった!」という話もよくあります。
そのため、明示的に rollbackFor
を指定するほうが混乱しない、というノウハウもあります。
-
Spring 系の公式ドキュメントでは "CGLIB proxy" と表現されていますが、指しているのは AspectJ です (GCLIB は AspectJ が内部で使っているコード生成ライブラリ名)。 ↩
-
Spring がデフォルトで使う java.lang.reflect.Proxy は interface のみに対する AOP が可能であり、その制約を嫌って AspectJ を使っている事例が少なからずあります ↩
-
対象メソッドが override できる必要があるというのはこのため ↩