春のブートで深い膝、トランザクションのイベントリスナーとCglibプロキシ


同僚と私は今日の春のブートアプリケーションで奇妙なバグを追跡2時間を過ごした.原因はとても面白かったので、ここでそれについて書かなければなりません.私は明らかにここで顧客プロジェクトについて書くことができないので、私は代わりにサンプルアプリケーションを使用して問題を提示しています.だからここに行く.
つの集合を持つドメイン駆動アプリケーションを持っているとしましょう.Order and Invoice . 我々はまた、自動的に新しい請求書を作成する必要がありますオーケストラを持って1回の順序遷移SHIPPED 状態.
次のアプリケーションサービスで新しい注文を作成します.
@Service
public class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional
    public Long createOrder() {
        var order = new Order();
        return orderRepository.saveAndFlush(order).getId();
    }

    @Transactional
    public void shipOrder(Long orderId) {
        orderRepository.findById(orderId).ifPresent(order -> {
            order.ship();
            orderRepository.saveAndFlush(order);
        });
    }
}
The Order.ship() メソッドは、SHIPPED そして、OrderStateChangedEvent .
次に、新しい請求書を作成するための別のアプリケーションサービスが必要です.
@Service
public class InvoiceService {
    private final InvoiceRepository invoiceRepository;

    InvoiceService(InvoiceRepository invoiceRepository) {
        this.invoiceRepository = invoiceRepository;
    }

    @Transactional
    Long createInvoiceForOrder(Order order) {
        var invoice = new Invoice(order);
        return invoiceRepository.saveAndFlush(invoice).getId();
    }
}
最後に、注文が出荷されたときにインボイスを作成するオーケストラが必要です.
@Component
class InvoiceCreationOrchestrator {

    private final OrderRepository orderRepository;
    private final InvoiceService invoiceService;

    InvoiceCreationOrchestrator(OrderRepository orderRepository, InvoiceService invoiceService) {
        this.orderRepository = orderRepository;
        this.invoiceService = invoiceService;
    }

    @TransactionalEventListener
    public void onOrderStateChangedEvent(OrderStateChangedEvent event) {
        if (event.getNewOrderState().equals(OrderState.SHIPPED)) {
            orderRepository
                .findById(event.getOrderId())
                .ifPresent(invoiceService::createInvoiceForOrder);
        }
    }
}
そら!今、私たちは、アプリケーションを実行すると、新しい順序を作成し、それを出荷します.請求書は作成されません.ログには例外はありません.それで、何が間違ったのか?
それは問題があることを@TransactionalEventListener . トランザクションがコミットされた後に実行するのはデフォルトで設定されます.これはまさに我々が望むものですが、春が実際にこれを実行する方法に注意があります.
ドメインイベントは、通常のアプリケーションイベント発行者を使用して発行されます.春は実際に普通を使ってキャッチします@EventListener 同様に.ただし、トランザクションイベントリスナーを直接呼び出す代わりに、TransactionSynchronizationTransactionSynchronizationManager . トランザクションが正常にコミットされた後、トランザクション同期マネージャがクリーンアップしている前にトランザクションイベントリスナーを呼び出します.
今、我々のイベントリスナーはcreateInvoiceForOrder メソッド@Transactional 注釈.のデフォルト伝播@Transactional is REQUIRED . つまり、アクティブなトランザクションが既に存在する場合、メソッドはそれに参加する必要がありますそれ以外の場合は、独自のトランザクションを作成する必要があります.
このメソッドはTransactionSynchronization , 実際には“アクティブ”トランザクションが、すでにコミットされています.したがって、saveAndFlushTransactionRequiredException . この例外はTransactionSynchronizationUtils (別のSpringクラス)、デバッグレベルを使用してログインします.したがって、この例外を検出する唯一の方法は、org.springframework.transaction.support パッケージ.
この問題の解決策はInvoiceService 常に自分のトランザクションの中で実行します.そこで、以下のような方法を変更します.
@Service
public class InvoiceService {
    @Transactional(propagation = REQUIRES_NEW)
    Long createInvoiceForOrder(Order order) {
      // Rest of the method omitted
    }
}
それはとにかく良いすべてのアプリケーションサービスを常に使用するように設定することですREQUIRES_NEW 彼らは責任があるので.
これでアプリケーションを再度実行します.まだ動作しません.アプリケーションは全く同じ動作します.今何が悪いのですか.
それがわかるcreateInvoiceForOrder はトランザクション内で実際に実行されていません.トランザクションが開始され、呼び出しによってコミットsaveAndFlush() リポジトリでは、そのメソッドはREQUIRED トランザクション伝搬どうして?
The InvoiceService はインターフェースを実装していないので、Springはトランザクションインターセプタを追加するためにCGlibプロキシを使用しています.しかしながら、方法createInvoiceForOrder パッケージの可視性があり、トランザクションインターセプターはパブリックメソッドにのみ適用されます.したがって、メソッドをpublicに変更する必要があります.
@Service
public class InvoiceService {
    @Transactional(propagation = REQUIRES_NEW)
    public Long createInvoiceForOrder(Order order) {
      // Rest of the method omitted
    }
}
今、我々はもう一度アプリケーションを実行し、それが最終的に動作する!