JavaRush /Курсы /Spring Core /От ApplicationListener

От ApplicationListener к @EventListener

Spring Core
18 уровень , 0 лекция
Открыта

1. Короткая форма для listeners

Когда вы только начинаете писать listeners, ApplicationListener<T> кажется идеальным: всё строго, типобезопасно и без сюрпризов. Но как только приложение становится чуть более живым, внезапно выясняется, что реакций на одно событие может быть несколько, а событий — тоже больше одного. И ваш проект начинает обрастать десятками маленьких классов, которые отличаются тремя строками.

Проблема тут не в том, что «много кода — плохо», а в том, что этот код становится шумом. Вы читаете проект и видите: «ага, класс, который существует только ради того, чтобы реализовать интерфейс и написать один метод». Это не катастрофа, но это быстро превращается в визуальный мусор, в котором сложно заметить действительно важные детали: какую именно реакцию мы делаем, какие зависимости у неё есть, и насколько она вообще относится к текущему бизнес-сценарию.

И вот здесь Spring даёт более удобную форму записи того же самого смысла: вместо «класс является listener-ом» мы начинаем думать «вот этот метод — обработчик события». Это звучит почти как «о, наконец-то нормальный мир», потому что часто реакция на событие — это действительно небольшой метод, а не полноценная сущность, которая заслуживает отдельного интерфейса.

2. Как работает publishEvent(...)

Прежде чем перейти к аннотации, важно удержать картину мира. Очень легко начать воспринимать @EventListener как «новую технологию», которая работает как-то иначе. На самом деле меняется только форма объявления обработчика, а сама событийная модель остаётся прежней: есть публикация, есть доставка, есть вызов обработчиков.

Удобно держать это в голове как простую схему. Мы не изучаем внутренности Spring «по косточкам», но mental model должна быть устойчивой:

flowchart TD
    A["Use-case сервис"] -->|publishEvent| B["ApplicationEventPublisher"]
    B --> C["Доставка события внутри контекста"]
    C --> D1["Listener #1"]
    C --> D2["Listener #2"]
    C --> D3["Listener #3"]

@EventListener не добавляет новый блок «магическая шина событий». Он всего лишь говорит Spring: «вот этот метод нужно зарегистрировать как обработчик такого-то события». Дальше контейнер всё делает так же, как и вчера: когда событие публикуется, оно доставляется подписчикам.

В проекте ContextFlow это значит, что OrderPlacementService и OrderCancellationService остаются теми же «публикаторами событий», а аудит/уведомления/статистика остаются теми же «реакциями». Мы просто перепишем их в более удобной форме, чтобы код читался как набор реакций, а не как коллекция «классов ради интерфейса».

3. ApplicationListener<T>: плюсы и шум

Чтобы переход на @EventListener был осмысленным, давайте честно посмотрим на стартовую точку. В ApplicationListener<T> есть сильные стороны: тип события виден в generic-параметре, контракт обработчика формализован, IDE отлично подсказывает сигнатуру, и вы почти не можете «случайно» слушать не то событие.

Вот как вчера мог выглядеть наш аудит на создание заказа (упрощённо, чтобы не потеряться в деталях):

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component // Spring создаёт bean, и только тогда он становится подписчиком на событие
public class AuditOrderCreatedListener implements ApplicationListener<OrderCreatedEvent> {

    @Override
    public void onApplicationEvent(OrderCreatedEvent event) {
        // Здесь важно: тип события задаётся generic-параметром интерфейса
        System.out.println("AUDIT created: " + event.orderId()); // AUDIT created: ...
    }
}

На маленьком количестве listeners это нормально. Но представьте, что вам нужно слушать ещё OrderCancelledEvent, плюс вы хотите логировать оба события в одном месте, плюс у вас появятся статистика, отчётность, «уведомления по разным каналам» и прочие радости жизни. Тогда вы либо создаёте много однотипных классов, либо начинаете городить сложные проверки внутри одного listener-а на разные типы (и теряете красоту type-safe модели).

И тут появляется простой вопрос: а обязательно ли нам каждый раз оформлять реакцию как «отдельный класс-слушатель», если по смыслу это просто метод: onOrderCreated(...)? Обычно — нет. И именно на этот вопрос отвечает @EventListener.

4. @EventListener: обработчик как метод bean-а

Переход на @EventListener полезно воспринимать не как «новую систему событий», а как более удобный синтаксис поверх старой. Мы по-прежнему делаем Spring-Bean, по-прежнему живём в ApplicationContext, по-прежнему пользуемся constructor injection. Просто теперь нам не нужно реализовывать интерфейс, чтобы сказать «этот метод реагирует на событие».

Вот тот же аудит, но в method-based стиле:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Важно: метод-обработчик будет зарегистрирован только у bean-а
public class AuditOrderEventsListener {

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Тип события берётся из параметра метода (а не из generic-интерфейса)
        System.out.println("AUDIT created: " + event.orderId()); // AUDIT created: ...
    }
}

С точки зрения результата всё то же самое: при публикации OrderCreatedEvent Spring вызовет обработчик. Но читать такой код приятнее: вы видите название метода (а значит — намерение), вы видите параметр события (а значит — тип), и вы не обязаны превращать каждый обработчик в отдельную «интерфейсную сущность».

Ещё одна приятная деталь: теперь класс может содержать несколько обработчиков, и это часто ровно то, чего мы хотим. Например, один bean может логировать все order-события, если его ответственность действительно «логировать события заказов», а не «делать всё подряд».

5. Как выбирается тип события

Когда вы используете ApplicationListener<T>, тип события задаётся параметром T. Когда вы используете @EventListener, тип события обычно определяется по параметру метода. То есть если метод принимает OrderCreatedEvent, то он будет вызван именно для OrderCreatedEvent (и потенциально для его подтипов, если вы их введёте).

Это ощущается почти слишком простым, и в этом как раз сила: ваш метод выглядит как обычный Java-метод, а Spring просто «подписывает» его на событие соответствующего типа.

Давайте на секунду зафиксируем, как у нас в ContextFlow могут выглядеть события. Мы в курсе уже обсуждали payload events: когда событие — это просто обычный объект (не обязательно наследник ApplicationEvent). В современных Spring-приложениях это часто самый приятный стиль.

Например, событие создания заказа:

import com.example.contextflow.domain.model.NotificationChannel;

/**
 * Payload-событие: обычный объект, который Spring доставляет слушателям.
 * orderId — идентификатор заказа.
 * channel — канал, который может понадобиться обработчикам (уведомлениям и т.п.).
 */
public record OrderCreatedEvent(String orderId, NotificationChannel channel) { }

И обработчик, который реагирует на него, получает этот payload как параметр:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Если убрать @Component, Spring не создаст bean — и обработчик не вызовется
public class OrderEventsLogger {

    @EventListener
    public void onCreated(OrderCreatedEvent event) {
        // Здесь event — тот самый payload, который опубликовали через publishEvent(...)
        System.out.println("created " + event.orderId()); // created ...
    }
}

Обратите внимание на важный психологический эффект: @EventListener делает событие похожим на обычный аргумент метода. Это сильно снижает «ритуальность» кода. Вы не чувствуете, что «входите в специальный мир Spring». Вы пишете обычный Java-метод с понятным параметром — и всё.

При этом есть граница. Если вы начнёте делать обработчики «слишком умными», начнёте складывать туда половину бизнес-логики, всё это перестанет быть простым. Но сама аннотация тут ни при чём. Она просто делает удобнее то, что мы уже умеем.

6. Несколько обработчиков в одном bean-е

После знакомства с @EventListener у многих возникает соблазн: «О! Значит, можно в один класс запихнуть вообще все обработчики и жить счастливо». Это выглядит как экономия файлов, но на практике быстро превращается в “God Listener”, который знает обо всём и делает всё. А мы только что весь курс учились уменьшать связанность, а не выращивать её в новом месте.

Правило, которое хорошо работает на уровне Junior: один listener-bean может иметь несколько @EventListener-методов, если они относятся к одной понятной ответственности. Например, «логирование order-событий» — одна ответственность. «Аудит всех действий по заказам» — тоже одна. «И аудит, и уведомления, и статистика, и формирование отчёта, и ещё чуть-чуть магии» — это уже комбайн.

Вот хороший пример: один bean, две реакции на два события, но обе — про логирование:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Один bean — несколько обработчиков, если ответственность единая
public class OrderEventsLogger {

    @EventListener
    public void onCreated(OrderCreatedEvent event) {
        // Реакция на создание заказа
        System.out.println("created " + event.orderId()); // created ...
    }

    @EventListener
    public void onCancelled(OrderCancelledEvent event) {
        // Реакция на отмену заказа
        System.out.println("cancelled " + event.orderId()); // cancelled ...
    }
}

А вот пример, который лучше не делать (я его не привожу кодом специально, чтобы не закреплять): «в одном классе три метода — один пишет аудит, второй отправляет уведомления, третий считает статистику». Вроде бы компактно, но с точки зрения чтения проекта это ухудшение: вы теряете модульность и начинаете смешивать разные причины изменения кода.

В ContextFlow нам как раз полезно, чтобы аудит жил отдельно от уведомлений, а уведомления — отдельно от статистики. Тогда реакцию можно не только читать, но и отключать профилем, менять реализацию, тестировать и развивать независимо. Мы к этому ещё вернёмся в следующих лекциях дня, но зерно дисциплины лучше посадить прямо сейчас.

7. Миграция ContextFlow на @EventListener

Сейчас сделаем очень практический шаг: перепишем один listener так, чтобы он стал method-based. Важно подчеркнуть: мы не меняем домен, не меняем сценарий, не меняем порядок действий и не усложняем проект. Мы просто меняем форму объявления обработчика, чтобы дальше нам было проще переносить побочные эффекты из сервисов в listeners.

Представим, что у нас уже есть сервис для аудита, который пишет записи через AuditWriterdev-профиле — в консоль). Пусть он выглядит примерно так:

import org.springframework.stereotype.Service;

@Service // Это зависимость listener-а: именно сюда уходит «побочный эффект» аудита
public class AuditService {

    public void recordOrderCreated(String orderId) {
        // В реальном проекте тут обычно будет запись в БД/лог/внешний аудит-сервис
        System.out.println("AUDIT: order created " + orderId); // AUDIT: order created ...
    }
}

Теперь — listener. Было (вчера): интерфейсный стиль.

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component // Spring создаёт bean и регистрирует его как ApplicationListener
public class AuditOrderCreatedListener implements ApplicationListener<OrderCreatedEvent> {

    private final AuditService auditService;

    public AuditOrderCreatedListener(AuditService auditService) {
        // Обычный constructor injection: никакой магии, просто зависимость
        this.auditService = auditService;
    }

    @Override
    public void onApplicationEvent(OrderCreatedEvent event) {
        // Реакция на событие: делегируем «побочный эффект» в сервис аудита
        auditService.recordOrderCreated(event.orderId());
    }
}

Стало (сегодня): аннотационный стиль. Логика та же, зависимости те же, даже имя метода похоже, просто Spring теперь понимает подписку по аннотации.

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // По-прежнему обычный Spring bean
public class AuditOrderEventsListener {

    private final AuditService auditService;

    public AuditOrderEventsListener(AuditService auditService) {
        // Зависимость такая же, как и раньше
        this.auditService = auditService;
    }

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Реакция на событие теперь выражена аннотацией на методе
        auditService.recordOrderCreated(event.orderId());
    }
}

Что мы выиграли? Теперь в этом же классе (если это будет логично) мы можем добавить второй обработчик, например на отмену заказа, и не плодить отдельный интерфейсный listener.

И очень важная мысль, которую стоит буквально проговорить вслух: AuditOrderEventsListener — всё ещё обычный Spring bean. Если вы создадите его руками через new (AuditOrderEventsListener(...)) где-нибудь вне контейнера, @EventListener не «включится». Аннотация не является магическим заклинанием, которое работает в вакууме. Она работает только потому, что контейнер сканирует beans и регистрирует методы как обработчики.

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

8. Типичные ошибки при @EventListener

Ошибка №1: воспринимать @EventListener как “новую систему событий”, а не как удобную форму старой.
Иногда после первой аннотации у новичка появляется ощущение: “Ого, теперь у меня event bus!”. Нет, у вас всё тот же Spring application event-механизм. Если раньше вы понимали, что происходит при publishEvent(...), то теперь вы просто объявляете обработчик короче. Это важно, чтобы не уехать в фантазию про «асинхронность по умолчанию» и «события сами всё решат».

Ошибка №2: делать обработчик не bean-ом и ждать, что он будет вызываться.
@EventListener работает только на методах объектов, которые реально живут в ApplicationContext. Если вы забыли @Component, не включили пакет в scanning или зарегистрировали класс не там, контейнер его не создаст, и событие уйдёт в пустоту. События — это не рефлексия по всему classpath, а подписка конкретных beans.

Ошибка №3: собирать все обработчики мира в один “универсальный” listener.
С аннотациями появляется искушение сделать один класс на всё: и аудит, и уведомления, и статистика, и отчёты. Это ухудшает архитектуру: вы снова создаёте центр связанности, только теперь он называется не OrderPlacementService, а EverythingListener. Группируйте методы по ответственности, а не по принципу “лишь бы меньше файлов”.

Ошибка №4: прятать смысл обработчика за плохим именем метода.
Когда listener — это интерфейс, у вас всегда есть onApplicationEvent, и смысл приходится искать в теле метода. В @EventListener-модели имя метода — часть читаемости. Если вы назвали метод handle() или doStuff(), вы сами себе выстрелили в ногу. onOrderCreated, onOrderCancelled, auditCreated, sendNotificationOnCreated — пусть будет длиннее, но понятнее.

Ошибка №5: делать listener ленивым “по привычке”, а потом удивляться первой задержке и поздним ошибкам.
Даже если listener будет корректно зарегистрирован, ленивое создание означает, что реальный объект может появиться только при первом событии. Тогда первый заказ может “вдруг” стать медленнее, а ошибка в конструкторе listener-а проявится не на старте приложения, а в момент первого события. Для базового teaching style лучше держать listeners обычными singleton-beans без @Lazy, чтобы приложение оставалось fail-fast и предсказуемым.

1
Задача
Spring Core, 18 уровень, 0 лекция
Недоступна
Слушатель оплаты как метод bean-а
Слушатель оплаты как метод bean-а
1
Задача
Spring Core, 18 уровень, 0 лекция
Недоступна
Два события в одном listener-bean
Два события в одном listener-bean
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ