Странное поведение (цикл) при использовании 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

  1. MailPublisherCommonEvent и AccountPublisherCommonEvent несколько subevent из BaseEvent
  2. createUserAccountPublisherCommon публикации событие типа AccountPublisherCommonEvent
  3. MailPublisherCommonEventListener зарегистрирован для обработки MailPublisherCommonEvent
  4. AccountPublisherCommonEventListener зарегистрирован для обработки BaseEvent и все под-события этого.
  5. 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. Надеюсь, в этом есть смысл.

спасибо.