Как решать циклические зависимости
Что проверяют
Понимаете ли вы, что цикл это симптом неверных границ, и знаете ли, почему обходные приёмы плохи.
Как отвечать
Разделите уровни, на которых возникает проблема.
Уровень бинов Spring. Два сервиса внедряют друг друга. С Spring Boot 2.6 это запрещено по умолчанию, и приложение не стартует. Обходы существуют: аннотация ленивой инициализации, внедрение через сеттер, получение зависимости из контекста вручную. Все они лечат симптом: цикл остаётся, просто разрывается во времени. Правильные решения:
- вынести общее в третий компонент. Обычно оказывается, что оба сервиса зовут друг у друга одну и ту же часть, и ей место отдельно;
- пересмотреть границы: если A постоянно дёргает B, а B дёргает A, скорее всего это один сервис или граница проведена не там;
- развязать событием: вместо прямого вызова публикуется событие, а второй компонент на него подписывается. Зависимость становится односторонней.
Уровень модулей и пакетов. Здесь работает инверсия зависимостей: интерфейс объявляется в том модуле, который его использует, а реализация лежит в другом. Стрелка компиляции разворачивается, цикл исчезает. Второй вариант это выделение общего модуля с моделями и контрактами.
Уровень сервисов. Синхронный вызов туда и обратно между двумя сервисами это не только цикл, но и удвоенная задержка и связанность по доступности. Лечится тем же: событиями, локальной копией нужных данных или объединением сервисов, если они на самом деле про одну ответственность.
Про обнаружение: циклы удобно ловить автоматически. В экосистеме Java для этого есть ArchUnit, где правило «пакеты не должны образовывать циклов» проверяется обычным тестом и падает при нарушении.
Про конструкторы: именно внедрение через конструктор делает цикл видимым сразу. Это довод в его пользу, а не недостаток.
Где спотыкаются
Разрывают цикл ленивой инициализацией и считают проблему решённой.
Переходят на внедрение через поля, чтобы обойти ошибку старта.
Не замечают циклов между пакетами, пока проект не станет неразбираемым.
Считают событие универсальным решением и превращают явный вызов в неявную цепочку.