Каждый 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 или в поле) там где есть асинхрон
б. распилить толстый класс на несколько там где ответственность расплывается
в. в случае проблем в кишках, главное не подгонять бизнес логику под фрейворк, а зависимости вероятно прийдется доставать руками, рекомендуемым способом на том этапе где вы пытаетесь влезть
Farongy
Ну привычно допустим - каждому своё. А логично то почему? Особенно если "принимать письма это принципиально новая работа для нашей системы".
Почему NotificationResponseListener не может заинджектить AuthService и разлогинить?
dstarakozhev Автор
Принципиально новая логика, потому что это интеграционное взаимодействие, в рамках которого нужно тянуть данные с почтового ящика. Это полноценная отдельная сложная работа, которую нельзя просто так вмазать в то что уже есть, нужно изолировать.
Привычно и логично, потому что по своей сути это вызов функции с возвращаемым значением, и хотелось бы чтобы это так и выглядело (как вызов метода, для этого и был дан "рецепт 2"), независимо от того как оно устроено внутри. Не должно быть распадания на 2 независимых потока (исходящий и входящий).
NotificationResponseListener не имеет полномочий принимать решение о том что нужно разлогинить, это ответственность AuthService.
NotificationResponseListener только обеспечивает входной трафик, не вдаваясь в суть входящих сообщений.
Farongy
Это как? Сама физика мира разделяет на входящий и исходящий потоки.
dstarakozhev Автор
Да, физически это 2 отдельных потока, но в этом и смысл дизайна, чтобы не обременять AuthService особенностями физики, а дать понятную абстракцию "запрос-ответ-реакция".
Вы же если в магазине заказываете товар которого нет в наличии приходите забирать его в этот же магазин, а не едете на склад.
Да, поездка на склад вполне рабочий вариант, но будет не очень удобно
Farongy
Причём здесь AuthService? Я спрашивал зачем вы пытаетесь процесс "разлогинить пользователя" запихать в нотификации?
Если у вас появится нотификация "предложить юзеру участие в акции". Вы пользователя к акции тоже будете в сервисе нотификаций подключать?
dstarakozhev Автор
Не, ни в коем случае я этого не пытаюсь сделать,
задача NotificztionService соединить запрос с ответом и отдать событие о том, что ответ получен сервисуу который генерировал запрос. В нашем случае AuthService, а уже его задача принять решение о разлогине.
Согласен что в примере не однозначно отразил идею, подумаю как скорректировать. Хотя я явно прописал, что в NotificationService нельзя явно вызывать логику разлогина