Каждый java разработчик который работал с достаточно нетривиальным проектом на spring рано или поздно сталкивался с подобным логом при старте приложения.
*************************** APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | service1 defined in file [C:\Users\d.starakojev\IdeaProjects\demo\target\classes\com\example\demo\Service1.class] ↑ ↓ | service3 defined in file [C:\Users\d.starakojev\IdeaProjects\demo\target\classes\com\example\demo\Service3.class] ↑ ↓ | service2 defined in file [C:\Users\d.starakojev\IdeaProjects\demo\target\classes\com\example\demo\Service2.class] └─────┘ Action: Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
Разберемся откуда это берется и что с этим делать на примере эволюции простенького сервиса.
комментарий от КЭПа про суть проблемы
Бины создаются в 2 этапа
1. создать объект
2. внедрить зависимости
(настройки и прочее опущу)
Проблема справедлива для инжекта в конструктор (целевой способ), потому что оба этапа объединятся и ситуация становится патовой.
Если шаги разделены, то проблем нет, так как связи можно делать сколь угодно сложными и запутанными, после того как все объекты созданы.
Но способ с инжектом в конструктор является целевым ни просто так по нескольким причинам
1. снижение зависимости от фреймворка
а. не нужны приключения с рефлексией при инжекте в поле, и любой объект можно ез проблем создать руками
б. не нужно делать специальных методов при инжекте через сеттер
2. снижаются возможности к получению невалидного объекта (который на пол пути создания)
3. из коробки работает контроль циклических зависимостей
4. сама система заставляет нас думать об архитектуре сразу, а не когда будет уже поздно
Рецепт 1: кладем на декомпозицию и SOLID (а именно Single responsibility)
Изначально у нас есть быстро сделанные сервисы
1. UserService — отвечает за все что касается пользователей
2. NosificationService — отвечает за отправку сообщений пользователям
3. NotificationService — просто отправляет письма на email
@Service public class UserService { /** * Такая незамысловатая логики выписывания токенов */ public String login(UUID id) { String token = "token " + getInfo(id) tokenRepo.put(id, token); return token; } public User getInfo(UUID id) { return new User(); // логика поиска } } @Service public class TokenRepo { public void put (UUID userId, String token) { // локига сохранения } } @Service @RequiredArgsConstructor public class NotificationService { private final UserService userService; private final NotificationSender notificationSender; public void send(String message, UUID userId) { User user = userService.getInfo(userId); notificationSender.send(message, user.getEmail()); } } @Service public class NotificationSender { public void send(String message, String email) { // логика отправки сообщения } }
Выглядит не особо красиво, но работает, до тех пор пока мы, например, не решим отправлять уведомления о входе в систему
@Service @RequiredArgsConstructor public class UserService { private final NotificationService notificationService; // новая зависимость /** * Такая незамысловатая логики выписывания токенов */ public String login(UUID id) { String token = "token " + getInfo(id); notificationService.send("login ", id); // новая логика return token; } public User getInfo(UUID id) { return new User(); // логика поиска } }
Циклическая зависимость в самом чистом виде и ошибка поднятия контекста.
*************************** APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | notificationService defined in file [C:\Users\d.starakojev\IdeaProjects\demo\target\classes\com\example\demo\bad\NotificationService.class] ↑ ↓ | userService defined in file [C:\Users\d.starakojev\IdeaProjects\demo\target\classes\com\example\demo\bad\UserService.class] └─────┘
Конечно же в боевом коде эта цепочка может достигать десятков звеньев, но способ лечения простой: ищем того, кто на себя слишком много взял и делим на 2 части. В нашем случае из UserService выделяем AuthService, а в самом UserService оставляем только ответственность за доступ к информации о пользователях
@Service @RequiredArgsConstructor public class AuthService { private final NotificationService notificationService; private final UserService userService; /** * Такая незамысловатая логики выписывания токенов */ public String login(UUID id) { String token = "token " + userService.getInfo(id); notificationService.send("login ", id); return token; } } @Service @RequiredArgsConstructor public class UserService { public User getInfo(UUID id) { return new User(); // логика поиска } }

Как показывает практика, следования этому принципу достаточно, чтобы не сталкивать с циклическими зависимостями очень долго.
Рецепт 2: просто жди ответ
На самом деле приложений без циклических зависимостей не существует, такие особенности просто хорошо заметены под ковер (в JVM и фреймворки).
Например: Любая функция с возвращаемым значением, автоматически создает у нас циклическую зависимость.
public static void main(String[] args) { String foo = method(); System.out.println(foo); } public static String method() { return "Hello"; }
Если нарисовать только «компоненты» и их зависимости в классическом понимании, когда один компонент знает адрес другого, то все выглядит складно

Но если на эту же схему наложить движение данных и управляющих сигналов, то тайное становится явным.

Почему это не становится проблемой?
Потому что на самом деле все исполняется примерно как на картинке ниже.
Наши «компоненты» друг с другом не общаются и друг о друге ничего не знают, всю работу делает JVM, которая позволяет нам игнорировать этот аспект, и все таки рассматривать методы как функциональные единицы с зависимостями.

Рецепт 3: просто добавь асинхрон
Разовьем пример выше, мы теперь хотим не просто нотификацию, а поддержать обратную связь, например если пользователь напишет со своего email, что это не он, тогда мы должны его разлогинить.
Такие задачи уже выходят из зоны ответственности JVM, и она за нас ничего не решит. Придется выкручиваться самостоятельно.
Вариантов реализации много, и функционально все будут работать правильно, но нам этого не достаточно, нужно чтобы решение было устойчивым, гибким и понятным, а именно чтобы сохранялась инкапсуляция и разделение ответственности.
-
В AuthService добавляем метод unLogin, потому что этот сервис логинит — ему и разлогинивать
-
Добавляем NotificationResponseListener, потому что принимать письма это принципиально новая работа для нашей системы
-
В NotificationService добавляем логику сопоставления запроса с ответом, потому что он формирует запрос. Так же для логично и привычно, чтобы получать ответ в том же месте, куда его отправили.
Дополнительно нам понадобится хранить отправленные сообщения, по этому добавляем класс MessageRepo (без этого никак не провести сопоставление)
@Service @RequiredArgsConstructor public class AuthService { private final NotificationService notificationService; private final UserService userService; private final TokenRepo tokenRepo; public String login(UUID id) { String token = "token " + userService.getInfo(id); tokenRepo.put(id, token); notificationService.send("login ", id); return token; } public void unLogin(UUID id) { tokenRepo.remove(id); } } @Service @RequiredArgsConstructor public class NotificationResponseListener { private final NotificationService notificationService; public void onMessage(Response response) { notificationService.handleResponse(response); } } @Service @RequiredArgsConstructor public class NotificationService { private final UserService userService; private final MessageRepo messageRepo; private final NotificationSender notificationSender; public void send(String messagePayload, UUID userId) { User user = userService.getInfo(userId); Message message = new Message(UUID.randomUUID(), messagePayload, user.getEmail()); messageRepo.save(message); notificationSender.send(message); } public void handleResponse(Response response) { RequestResponseBundle bundle = match(response); // вызвать логику разлогина } private RequestResponseBundle match(Response response) { Message sourceMessage = messageRepo.getMessage(response.getSourceMessageId()); return new RequestResponseBundle(sourceMessage, response); } }
А теперь вся соль в том, как вызвать логику разлогина?
Если притащим в NotificationService AuthService, снова получим циклическую зависимость,
да и не положено NotificationService знать про авторизацию.
Тут на самом деле остается 2 варианта, вполне рабочих
-
Добавить параметром Callback при отправке сообщения, и вызывать его в случае если пришел негативный ответ от пользователя.
-
Реализовать генерацию события о том что пришел негативный ответ от пользователя
(тут относительно первого способа появляется необходимость в регистрации слушателя и связь становится неявной, но зато событие могут слушать и другие сервисы)
И в том и в другом случае появляется необходимость в новом компоненте, который будет получать событие от NotificationService и вызывать нужный метод у AuthService.
Но какой бы способ мы не выбрали, циклическая зависимость никуда не денется.
Она просто заметается под явление «позднее связывание», зеленая стрелка появляется не на этапе компиляции, а уже в рантайме.
В то же время EventListenerRegistrar можно после страта контекста выбрасывать в помойку за ненадобностью.
И это не плохо, так устроен мир)
P.S. Отдельная история как эти события сделать персистентными и не терять при рестартах приложения.
Экстра рецепт: Просто залезь в кишки
Тут важно начать с того, что приложение это не просто автомобиль, который мы купили/сделали а дальше остается только кататься да чинить.
Приложение это еще и сырье, и чертеж, и завод (и завод для завода), и дорога.
На разных этапах своего жизненного цикла оно принимает разные формы с совершенно разной архитектурой.
Просто посмотрим на псевдокодный жизненный цикл spring boot приложения:
-
Притянуть библиотеки
-
Скомпилировать исходники
-
Упаковать образ
-
Запустить JVM
-
Поднять окружение Java
-
Запустить ядро спринга
-
Прочитать конфигрурации фабрики
-
Построить фабрику
-
Прочитать конфигурации контекста
-
Построить контекст
-
Настроить контекст
-
Установить соединения с интеграциями
-
Донастроить контекст
-
Приступить к выполнению полезной работы
На любом из этих этапов есть точки расширения и необходимость вернуться к предыдущим шагам или динамически модифицировать следующие, что в итоге формирует очень страшную спагетину из зависимостей. При чем как устроено большинство этапов для целевой работы приложения абсолютно не важно, по этому там и есть элемент хаоса.
Больше стрелок богу стрелок
Добавим детализации.
Добавляем Mailbox — в нашем случае как внешнюю систему, в которой пользователь работает с письмами.
Добавим красных стрелок, которые будут отражать направление движения данных в тех случаях, когда оно не совпадает с направлением владельца связи.

Соль в том, что белую стрелку, которая представляет собой «зависимость по контракту», мы можем развернуть, а вот направление движения данных никак.
Для примера, развернем белую стрелку между NotificationService и UserService при помощи DependencyInjection и динамического скоупа.
Теперь NotificationService ни сам забирает данные о пользователе, а к моменту вызова они у него уже есть. Задача в том, чтобы NotificationService даже не догадывался о том, откуда берутся данные о пользователе.
public class AuthService { private final NotificationService notificationService; private final ActiveUserInjector activeUserInjector; // новая зависимость private final UserService userService; private final TokenRepo tokenRepo; public String login(UUID id) { String token = "token " + userService.getInfo(id); tokenRepo.put(id, token); activeUserInjector.injectActiveUser(id); // новая логика notificationService.sendToActiveUser("login "); return token; } } public class ActiveUserInjector { private final UserService userService; private final NotificationService notificationService; public void injectActiveUser(UUID id) { notificationService.setActiveUser(userService.getInfo(id)); } } public class NotificationService { private final ThreadLocal<User> activeUser = new ThreadLocal<>(); // меняем UserService на юзера private final MessageRepo messageRepo; private final NotificationSender notificationSender; public void sendToActiveUser(String messagePayload) { Message message = new Message(UUID.randomUUID(), messagePayload, activeUser.get().getEmail()); messageRepo.save(message); notificationSender.send(message); } public void setActiveUser(User user) { // добавляем для упрщения инжекта activeUser.set(user); } }

Конечно, реализация носит исключительно концептуальный характер для демонстрации того, что стрелки «отношений» можно вертеть как угодно, но направление движения данных осталось неизменным.
Так же подсвечу, что то, что мы сделали с разворотом стрелки, это не инверсия зависимостей по канонам SOLID.
Мы физически убрали зависимость NotificationService от UserService, SOLID же говорит о том, что не стоит завязывать на реализацию зависимости. Достигается это за счет выделения интерфейса. Стрелки зависимости по контракту разворачиваются автоматически.
Таким образом мы подошли с новому виду стрелки «направления передачи управления».
И в данном случае все они смотрят в разные стороны.
1. белая — зависимость по контракту
2. красная — направление движения данных
3. фиолетовая — направления передачи управления
(если красная или фиолетовая стрелка совпадает с белой, она не отрисовывается, дабы не загромождать диаграмму)

Выводы
-
Архитектура сервиса не статична в течение жизненного цикла.
-
Зависимость — это явление, которое находится в состоянии суперпозиции, и то как она выглядит, зависит от того с какой точки зрения и в какое время мы смотрим.
-
На один и тот же процесс или сервис можно нарисовать множество диаграмм, и нужно четко понимать что, для кого и с какой целью мы рисуем
-
Циклически зависимости по своей сути не зло с которым нужно бороться любой ценой, а неотъемлемая часть устройства мира, которую нужно готовить осознанно
-
Многие привычные нам понятия вроде методов, если достаточно глубоко капнуть, взаимодействуют друг с другом совершенно иначе чем чем нам удобно думать.
Я сюда ни философствовать пришел, что делать?
-
Поддерживать баланс между разделением ответственности и инкапсуляцией логики
-
Внимательно относиться к асинхрону и колбэкам — это та область где циклическая зависимость является нормой, но готовить красиво придется самим
-
При получении ошибки поднятия контекста из-за циклической зависимости, нужно методично пройтись по всей цепочке
а. сделать позднее связывание (инжект через setter или в поле) там где есть асинхрон
б. распилить толстый класс на несколько там где ответственность расплывается
в. в случае проблем в кишках, главное не подгонять бизнес логику под фрейворк, а зависимости вероятно прийдется доставать руками, рекомендуемым способом на том этапе где вы пытаетесь влезть
ссылка на оригинал статьи https://habr.com/ru/articles/915356/
Добавить комментарий