10 февраля 2025
Тэги: Kotlin, rest, Spring, Spring Boot, YouTube.
Spring «из коробки» предоставляет простой механизм работы с событиями, которые позволяют уменьшить связность компонентов системы. Событие, которое возникает в одной точке приложения, может быть перехвачено и обработано в любой другой части приложения благодаря таким сущностям как publisher и eventListener.
Для примера рассмотрим rest-приложение на Kotlin, в котором есть метод создания некоторого объекта. И мы хотим фиксировать каждое создание объекта в системе. Чтобы не повышать связность кода и не делать явный вызов конкретного компонента, мы можем публиковать событие в нашей системе и любые компоненты, которые будут «прослушивать» тип такого события, смогут выполнить дополнительные действия.
Для начала определимся, какие события могут происходить в системе. Например, объекты у нас могут создаваться, обновляться и удаляться. Такой набор действий удобно представить в виде перечисления ActionType:
Помимо типа действия нам было бы неплохо знать, какой id у только что созданного объекта. Объединим тип действия и id в data-класс ActionInfo:
Создадим собственный тип события (ещё один data-класс), унаследовав его от стандартного класса ApplicationEvent.
У этого класса есть два поля:
Теперь нужно сделать сервис, который будет создавать объект. Именно в нём должна быть инкапсулирована бизнес-логика по обработке и сохранению объекта (например, в БД). Но мы для краткости взаимодействовать с БД не будем. Также подтянем стандартный компонент ApplicationEventPublisher, чтобы публиковать через него наше событие.
Сам метод сначала создаёт объект, затем публикует событие об этом. После каждого из этих шагов делаем запись в лог для наглядности.
Создадим второй сервис, который отвечает за аудит:
Метод onBusinessEvent() будет перехватывать все события типа BusinessEvent благодаря аннотации @EventListener, в которой указывается тип этого события. Вы можете создать любое количество обработчиков на каждый тип события. Или же, наоборот, создать базовый класс для ваших событий и обрабатывать все события с помощью одного метода.
В реальном приложении сервис аудита мог бы отправлять какие-то уведомления по email или же фиксировать событие в общую таблицу аудита в БД. В нашем случае мы просто логируем payload (т.е. ActionInfo).
Обратите внимание, что AuditService не содержит никаких ссылок на BusinessService и наоборот. Событийная модель уменьшает связность между компонентами приложения.
Чтобы создавать объекты в системе по запросу извне, создадим rest-контроллер, который вызывает BusinessService:
Он обрабатывает POST-запрос, в котором передаётся id объекта как часть url. Мы создаём объект и в ответ на запрос возвращаем сообщение об этом.
Запустим наше приложение и выполним POST-запрос на url http://127.0.0.1:8080/123, где 123 – это id нашего объекта. В ответ получим текст «Объект с id = 123 создан в системе!». Если посмотрим в логи, то увидим следующие записи:
То есть событие было успешно обработано.
Если присмотреться к записям в логах, можно увидеть, что событие сначала было обработано сервисом аудита и только после этого появилась запись о том, что событие отправлено. Это может сбивать с толку, т.к. мы ожидаем, что событие сначала будет отправлено и только потом обработано. То есть отправка события и его обработка сейчас происходит в один поток, как если бы мы вызывали сервис аудита напрямую. Для уменьшения связности лучше отправлять события в асинхронном режиме.
Благо в Spring Boot сделать это довольно просто. Добавим аннотацию @Async к нашему обработчику в сервисе аудита:
А чтобы активировать механизм асинхронной обработки, добавим аннотацию @EnableAsync к main-классу нашего приложения. В качестве альтернативы можно создать отдельный спринговый бин с конфигурацией и поместить аннотацию туда. Но, поскольку main-класс сам по себе является конфигурацией (см. @SpringBootApplication), то мы этого можем не делать.
Запустим ещё раз приложение, повторим запрос и посмотрим логи:
Теперь последовательность записей в логах соответствует нашим ожиданиям. Хотя в теории последовательность не всегда может быть такой – на то она и асинхронная.
Spring позволяет легко реализовать простейшую событийную модель в приложении. Благодаря паре аннотаций обработку событий можно сделать асинхронной.
Прежде всего это снижает связность отдельных компонентов системы между собой. В дальнейшем такая архитектура позволит легко перейти на более «серьёзные» решения вроде полноценных очередей RabbitMQ или Kafka.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.