«1. Обзор

В этом руководстве мы обсудим, как использовать события в Spring.

События — одна из самых недооцененных функций фреймворка, но также и одна из самых полезных. И, как и многое другое в Spring, публикация событий — это одна из возможностей, предоставляемых ApplicationContext.

Есть несколько простых рекомендаций, которым нужно следовать:

    Класс события должен расширять ApplicationEvent, если мы используем версии до Spring Framework 4.2. Начиная с версии 4.2, классы событий больше не должны расширять класс ApplicationEvent. Издатель должен внедрить объект ApplicationEventPublisher. Слушатель должен реализовать интерфейс ApplicationListener.

2. Пользовательское событие

Spring позволяет нам создавать и публиковать пользовательские события, которые по умолчанию являются синхронными. Это имеет несколько преимуществ, таких как возможность прослушивателя участвовать в контексте транзакции издателя.

2.1. Простое событие приложения

Давайте создадим простой класс события — просто заполнитель для хранения данных события.

В этом случае класс события содержит сообщение String:

public class CustomSpringEvent extends ApplicationEvent {
    private String message;

    public CustomSpringEvent(Object source, String message) {
        super(source);
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

2.2. Издатель

Теперь давайте создадим издателя этого события. Издатель создает объект события и публикует его всем, кто его слушает.

Чтобы опубликовать событие, издатель может просто внедрить ApplicationEventPublisher и использовать API publishEvent(): запуск приложения. Обычно проще добавить издателю @Autowire.

@Component
public class CustomSpringEventPublisher {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publishCustomEvent(final String message) {
        System.out.println("Publishing custom event. ");
        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
        applicationEventPublisher.publishEvent(customSpringEvent);
    }
}

Начиная с Spring Framework 4.2, интерфейс ApplicationEventPublisher предоставляет новую перегрузку для метода publishEvent(Object event), который принимает любой объект в качестве события. Поэтому событиям Spring больше не нужно расширять класс ApplicationEvent.

2.3. Слушатель

Наконец, давайте создадим слушателя.

Единственным требованием к слушателю является то, что он должен быть bean-компонентом и реализовывать интерфейс ApplicationListener:

Обратите внимание, как наш пользовательский слушатель параметризован с помощью универсального типа пользовательского события, что делает метод onApplicationEvent() типобезопасным . Это также избавляет от необходимости проверять, является ли объект экземпляром определенного класса событий, и приводить его.

@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
    @Override
    public void onApplicationEvent(CustomSpringEvent event) {
        System.out.println("Received spring custom event - " + event.getMessage());
    }
}

И, как уже обсуждалось (по умолчанию события Spring синхронны), метод doStuffAndPublishAnEvent() блокируется до тех пор, пока все слушатели не закончат обработку события.

3. Создание асинхронных событий

В некоторых случаях синхронная публикация событий не совсем то, что нам нужно — нам может понадобиться асинхронная обработка наших событий.

Мы можем включить это в конфигурации, создав bean-компонент ApplicationEventMulticaster с исполнителем.

Для наших целей здесь хорошо работает SimpleAsyncTaskExecutor:

Реализации события, издателя и слушателя остаются такими же, как и раньше, но теперь слушатель будет асинхронно обрабатывать событие в отдельном потоке.

@Configuration
public class AsynchronousSpringEventsConfig {
    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster =
          new SimpleApplicationEventMulticaster();
        
        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }
}

4. Существующие события фреймворка

Сам Spring публикует различные события из коробки. Например, ApplicationContext будет запускать различные события фреймворка: ContextRefreshedEvent, ContextStartedEvent, RequestHandledEvent и т. д.

Эти события предоставляют разработчикам приложений возможность подключаться к жизненному циклу приложения и контексту и добавлять собственную пользовательскую логику там, где это необходимо.

Вот краткий пример слушателя, прослушивающего обновления контекста:

Чтобы узнать больше о существующих событиях фреймворка, взгляните на наше следующее руководство здесь.

public class ContextRefreshedListener 
  implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent cse) {
        System.out.println("Handling context re-freshed event. ");
    }
}

5. Прослушиватель событий, управляемый аннотациями

Начиная с Spring 4.2, прослушиватель событий не обязательно должен быть bean-компонентом, реализующим интерфейс ApplicationListener — его можно зарегистрировать в любом общедоступном методе управляемого bean-компонента через @ Аннотация EventListener:

Как и прежде, сигнатура метода объявляет тип потребляемого события.

@Component
public class AnnotationDrivenEventListener {
    @EventListener
    public void handleContextStart(ContextStartedEvent cse) {
        System.out.println("Handling context started event.");
    }
}

«По умолчанию прослушиватель вызывается синхронно. Однако мы можем легко сделать его асинхронным, добавив аннотацию @Async. Нам просто нужно не забыть включить поддержку Async в приложении.

6. Поддержка дженериков

Также можно отправлять события с информацией об дженериках в типе события.

6.1. Общее событие приложения

Давайте создадим общий тип события.

В нашем примере класс события содержит любое содержимое и индикатор состояния успеха:

Обратите внимание на разницу между GenericSpringEvent и CustomSpringEvent. Теперь у нас есть возможность публиковать любое произвольное событие, и больше не требуется расширяться от ApplicationEvent.

public class GenericSpringEvent<T> {
    private T what;
    protected boolean success;

    public GenericSpringEvent(T what, boolean success) {
        this.what = what;
        this.success = success;
    }
    // ... standard getters
}

6.2. Слушатель

Теперь давайте создадим прослушиватель этого события.

Мы могли бы определить слушателя, реализовав интерфейс ApplicationListener, как раньше:

Но это определение, к сожалению, требует, чтобы мы наследовали GenericSpringEvent от класса ApplicationEvent. Итак, для этого руководства давайте воспользуемся прослушивателем событий, управляемым аннотациями, который обсуждался ранее.

@Component
public class GenericSpringEventListener 
  implements ApplicationListener<GenericSpringEvent<String>> {
    @Override
    public void onApplicationEvent(@NonNull GenericSpringEvent<String> event) {
        System.out.println("Received spring generic event - " + event.getWhat());
    }
}

Также можно сделать прослушиватель событий условным, определив логическое выражение SpEL в аннотации @EventListener.

В этом случае обработчик событий будет вызываться только для успешного события GenericSpringEvent of String:

Spring Expression Language (SpEL) — это мощный язык выражений, подробно описанный в другом учебнике.

@Component
public class AnnotationDrivenEventListener {
    @EventListener(condition = "#event.success")
    public void handleSuccessful(GenericSpringEvent<String> event) {
        System.out.println("Handling generic event (conditional).");
    }
}

6.3. Издатель

Издатель события аналогичен описанному выше. Но из-за стирания типа нам нужно опубликовать событие, которое разрешает параметр generics, по которому мы будем фильтровать, например, class GenericStringSpringEvent extends GenericSpringEvent\u003cString\u003e.

Кроме того, есть альтернативный способ публикации событий. Если мы вернем в качестве результата ненулевое значение из метода, аннотированного @EventListener, Spring Framework отправит нам этот результат как новое событие. Более того, мы можем публиковать несколько новых событий, возвращая их в коллекцию в результате обработки событий.

7. События, связанные с транзакцией

Этот раздел посвящен использованию аннотации @TransactionalEventListener. Чтобы узнать больше об управлении транзакциями, ознакомьтесь с Transactions With Spring и JPA.

Начиная с Spring 4.2, платформа предоставляет новую аннотацию @TransactionalEventListener, которая является расширением @EventListener и позволяет привязать прослушиватель события к фазе транзакции.

Привязка возможна к следующим фазам транзакции:

AFTER_COMMIT (по умолчанию) используется для запуска события, если транзакция завершилась успешно. AFTER_ROLLBACK — если транзакция откатилась AFTER_COMPLETION — если транзакция завершена (псевдоним для AFTER_COMMIT и AFTER_ROLLBACK) BEFORE_COMMIT используется для запуска события непосредственно перед фиксацией транзакции.

    Вот краткий пример прослушивателя транзакционных событий:

Этот прослушиватель будет вызываться только в том случае, если есть транзакция, в которой работает производитель событий, и она вот-вот будет зафиксирована.

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleCustom(CustomSpringEvent event) {
    System.out.println("Handling event inside a transaction BEFORE COMMIT.");
}

И если ни одна транзакция не выполняется, событие вообще не отправляется, если только мы не переопределим это, установив для атрибута fallbackExecution значение true.

8. Заключение

В этой быстрой статье мы рассмотрели основы работы с событиями в Spring, включая создание простого пользовательского события, его публикацию и последующую обработку в слушателе.

Мы также кратко рассмотрели, как включить асинхронную обработку событий в конфигурации.

Затем мы узнали об улучшениях, представленных в Spring 4.2, таких как прослушиватели на основе аннотаций, улучшенная поддержка дженериков и привязка событий к фазам транзакций.

Как всегда, код, представленный в этой статье, доступен на GitHub. Это проект на основе Maven, поэтому его легко импортировать и запускать как есть.

«