Статьи Утилиты Telegram YouTube VK Видео 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.


См. также

Облако тэгов

Kotlin, Java, Spring, Spring Boot, Spring Data, Spring AI, SQL, PostgreSQL, Oracle, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.

Последние статьи


Комментарии

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

×

devmark.ru