やりたかったこと
下記のような感じで、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 とは何なのか調べてみます。
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
がついたメソッドを呼び出してもトランザクションは有効になりません。
まとめ
-
@Transactional
は Spring AOP を、つまり Dynamic Proxy (または CGLIB) を使ってる - Proxy によるメソッドの前後での処理を実行したいなら、クラス内から対象のメソッドを呼び出すのではなく、DIした他クラスから呼び出さないといけない
- なので、それぞれのメソッドでトランザクションを実行したいなら、DIされた他クラスからそれぞれ呼び出すか、別クラスに移してそれぞれ呼び出すと良い