第7回 Springの宣言的トランザクションのしくみ【AOP】

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

【求人のご案内】Javaアプリ開発エンジニア募集

よく訓練されたアップル信者、都元です。長雨が続いたと思ったらこの晴れ間。夏っすね。止まってしまわないうちにガンガン書いて行こうと思います。

Springによるトランザクションの実現方法を理解する

さて前回、@Transactionalというアノテーションを付与することにより、宣言的にトランザクション制御を記述できることを学びました。

@Autowired
UserRepository userRepos;

@Transactional
public void execute() {
// 成功するDB書き込み操作
userRepos.save(new User("torazuka", "$2a$10$fx33wHST4ecwp53MB5QvROQtIYwkdCU2O3XJK6LuCmm415dRncluC"));
// からの失敗
throw new RuntimeException();
}

つまり、@Transactionalが付いたメソッドを呼ぶ前後に、トランザクションの開始やコミットの処理が黒魔術として動いているわけです。このようにメソッド呼び出しの前後等に一般化した共通処理を差し込むプラグラミング方式をAOP(Aspect oriented programming=アスペクト指向プログラミング)と呼びます。

さて実際この時、何が起こっているのか、その一端を見てみましょう。context.getBeanの直後、System.out.println(main);でインスタンスの詳細を覗き見してみます。この結果は

jp.classmethod.example.berserker.DataAccessSample@5167f57d

という感じでDataAccessSampleクラスのインスタンスであることを期待したと思いますが、実際は

jp.classmethod.example.berserker.DataAccessSample$$EnhancerBySpringCGLIB$$e8b1a238@5167f57d

などという結果が帰ってきます。DataAccessSample$$EnhancerBySpringCGLIB$$e8b1a238がクラス名です。これはプロキシと呼ばれるインスタンスです。

BerserkerApplicationクラスのインスタンスに対して素朴にexecuteメソッドを呼んだ場合、@Transactionalの有無にかかわらず、素朴にメソッドの中身が実行されてしまい、トランザクション制御どころではありません。

そうではなく、例えばこのような派生クラスを作ることによって、execute本体を実行する前後に任意の処理を挟み込むことが可能になりますね。

public class DataAccessSample$$EnhancerBySpringCGLIB$$e8b1a238
extends BerserkerApplication {

private BerserkerApplication delegate;

public BerserkerApplication$$EnhancerBySpringCGLIB$$e8b1a238(BerserkerApplication delegate) {
this.delegate = delegate;
}

@Overwrite
public void execute() {
// 前
delegate.execute();
// 後
}
}

そしてDataAccessSample$$EnhancerBySpringCGLIB$$e8b1a238のインスタンスはDataAccessSampleを継承しているため、利用者はあたかもDataAccessSampleを素で触っているような錯覚を起こしながら、これらの機能を享受できる、という仕組みです。

「いや、でも俺DataAccessSampleの派生クラスなんて作ってないし」と思うかもしれません。素朴なJavaの世界では、自分の作ったクラスの派生クラスは、自分が書かない限り存在しないはずでした。しかし、cglib(Code Generation Library)等のJavaライブラリを使えば、(コンパイル時ではなく)実行時に新しいクラスを作り出し、そのインスタンスを生成したりできます。

これが黒魔術の正体です。

プロキシによるAOPの弱点

さて、次のようなコードを実行してみます。

public void execute() {
execute0();
}

@Transactional
private void execute0() {
// 成功するDB書き込み操作
userRepos.save(new User("torazuka", "$2a$10$fx33wHST4ecwp53MB5QvROQtIYwkdCU2O3XJK6LuCmm415dRncluC"));
}

結果はUserRepository#save内での例外となりました。

java.lang.IllegalStateException: It seems not to be existing a transaction.

前提として、UserRepository#saveメソッドはトランザクションの中でしか呼べません。例えば@Transactionalの付け忘れなど、トランザクション外で呼ぶと、このような例外が発生します。これはSpring連携した際のMirageの制限事項です。さて今回はきちんとアノテーションを付けているはずなのに、なぜトランザクションの中にいないと怒られてしまったのでしょうか。

落ち着いて、頭の中でこのクラスのサブクラスを作って、execute0メソッドの@Transactional処理をどのように実行時に実現するのか想像してみてください。……privateメソッドのオーバーライドはできない!? ということに気づきましたか?

ではこれがpublicであれば問題ないのでしょうか。さらに想像してみてください。proxyのexecute0がオーバーライドされていても…

  1. proxyのexecuteが呼ばれる
  2. delegateのexecuteが呼ばれる
  3. delegateのexecute0が呼ばれる
  4. userReposのsaveが呼ばれる

おっと、proxyのexecute0が呼ばれるタイミングが無く、トランザクションの魔法は掛かりません。

これがプロキシによるAOPの弱点です。

まとめ

この弱点は、AOPを「プロキシによって」ではなく、「ウィービング(織り込み)によって」実現することによって克服できたりするようです。ウィービングはAspectJと呼ばれるあれやこれやを使います。しかしひとまず私自身は、この弱点を理解して付き合っていくという方針を取っており、AspectJで頑張ったことがありません。

これまでの経験上、「アスペクトの掛かったメソッドを自分自身のクラスの中から呼ぶ」という行為にさえ気をつけていれば(たまに不便なこともありますが)ほとんどの問題は回避可能です。