LoginSignup
3
1

More than 3 years have passed since last update.

【Spring Framework】@Transactional のメソッドはクラス内から呼ぶとトランザクションが実行されない

Posted at

やりたかったこと

下記のような感じで、1 メソッドでトランザクションを 2 回かけたかったです。
公式ドキュメントを読むと、public method に @Transactional が使えると書いてあるので、1 メソッドからトランザクションをかけるメソッドをそれぞれ呼び出しても大丈夫かと思い試してみたところ、どうやらこのサービスクラスをプロパティに持つクラスから registerA registerB をそれぞれ呼び出さないとトランザクションは効きませんでした。


@Service
@RequiredArgsConstructor
public class HogeHogeServiceImpl implements HogeHogeService {

  private final HogeHogeRepository hogehogeRepository;
  private final FugaFugaRepository fugafugaRepository;

  @Override
  public void registerAB(List<HogeHoge> hogehogeList) {
    List<FuaFuga> fugafugaList = registerA(hogehogeList);
    registerB(fugafugaList);
  }

  @Override
  @Transactional
  public List<FugaFuga> registerA(List<HogeHoge> hogehoegList) {
    try {
        ...いろいろな処理

      hogehogeRepository.register(hogehogeList);

        ...いろいろな処理

      return createSuccessFugaFugaList();
    } catch (Exception e) {
      return createFailedFugaFugaList();
    }

  }

  @Override
  @Transactional
  public void registerB(List<FugaFuga> hogehoegList) {
     ...いろいろな処理

    fugafugaRepository.register(fugafugaList);

  }

  ...
}

@Transactional は Spring AOP を使ってる

なぜ @Transactional をメソッドにつけると、トランザクションが実行されるのでしょうか。
おそらく TransactionManager をどこかのタイミングで実行しているのだと思います。
registerA の処理の始めにコールスタックを吐き出すようにして、 registerA をサービスクラス外から呼び出して調べてみました。

  @Override
  @Transactional
  public List<FugaFuga> registerA(List<HogeHoge> hogehoegList) {
    Arrays.stream(Thread.currentThread().getStackTrace()).forEach(stackTraceElement -> {
      System.out.println("class name = " + stackTraceElement.getClassName());
      System.out.println("method name = " + stackTraceElement.getMethodName());
      System.out.println("-------------------------");
    });
  ...
  }

出力

class name = java.lang.Thread
method name = getStackTrace
-------------------------
class name = xxxx.xxxx.HogeHogeServiceImpl
method name = registerA
-------------------------
class name = xxxx.xxxx.HogeHogeServiceImpl$$FastClassBySpringCGLIB$$40a6490f
method name = invoke
-------------------------
class name = org.springframework.cglib.proxy.MethodProxy
method name = invoke
-------------------------
class name = org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation
method name = invokeJoinpoint
-------------------------
class name = org.springframework.aop.framework.ReflectiveMethodInvocation
method name = proceed
-------------------------
class name = org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation
method name = proceed
-------------------------
class name = org.springframework.transaction.interceptor.TransactionAspectSupport
method name = invokeWithinTransaction
-------------------------
class name = org.springframework.transaction.interceptor.TransactionInterceptor
method name = invoke
-------------------------
class name = org.springframework.aop.framework.ReflectiveMethodInvocation
method name = proceed
-------------------------
class name = org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation
method name = proceed
-------------------------
class name = org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor
method name = intercept
-------------------------
class name = xxxx.xxxx.HogeHogeServiceImpl$$EnhancerBySpringCGLIB$$8837d8e0
method name = registerA
-------------------------
class name = yyyy.yyyy.HogeHogeController
method name = run
-------------------------

これだけだとTransactionManager をどのタイミングで使っているか分かりません。
出力されるまでに、 org.springframework.aop.framework.CglibAopProxy が使われているのが分かりました。パッケージパスを見た限り Spring AOP を使ってそうです。

Spring AOP

Spring AOP は、アスペクト指向プログラミングを実現するフレームワークで、これにより Spring Bean でのクラス・メソッドの前後に処理を追加することができます。
例えば、すべてのコントローラのメソッドでリクエストを受け取るたびにリクエストの中身をログ出力する、といったことが簡単に実現できます。

Aspect Oriented Programming with Spring

@Transactional は Spring AOP を使っています。というのも、コールスタックを確認して呼び出されているクラス・メソッドを追わなくても、公式ドキュメントに説明が書いてありました。

Understanding the Spring Framework’s Declarative Transaction Implementation

@Transactionalを使うと、 AOP Proxy を介してトランザクションが有効になり TransactionInterceptor クラスによってトランザクション実行・コミット・ロールバックが制御されるようです。
トランザクションが有効にならないのは、AOP Proxy の制御が関係ありそうなので、AOP プロキシについて調べてみます。

AOP Proxy

Spring AOP は Dynamic Proxy または CGLIB を使って Proxy をベースに作られています。
インターフェースを実装したクラスでは Dynamic Proxy が使われるようです。
Proxy とは何なのか調べてみます。

Proxying Mechanisms

Proxy

Proxy とは Java標準クラスの java.lang.reflect.Proxy のことです。
Proxy を使うことで、特定のクラス・メソッドの前後に処理を追加することが可能です。
InvocationHandler の実装クラスを用意して、対象のクラス・メソッドの前後での処理をメソッドで書きます。
handler を引数で渡して、Proxy を使って対象のクラスをインスタンス生成してメソッド呼び出すと、対象のメソッドに加えて前後の処理が実行されます。

public class ProxySample {

  public static void main(String[] args) {
    HogeHogeInvocationHandler handler = new HogeHogeInvocationHandler(new HogeHogeImpl());
    HogeHoge hogeHoge = (HogeHoge) Proxy
        .newProxyInstance(HogeHoge.class.getClassLoader(), new Class<?>[]{HogeHoge.class}, handler);
    hogeHoge.run();
  }

}

interface HogeHoge {

  void run();
}

class HogeHogeImpl implements HogeHoge {

  @Override
  public void run() {
    System.out.println("hoge hoge");
  }
}

class HogeHogeInvocationHandler implements InvocationHandler {

  private HogeHoge hogeHoge;

  HogeHogeInvocationHandler(HogeHoge hogeHoge) {
    this.hogeHoge = hogeHoge;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("before");
    Object o = method.invoke(this.hogeHoge, args);
    System.out.println("after");
    return o;
  }
}

出力

before
hoge hoge
after

以下のように、public method から別の public method を呼び出すようにしても、クラス内から呼び出した場合は proxy を介さないので、前後の処理は run2 では実行されません。

class HogeHogeImpl implements HogeHoge {

  @Override
  public void run() {
    System.out.println("hoge hoge");
    run2();
  }

  @Override
  public void run2() {
    System.out.println("hoge hoge 2");
  }
}

出力

before
hoge hoge
hoge hoge 2
after

Spring AOP もこの Proxy 同様に、前後の処理を呼び出すのに AOP Proxy を介さないといけないので、クラス内で @Transactional がついたメソッドを呼び出してもトランザクションは有効になりません。

Understanding AOP Proxies

まとめ

  • @Transactional は Spring AOP を、つまり Dynamic Proxy (または CGLIB) を使ってる
  • Proxy によるメソッドの前後での処理を実行したいなら、クラス内から対象のメソッドを呼び出すのではなく、DIした他クラスから呼び出さないといけない
  • なので、それぞれのメソッドでトランザクションを実行したいなら、DIされた他クラスからそれぞれ呼び出すか、別クラスに移してそれぞれ呼び出すと良い
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1