Сегодня мы с коллегой потратили два часа на поиск странной ошибки в нашем приложении Spring Boot. Причина была настолько интересной, что я должен написать об этом здесь. Поскольку я, очевидно, не могу писать здесь о проектах клиентов, я представляю проблему, используя вместо этого пример приложения. Итак, поехали.
Допустим, у нас есть приложение, управляемое доменом, с двумя агрегатами: Порядок и Счет-фактура . У нас также есть оркестратор, который автоматически должен создавать новые счета-фактуры, как только заказ переходит в состояние ОТПРАВЛЕНО .
Мы начинаем со следующего сервиса приложений для создания новых заказов:
@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);
});
}
}
Метод Order.ship() изменит состояние заказа на ОТПРАВЛЕНО и опубликует Order StateChangedEvent .
Затем нам понадобится другое приложение сервис для создания новых счетов фактур:
@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);
}
}
}
Там! Теперь мы просто запускаем приложение, создаем новый заказ, отправляем его и… счет-фактура не создается. В журнале также нет никаких исключений. Так что же пошло не так?
Оказывается, проблема заключается в @Transactioneventlistener . По умолчанию он настроен на запуск после фиксации транзакции. Это именно то, чего мы хотим, но есть предостережение в том, как Spring на самом деле реализует это.
События домена публикуются с помощью обычного издателя событий приложения. Spring на самом деле также перехватит их с помощью обычного @EventListener . Однако вместо прямого вызова прослушивателя транзакционных событий Spring зарегистрирует Синхронизация транзакций с помощью TransactionSynchronizationManager . Это вызовет прослушиватель событий транзакций после успешной фиксации транзакции, но до того, как диспетчер синхронизации транзакций очистит себя.
Теперь наш прослушиватель событий вызывает метод createInvoiceForOrder , который имеет аннотацию @Transactional . Распространение по умолчанию для @Transactional равно ТРЕБУЕТСЯ . Это означает, что если уже существует активная транзакция, метод должен участвовать в ней; в противном случае он должен создать свою собственную транзакцию.
Поскольку этот метод вызывается внутри Синхронизация транзакций , на самом деле существует “активная” транзакция, но она уже зафиксирована. Таким образом, вызов saveAndFlush приведет к TransactionRequiredException . Это исключение проглатывается TransactionSynchronizationUtils (еще один весенний урок) и регистрируется с использованием уровня ОТЛАДКИ. Таким образом, единственный способ обнаружить это исключение – включить ведение журнала ОТЛАДКИ для пакета org.springframework.transaction.support .
Решение этой проблемы состоит в том, чтобы убедиться что Служба выставления счетов всегда выполняется внутри своей собственной транзакции. Итак, мы меняем метод следующим образом:
@Service
public class InvoiceService {
@Transactional(propagation = REQUIRES_NEW)
Long createInvoiceForOrder(Order order) {
// Rest of the method omitted
}
}
В любом случае рекомендуется настроить все ваши службы приложений на всегда использовать REQUIRES_NEW поскольку они отвечают за контроль транзакций .
Теперь мы снова запускаем приложение и… это все равно не работает. Приложение ведет себя точно так же. Что теперь не так?
Получается, что createInvoiceForOrder на самом деле вообще не выполняется внутри транзакции. Транзакция запускается и фиксируется вызовом saveAndFlush() в репозитории, и этот метод по-прежнему использует ТРЕБУЕТСЯ распространение транзакций. Как так вышло?
Служба Invoice Service не реализует никаких интерфейсов, поэтому Spring использует прокси-сервер CGLIB для добавления перехватчиков транзакций. Однако метод createInvoiceForOrder имеет видимость пакета, а перехватчик транзакций применяется только к общедоступным методам. Поэтому нам нужно изменить метод, чтобы он был общедоступным:
@Service
public class InvoiceService {
@Transactional(propagation = REQUIRES_NEW)
public Long createInvoiceForOrder(Order order) {
// Rest of the method omitted
}
}
Теперь мы запускаем приложение еще раз, и оно, наконец, работает!
Оригинал: “https://dev.to/peholmst/knee-deep-in-spring-boot-transactional-event-listeners-and-cglib-proxies-1il9”