Если вы хоть раз разрабатывали проект на Spring, наверняка сталкивались с циклическими зависимостями. Эта проблема настолько распространена и одновременно болезненна, что о ней можно услышать как от спикеров, так и от участников крупнейших технических конференций. На недавнем JPoint один из разработчиков рассказал мне, как вручную избавлялся от циклических зависимостей на огромном боевом проекте, изучая гигантскую диаграмму зависимостей.

К счастью, современные инструменты разработчика позволяют решать эту задачу намного быстрее и эффективнее. В этой статье я рассмотрю причины возникновения циклических зависимостей, продемонстрирую наиболее типичные случаи и расскажу о нескольких способах их решения, используя профессиональные инструменты разработчика. Материал также доступен в видео-формате.

Смотреть на YouTube: https://youtu.be/g7mlFwTEbDU
Смотреть в VK Видео:
https://vk.com/video-222549074_456239183

Реальный пример циклической зависимости

Давайте не будем брать заезженный пример с простыми зависимостями типа "бин А зависит от бина Б". Вместо этого рассмотрим ситуацию, которая вполне могла бы произойти на реальном проекте.

Предположим, что на проекте все сервисы представлены в виде интерфейсов, а их реализации инжектируются в бины через названия интерфейсов. Такая практика помогает в тестировании и соблюдении принципов SOLID. Однако у неё есть и обратная сторона: при возникновении циклической зависимости Spring в своих логах показывает название бина (именно реализации), а не интерфейса, что делает идентификацию и поиск проблемы менее очевидными.

Описанный далее пример можно более подробно изучить на GitHub. Предположим, что у нас есть приложение для работы с продуктами. Конкретно та часть этого приложения, которая нас будет сегодня интересовать представлена ниже в виде схемы. Я покажу только те классы, с которыми работал для решения своей задачи. Нюансы остальных классов, которые используются в ProductController и ProductServiceImpl демонстрировать не буду.

Отмечу, что на схеме указаны именно названия Java-классов, а не названия Spring-бинов. Давайте попробуем запустить наше приложение. 

В приложенном скриншоте видно, что Spring сообщает о циклической зависимости между бинами productServiceImpl и yandexS3Storage

И если мы вернемся к изначальной схеме, то никакого yandexS3Storage ни в виде названия поля, ни в виде названия класса мы не обнаружим. 

В текущей ситуации я бы мог начать изучение всех полей класса ProductServiceImpl, для выяснения того, какое же из полей в действительности является бином yandexS3Storage

Либо я мог бы пойти от обратного и перейти в класс YandexS3Storage, FQN которого явно указан в логах, и отталкиваясь от него понять, какое же поле в ProductServiceImpl связано с классом YandexS3Storage

Оба варианта потребуют от меня много времени и серьезной концентрации, чтобы разобраться с проблемой. Однако в наши дни многие задачи можно доверить профессиональным инструментам разработчика, что значительно упростит работу.

Для визуального анализа можно использовать инструмент Bean Navigation от Amplicode. Он позволяет наглядно увидеть все зависимости между бинами и сразу подсвечивает циклические зависимости, ещё до запуска приложения.

С таким подходом обнаружить источник проблемы становится значительно проще, особенно если граф зависимостей достаточно большой.

В нашем случае с помощью Bean Navigation удалось быстро обнаружить проблему: реализация YandexS3Storage неожиданно зависела от интерфейса ProductService, что нарушало правильную архитектуру приложения и создавало цикл. Теперь, когда проблема выявлена, давайте перейдём к способам её решения

Возможные пути решения проблемы

Существует четыре основных способа решения проблемы циклических зависимостей в Spring-приложениях:

1. Использование аннотации @Lazy

Это самый простой и наиболее распространённый способ. Достаточно отметить точку инжекции одного из бинов, участвующих в цикле, аннотацией @Lazy

Важно отметить, что данный подход делает ленивой только саму инжекцию, а не весь бин. Оба бина будут инициализированы жадно, но зависимость будет подгружена позже.

Если вам необходимо полностью ленивое поведение (инициализация только при обращении к бину), то стоит использовать аннотацию @Lazy как в точке инжекции, так и в точке объявления самого бина.

2. Свойство allow-circular-references

Данный способ Spring сам предлагает в логах при обнаружении циклической зависимости, однако он считается "крайним случаем". 

Если выставить свойство allow-circular-references в true, то в некоторых случаях Spring может разрешить циклические зависимости. Однако это будет работать только с инжекцией через поля (field-based) или через сеттеры (setter-based). Constructor-based инжекция не поддерживается в данном случае, и приложение всё равно не запустится.

Примечательно, что до версии Spring Boot 2.6 это свойство было включено по умолчанию, что приводило к неожиданному поведению при обновлении версии.

3. Свойство spring.main.lazy-initialization

Ещё один нерекомендуемый подход, при котором абсолютно все бины становятся ленивыми, использование свойства spring.main.lazy-initialization=true

Приложение запустится, но проблема останется: при первом обращении к сервисам, участвующим в циклической зависимости, произойдет ошибка. Это решение (если его таковым вообще можно назвать) можно считать самым неудачным, поскольку оно лишь откладывает обнаружение ошибки на этап работы приложения.

4. Пересмотр логики приложения

Самое правильное решение, хоть и не всегда самое простое, заключается в пересмотре архитектуры и логики приложения. Обычно циклические зависимости свидетельствуют о проблемах дизайна. Стоит задаться вопросом: действительно ли циклическая зависимость необходима? Часто ответ будет отрицательным, и проблему удастся устранить с помощью грамотного рефакторинга.

В нашем конкретном примере реальным решением стало именно переписывание логики приложения. Было обнаружено, что YandexS3Storage почему-то включал в свою зону ответственности ProductService, что явно нарушало принципы проектирования.

После рефакторинга эта проблема была устранена. И циклические зависимости более не отображались в Bean Navigation от Amplicode, а приложение без проблем запустилось и корректно заработало.

Подводя итоги

Циклические зависимости — частая головная боль при разработке Spring-приложений. Решений много, но самое надёжное — пересмотреть архитектуру и устранить саму причину цикла. Иногда, конечно, выручает аннотация @Lazy, особенно когда сроки поджимают. Но лучше всё же проектировать систему так, чтобы до этого не доходило. Продуманная архитектура — залог стабильности и спокойствия в будущем.

Рисунок
Рисунок

Подписывайтесь на наши Telegram и YouTube, чтобы не пропустить новые материалы про Amplicode, Spring и связанные с ним технологии!  

А если вы хотите попробовать Amplicode в действии – то можете установить его абсолютно бесплатно уже сейчас, как в IntelliJ IDEA/GigaIDE, так и в VS Code

Комментарии (1)


  1. Kinski
    17.07.2025 08:18

    Давайте не будем брать заезженный пример с простыми зависимостями типа "бин А зависит от бина Б"

    Берём пример где бин А зависит от бина Б)

    По вашим трём скринам можно догадаться что yandex storage это имплементация FileUploader, без всяких рекламных плагинов)

    Для рекламы могли бы постараться и получше пример придумать)