大まかなコードのイメージ
package com.example.app.application import com.example.app.domain.Sample import com.example.app.domain.SampleRepository import org.springframework.aop.framework.AopContext import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.interceptor.TransactionAspectSupport @Service class SampleService( private val otherService: OtherService, private val fallbackService: FallbackService, ) { @Transactional fun handle() { try { // ここの呼び出しがロールバックするなら処理を保存しときたい // 実際は他にもトランザクションを使ったなんらかの処理 otherService.handle() } catch (e: RuntimeException) { // UnexpectedRollbackExceptionにならないように明示的に指定する必要がある val transactionStatus = TransactionAspectSupport.currentTransactionStatus() transactionStatus.setRollbackOnly() fallbackService.handle() } } } @Service class OtherService { @Transactional fun handle() { // 実際はなんらかの処理 throw RuntimeException("runtime error") } } @Service class FallbackService( private val repository: SampleRepository, ) { @Transactional(propagation = Propagation.REQUIRES_NEW) fun handle() { // 実際はデータ受け取って書き込むだけ val sample = Sample.of() repository.save(sample) } }
なんかこの為だけに別クラス作って呼び出したり、EventPublisher経由で別メソッドを呼び出したりするのがイマイチだなあと思ってたらAOPで解決する方法もあるとのことだった
www.youtube.com
詳解Springトランザクション -初級から上級まで- #jsug | ドクセル
別メソッドで Propagation.REQUIRES_NEW
しているなら、トランザクション別で自動的に貼り直してくれれば良いのに
と思っていたらAOPの仕組み的に SampleService#handle
に入る際にProxy経由で呼び出されて、
SampleService#handle
内で 別メソッドを呼び出しても割り込まれずに直接呼び出される(Annotationが効かない)という話っぽい
この辺ちゃんと頭の中でイメージ出来ていないと、なんで書いた通りに呼び出されないんだ???となるのでとても良い資料だった
解説されていたサイトの方、どうやってProxy経由で呼び出しているのかちょっとだけ見てみた
- Configurationの設定でThreadLocalにProxyを公開しているらしい
- https://github.com/spring-projects/spring-framework/blob/43031509c847eb17754af3df0dc0a180199cf183/spring-context/src/main/java/org/springframework/context/annotation/AspectJAutoProxyRegistrar.java#L53-L55
- https://github.com/spring-projects/spring-framework/blob/main/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java#L113
- https://github.com/spring-projects/spring-framework/blob/43031509c847eb17754af3df0dc0a180199cf183/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java#L50
- exposeProxyを最終的に誰が読み取ってsetCurrentProxy呼ぶのかと思ったらCglibAopProxy辺りが呼び出しそう
あとは SampleService#handle
から SampleService(Proxy経由)#別メソッド
を呼びだしている、ということっぽい