Вступление
Как вы, вероятно, уже знаете (например, из моего предыдущего поста в блоге ), больше не нужно создавать отдельный класс, реализующий ApplicationListener
с методом onApplicationEvent
чтобы иметь возможность реагировать на события приложения (как из самой Spring Framework, так и из наших собственных событий домена). Начиная с Spring 4.2 была добавлена поддержка обработчиков событий, управляемых аннотациями. Достаточно использовать @EventListener
на уровне метода, который автоматически зарегистрирует соответствующий ApplicationListener
:
1
2
3
4
|
@EventListener public void blogAdded(BlogAddedEvent blogAddedEvent) { externalNotificationSender.blogAdded(blogAddedEvent); } |
Обратите внимание, что использование доменных объектов в событиях имеет существенный недостаток и не является лучшей идеей во многих ситуациях. Псевдодоменные объекты в примерах кода использовались, чтобы не вносить ненужную сложность.
События, связанные с транзакцией
Просто и компактно. Для «стандартных» событий все выглядит отлично, но в некоторых случаях необходимо выполнить некоторые операции (обычно асинхронные) сразу после того, как транзакция была зафиксирована (или откатана). Что тогда? Можно ли использовать новый механизм?
Бизнес-требования
Во-первых, небольшое отступление — требования бизнеса. Давайте представим себе супер-необычный сервис агрегации блогов. Событие генерируется каждый раз, когда добавляется новый блог. Подписанные пользователи могут получить SMS или push-уведомление. Событие может быть опубликовано после того, как объект блога планируется сохранить в базе данных. Однако в случае сбоя фиксации / сброса (нарушение ограничений базы данных, проблема с генератором идентификаторов и т. Д.) Вся транзакция БД будет откатываться. Много недовольных пользователей с испорченными уведомлениями появятся у дверей …
Технические неполадки
В современном подходе к управлению транзакциями транзакции настраиваются декларативно (например, с @Transactional
аннотации @Transactional
), и фиксация запускается в конце области транзакции (например, в конце метода). В целом это очень удобно и намного менее подвержено ошибкам (чем программный подход). С другой стороны, фиксация (или откат) выполняется автоматически вне нашего кода, и мы не можем реагировать «классическим способом» (т.е. публиковать событие в следующей строке после transaction.commit()
).
Старая школа реализации
Одно из возможных решений для Spring (и очень элегантное) было представлено незаменимым Томеком Нуркевичем . Он использует TransactionSynchronizationManager
для регистрации синхронизации транзакции для текущего потока. Например:
01
02
03
04
05
06
07
08
09
10
11
12
|
@EventListener public void blogAddedTransactionalOldSchool(BlogAddedEvent blogAddedEvent) { //Note: *Old school* transaction handling before Spring 4.2 - broken in not transactional context TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronizationAdapter() { @Override public void afterCommit() { internalSendBlogAddedNotification(blogAddedEvent); } }); } |
Переданный код выполняется в нужном месте рабочего процесса транзакции Spring (в этом случае «сразу» после коммита).
Чтобы обеспечить поддержку выполнения в нетранзакционном контексте (например, в интеграционных тестовых случаях, которые не могли заботиться о транзакциях), его можно расширить до следующей формы, чтобы не вызывать сбоев с java.lang.IllegalStateException: Transaction synchronization is not active
исключением:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
@EventListener public void blogAddedTransactionalOldSchool( final BlogAddedEvent blogAddedEvent) { //Note: *Old school* transaction handling before Spring 4.2 //"if" to not fail with "java.lang.IllegalStateException: Transaction synchronization is not active" if (TransactionSynchronizationManager.isActualTransactionActive()) { TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronizationAdapter() { @Override public void afterCommit() { internalSendBlogAddedNotification(blogAddedEvent); } }); } else { log.warn( "No active transaction found. Sending notification immediately." ); externalNotificationSender.newBlogTransactionalOldSchool(blogAddedEvent); } } |
С этим изменением в случае отсутствия активной транзакции предоставленный код выполняется немедленно. Пока работает отлично, но давайте попробуем добиться того же с помощью обработчиков событий, управляемых аннотациями, в Spring 4.2.
Spring 4.2+ реализация
В дополнение к @EventListener
Spring 4.2 предоставляет еще одну аннотацию @TransactionalEventListener
.
1
2
3
4
|
@TransactionalEventListener public void blogAddedTransactional(BlogAddedEvent blogAddedEvent) { externalNotificationSender.newBlogTransactional(blogAddedEvent); } |
Выполнение может быть привязано к стандартным этапам транзакции: до / после принятия, после отката или после завершения (как принятие, так и откат). По умолчанию оно обрабатывает событие, только если оно было опубликовано в рамках транзакции. В другом случае событие отбрасывается.
Для поддержки выполнения в falbackExecution
контексте может использоваться флаг falbackExecution
. Если установлено значение «истина», событие обрабатывается немедленно, если транзакция не выполняется.
1
2
3
4
|
@TransactionalEventListener (fallbackExecution = true ) public void blogAddedTransactional(BlogAddedEvent blogAddedEvent) { externalNotificationSender.newBlogTransactional(blogAddedEvent); } |
Резюме
Представленные в Spring 4.2 обработчики событий на основе аннотаций продолжают тенденцию к сокращению стандартного кода в приложениях, основанных на Spring (Boot). Не нужно вручную создавать реализации ApplicationListener
, не нужно напрямую использовать TransactionSynchronizationManager
— только одна аннотация с правильной конфигурацией. Другая сторона медали в том, что все слушатели событий немного сложнее найти, особенно если в нашем монолитном приложении их десятки (хотя их легко сгруппировать). Конечно, новый подход — это только вариант, который может быть полезен в данном случае использования или нет. Тем не менее, еще один кусочек весеннего (загрузочного) волшебства попал в наши системы. Но, может быть, сопротивление бесполезно?
Обратите внимание, что Spring Framework 4.2 является зависимостью по умолчанию Spring Boot 1.3 (на момент написания 1.3.0.M5 доступен). Кроме того, можно вручную обновить версию Spring Framework в Gradle / Maven для Spring Boot 1.2.5 — она должна работать в большинстве случаев.
- Примеры кода доступны на GitHub .
Кстати, написание примеров для этого поста в блоге дало мне первую реальную возможность использовать новую тестовую систему управления транзакциями, представленную в Spring 4.1 (в прошлом я упоминал об этом только во время моих учебных занятий Spring). Возможно, скоро напишу об этом подробнее.
Ссылка: | Упрощенная обработка связанных с асинхронными транзакциями событий в Spring 4.2 от нашего партнера по JCG Марцина Заячковского из блога Solid Soft . |