Статьи

Упрощенная обработка связанных событий асинхронных транзакций в Spring 4.2+

Вступление

Как вы, вероятно, уже знаете (например, из моего предыдущего поста в блоге ), больше не нужно создавать отдельный класс, реализующий 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 — только одна аннотация с правильной конфигурацией. Другая сторона медали в том, что все слушатели событий немного сложнее найти, особенно если в нашем монолитном приложении их десятки (хотя их легко сгруппировать). Конечно, новый подход — это только вариант, который может быть полезен в данном случае использования или нет. Тем не менее, еще один кусочек весеннего (загрузочного) волшебства попал в наши системы. Но, может быть, сопротивление бесполезно?

Сопротивление бесполезно? Источник: http://en.memory-alpha.wikia.com/wiki/Borg_cube

Сопротивление бесполезно?
Источник: http://en.memory-alpha.wikia.com/wiki/Borg_cube

Обратите внимание, что 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). Возможно, скоро напишу об этом подробнее.