Статьи Утилиты Telegram YouTube RuTube Отзывы

Работа с событиями в Spring

Исходники

10 февраля 2025

Тэги: Kotlin, rest, Spring, Spring Boot, YouTube.

Содержание

  1. Создаём модель события
  2. Сервис работы с объектами
  3. Обработчик событий
  4. Rest-контроллер
  5. Асинхронная обработка событий
  6. Выводы

Spring «из коробки» предоставляет простой механизм работы с событиями, которые позволяют уменьшить связность компонентов системы. Событие, которое возникает в одной точке приложения, может быть перехвачено и обработано в любой другой части приложения благодаря таким сущностям как publisher и eventListener.

Для примера рассмотрим rest-приложение на Kotlin, в котором есть метод создания некоторого объекта. И мы хотим фиксировать каждое создание объекта в системе. Чтобы не повышать связность кода и не делать явный вызов конкретного компонента, мы можем публиковать событие в нашей системе и любые компоненты, которые будут «прослушивать» тип такого события, смогут выполнить дополнительные действия.

Создаём модель события

Для начала определимся, какие события могут происходить в системе. Например, объекты у нас могут создаваться, обновляться и удаляться. Такой набор действий удобно представить в виде перечисления ActionType:

enum class ActionType {
    CREATE,
    UPDATE,
    DELETE,
}

Помимо типа действия нам было бы неплохо знать, какой id у только что созданного объекта. Объединим тип действия и id в data-класс ActionInfo:

data class ActionInfo(
    val objectId: Int,
    val actionType: ActionType,
)

Создадим собственный тип события (ещё один data-класс), унаследовав его от стандартного класса ApplicationEvent.

data class BusinessEvent(
    val sourceObject: Any,
    val payload: ActionInfo,
): ApplicationEvent(sourceObject)

У этого класса есть два поля:

  1. sourceObject для ссылки на объект, в котором возникло событие и который мы передаём в базовый класс события.
  2. payload – полезная мета-информация. В данном случае это наш ActionInfo.

Сервис работы с объектами

Теперь нужно сделать сервис, который будет создавать объект. Именно в нём должна быть инкапсулирована бизнес-логика по обработке и сохранению объекта (например, в БД). Но мы для краткости взаимодействовать с БД не будем. Также подтянем стандартный компонент ApplicationEventPublisher, чтобы публиковать через него наше событие.

@Service
class BusinessService(
    val publisher: ApplicationEventPublisher,
) {
    private val logger = LoggerFactory.getLogger(BusinessEvent::class.java)

    fun createObject(objectId: Int) {
        // бизнес-логика
    }
}

Сам метод сначала создаёт объект, затем публикует событие об этом. После каждого из этих шагов делаем запись в лог для наглядности.

fun createObject(objectId: Int) {
    // бизнес-логика
    logger.info("Object with id = $objectId created.")

    val event = BusinessEvent(
        sourceObject = this,
        payload = ActionInfo(
            objectId = objectId,
            actionType = ActionType.CREATE,
        )
    )
    publisher.publishEvent(event)
    logger.info("Event sent.")
}

Обработчик событий

Создадим второй сервис, который отвечает за аудит:

@Service
class AuditService {

    private val logger = LoggerFactory.getLogger(AuditService::class.java)

    @EventListener(BusinessEvent::class)
    fun onBusinessEvent(event: BusinessEvent) {
        logger.info("Business event received: ${event.payload}.")
    }
}

Метод onBusinessEvent() будет перехватывать все события типа BusinessEvent благодаря аннотации @EventListener, в которой указывается тип этого события. Вы можете создать любое количество обработчиков на каждый тип события. Или же, наоборот, создать базовый класс для ваших событий и обрабатывать все события с помощью одного метода.

В реальном приложении сервис аудита мог бы отправлять какие-то уведомления по email или же фиксировать событие в общую таблицу аудита в БД. В нашем случае мы просто логируем payload (т.е. ActionInfo).

Обратите внимание, что AuditService не содержит никаких ссылок на BusinessService и наоборот. Событийная модель уменьшает связность между компонентами приложения.

Rest-контроллер

Чтобы создавать объекты в системе по запросу извне, создадим rest-контроллер, который вызывает BusinessService:

@RestController
class BusinessController(
    private val businessService: BusinessService,
) {

    @PostMapping("/{id}")
    fun create(@PathVariable id: Int): String {
        businessService.createObject(id)
        return "Объект с id = $id создан в системе!"
    }
}

Он обрабатывает POST-запрос, в котором передаётся id объекта как часть url. Мы создаём объект и в ответ на запрос возвращаем сообщение об этом.

Запустим наше приложение и выполним POST-запрос на url http://127.0.0.1:8080/123, где 123 – это id нашего объекта. В ответ получим текст «Объект с id = 123 создан в системе!». Если посмотрим в логи, то увидим следующие записи:

ru.devmark.event.BusinessEvent: Object with id = 123 created.
ru.devmark.service.AuditService: Business event received: ActionInfo(objectId=123, actionType=CREATE).
ru.devmark.event.BusinessEvent: Event sent.

То есть событие было успешно обработано.

Асинхронная обработка событий

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

Благо в Spring Boot сделать это довольно просто. Добавим аннотацию @Async к нашему обработчику в сервисе аудита:

@Async
@EventListener(BusinessEvent::class)
fun onBusinessEvent(event: BusinessEvent) {
    logger.info("Business event received: ${event.payload}.")
}

А чтобы активировать механизм асинхронной обработки, добавим аннотацию @EnableAsync к main-классу нашего приложения. В качестве альтернативы можно создать отдельный спринговый бин с конфигурацией и поместить аннотацию туда. Но, поскольку main-класс сам по себе является конфигурацией (см. @SpringBootApplication), то мы этого можем не делать.

@SpringBootApplication
@EnableAsync
class SpringEventExampleApplication

fun main(args: Array<String>) {
    runApplication<SpringEventExampleApplication>(*args)
}

Запустим ещё раз приложение, повторим запрос и посмотрим логи:

ru.devmark.event.BusinessEvent: Object with id = 123 created.
ru.devmark.event.BusinessEvent: Event sent.
ru.devmark.service.AuditService: Business event received: ActionInfo(objectId=123, actionType=CREATE).

Теперь последовательность записей в логах соответствует нашим ожиданиям. Хотя в теории последовательность не всегда может быть такой – на то она и асинхронная.

Выводы

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

Прежде всего это снижает связность отдельных компонентов системы между собой. В дальнейшем такая архитектура позволит легко перейти на более «серьёзные» решения вроде полноценных очередей RabbitMQ или Kafka.


См. также


Комментарии

Добавить комментарий

×

devmark.ru