Странное поведение (цикл) при использовании Spring @TransactionalEventListener для публикации события
у меня странная проблема, которая включает @TransactionalEventListener
не стрельба правильно или поведение, как ожидалось, когда срабатывает другой @TransactionalEventListener
.
общий поток:
- AccountService опубликовать событие (AccountEventListener)
- AccountEventListener прослушивает событие
- выполните некоторую обработку, а затем опубликуйте другое событие (в MailEventListener)
- MailEventListener прослушивает событие и peform некоторые обработка
Итак, вот классы (выдержка).
public class AccountService {
@Transactional
public User createAccount(Form registrationForm) {
// Some processing
// Persist the entity
this.accountRepository.save(userAccount);
// Publish the Event
this.applicationEventPublisher.publishEvent(new RegistrationEvent());
}
}
public class AccountEventListener {
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public MailEvent onAccountCreated(RegistrationEvent registrationEvent) {
// Some processing
// Persist the entity
this.accountRepository.save(userAccount);
return new MailEvent();
}
}
public class MailEventListener {
private final MailService mailService;
@Async
@EventListener
public void onAccountCreated(MailEvent mailEvent) {
this.mailService.prepareAndSend(mailEvent);
}
}
этот код работает, но я намерен использовать @TransactionalEventListener
в своем MailEventListener
класса. Следовательно, момент, когда я меняюсь с @EventListener
to @TransactionalEventListener
на MailEventListener
класса. MailEvent не запускается.
public class MailEventListener {
private final MailService mailService;
@Async
@TransactionalEventListener
public void onAccountCreated(MailEvent mailEvent) {
this.mailService.prepareAndSend(mailEvent);
}
}
MailEventListener
никогда не был спровоцирован. Поэтому я пошел посмотреть Весна Документации, и в нем говорится, что @Async @EventListener
не поддерживает событие, которое публикуется возвращением другое событие. И поэтому я перешел на использование ApplicationEventPublisher
в своем AccountEventListener
класса.
public class AccountEventListener {
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void onAccountCreated(RegistrationEvent registrationEvent) {
// Some processing
this.accountRepository.save(userAccount);
this.applicationEventPublisher.publishEvent(new MailEvent());
}
}
как только я перешел на вышеуказанное, мой MailEventListener
теперь будет забрать событие, которое отправляется из AccountEventListener
но веб-страница зависает, когда форма отправлена, и через некоторое время она выдает некоторое исключение, а затем она также отправила мне около 9 того же письма на мою учетную запись электронной почты.
я добавил несколько журналов и узнал, что мой AccountEventListener
(this.accountRepository.save()
) фактически побежал 9 раз перед ударом исключения, что тогда вызывает мой MailEventListener
для выполнения 9 раз я считаю, и именно поэтому я получил 9 писем в своем почтовом ящике.
вот журналы сайт Pastebin.
я не уверен, почему и что заставляет его работать 9 раз. В моих методах нет цикла или чего-то еще, будь то в AccountService
, AccountEventListener
или MailEventListener
.
спасибо!
2 ответов
поэтому я пошел посмотреть весеннюю документацию, и в ней говорится, что @Async @EventListener не поддерживает событие, которое публикуется возвращением другого события. И поэтому я перешел на использование ApplicationEventPublisher в моем классе AccountEventListener.
ваше понимание неверно.
эта функция не поддерживается для асинхронных слушатели.
это не значит
в нем говорится, что @Async @EventListener не поддерживает событие, опубликованное возвращением другого события.
это значит:
эта функция не поддерживает возврат событий из @Async @EventListener.
настройки:
@Async
@TransactionalEventListener
public void onAccountCreated(MailEvent mailEvent) {
this.mailService.prepareAndSend(mailEvent);
}
не работает потому что, как говорится в документе:
если событие не опубликовано в границах управляемой транзакции, событие отбрасывается, если явно не установлен флаг fallbackExecution (). Если транзакция выполняется, событие обрабатывается в соответствии с ее TransactionPhase.
если вы используете отладку, вы можете видеть, что если ваше событие возвращается из прослушивателя событий, это происходит после фиксации транзакции, следовательно, событие отбрасывается.
Итак, если вы установите fallbackExecution = true
Как заявил в документе ваше мероприятие будет правильно прослушано:
@Async
@TransactionalEventListener(fallbackExecution = true)
public void onAccountCreated(MailEvent mailEvent) {
this.mailService.prepareAndSend(mailEvent);
}
повторное поведение выглядит как некоторое поведение повторной попытки, соединение встало в очередь, исчерпало пул и выбросило исключение. Если вы не предоставите минимальный исходный код для воспроизведения проблемы, я не смогу ее идентифицировать.
обновление
читая ваш код, Основная причина теперь ясна.
посмотрите на свою настройку для POST /registerPublisherCommon
-
MailPublisherCommonEvent
иAccountPublisherCommonEvent
несколько subevent изBaseEvent
-
createUserAccountPublisherCommon
публикации событие типаAccountPublisherCommonEvent
-
MailPublisherCommonEventListener
зарегистрирован для обработкиMailPublisherCommonEvent
-
AccountPublisherCommonEventListener
зарегистрирован для обработкиBaseEvent
и все под-события этого. -
AccountPublisherCommonEventListener
выпускаетMailPublisherCommonEvent
(который также являетсяBaseEvent
).
Читать 4 + 5 вы увидите причину: AccountPublisherCommonEventListener
опубликовывает MailPublisherCommonEvent
который также обрабатывается сам по себе, следовательно, бесконечное обработка событий.
чтобы решить эту проблему, просто сузьте тип события, которое он может обрабатывать, как вы это сделали.
Примечание
настройки для MailPublisherCommonEvent
работает независимо от fallbackExecution
флаг, потому что вы публикуете его INSIDE A TRANSACTION
, а не OUTSIDE A TRANSACTION
(по возвращении из прослушивателя событий), как вы указали в своем вопросе.
для чего это стоит, я узнал, что вызывает цикл и как его решить, но я все еще не могу понять, почему это происходит как таковое.
и поправьте меня, если я ошибаюсь, параметр fallbackExecution = true
- это не ответ на вопрос.
на основе Spring документации, the event is processed according to its TransactionPhase.
так у меня было @Transactional(propagation = Propagation.REQUIRES_NEW)
в своем AccountEventListener
класс, который должен быть транзакцией сам по себе, и MailEventListener
должен выполняться только в том случае, если фаза, которая по умолчанию AFTER_COMMIT
для @TransactionalEventListener
.
я настраиваю git, чтобы воспроизвести проблему, и при этом позволяет мне узнать, что действительно пошло не так. Сказав это, я все еще не понимаю коренную причину этого.
прежде чем я это сделаю, есть некоторые вещи, в которых я не уверен на 100%, но это просто моя догадка/понимание в данный момент.
как говорится в Весна Документации,
если событие не опубликовано в пределах управляемая транзакция событие отбрасывается, если явно не установлен флаг fallbackExecution (). Если транзакция выполняется, событие обрабатывается в соответствии с ее TransactionPhase.
и моя догадка о причине, почему MailEventListener
класс не забрал событие при использовании события в качестве возвращаемого типа для автоматической публикации Spring, поскольку оно публикуется за пределами границ управляемой транзакции. Поэтому, если вы установите (fallbackExecution = true)
на MailEventListener
, это будет работа / запуск, потому что не имеет значения, входит ли он в транзакцию или нет.
Примечание: классы, упомянутые выше, взяты из моего первоначального поста. Этот классы ниже названы немного по-разному, но по существу, все все то же самое, только другое имя.
теперь вернемся к тому моменту, когда я сказал, что нашел ответ на вопрос, почему он вызывает цикл.
В принципе, это когда параметр, помещенный в прослушиватель, является BaseEvent
of род.
предположим, что у меня есть следующие классы:
public class BaseEvent {
private final User userAccount;
}
public class AccountPublisherCommonEvent extends BaseEvent {
public AccountPublisherCommonEvent(User userAccount) {
super(userAccount);
}
}
public class MailPublisherCommonEvent extends BaseEvent {
public MailPublisherCommonEvent(User userAccount) {
super(userAccount);
}
}
и классы слушателей (Notice that the parameter is the BaseEvent)
:
public class AccountPublisherCommonEventListener {
private final AccountRepository accountRepository;
private final ApplicationEventPublisher eventPublisher;
// Notice that the parameter is the BaseEvent
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void onAccountPublisherCommonEvent(BaseEvent accountEvent) {
User userAccount = accountEvent.getUserAccount();
userAccount.setUserFirstName("common");
this.accountRepository.save(userAccount);
this.eventPublisher.publishEvent(new MailPublisherCommonEvent(userAccount));
}
}
public class MailPublisherCommonEventListener {
@Async
@TransactionalEventListener
public void onMailPublisherCommonEvent(MailPublisherCommonEvent mailEvent) {
log.info("Sending common email ...");
}
}
в основном, если настройка слушателя как таковая (выше), то вы вводите цикл и нажимаете исключение, как упоминалось в предыдущем плакате.
повторное поведение выглядит как некоторое поведение повторной попытки, соединение встало в очередь, исчерпало пул и выбросило исключение.
и решите проблему, просто измените входные данные и определите классы для прослушивания с помощью (Notice the addition of ({AccountPublisherCommonEvent.class}))
:
public class AccountPublisherCommonEventListener {
private final AccountRepository accountRepository;
private final ApplicationEventPublisher eventPublisher;
// Notice the addition of ({AccountPublisherCommonEvent.class})
@TransactionalEventListener({AccountPublisherCommonEvent.class})
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void onAccountPublisherCommonEvent(BaseEvent accountEvent) {
User userAccount = accountEvent.getUserAccount();
userAccount.setUserFirstName("common");
this.accountRepository.save(userAccount);
this.eventPublisher.publishEvent(new MailPublisherCommonEvent(userAccount));
}
}
альтернативой будет изменение параметра на фактическое имя класса вместо BaseEvent
класс, я полагаю. И нет никаких изменений, необходимых для MailPublisherCommonEventListener
делая это, он больше не цикл, ни попал в исключение. Поведение будет работать так, как я хочу и ожидал.
Я был бы признателен, если бы кто-нибудь мог ответить на вопрос, почему это произойдет точно, если я ставлю BaseEvent
поскольку вход вызвал бы цикл. Вот ссылка на git для poc. Надеюсь, в этом есть смысл.
спасибо.