Почему возникает циклическая зависимость при инжекции самого себя в Spring
Что проверяют
Знаете ли вы, как контейнер создаёт синглтоны, и понимаете ли, зачем бин вообще внедряют в самого себя. Почти всегда за этим стоит попытка обойти проксирование, и собеседующий ждёт именно такого объяснения.
Как отвечать
Опишите механику. При внедрении через конструктор контейнеру нужен готовый экземпляр до вызова конструктора, а сам бин в этот момент ещё создаётся, поэтому цикл не разрешается и старт падает с BeanCurrentlyInCreationException. При внедрении в поле или сеттер объект уже создан, ссылка на него доступна из кэша незавершённых синглтонов, и подстановка проходит.
Дальше про причину. Аннотации вроде @Transactional, @Cacheable и @Async работают через заместителя, а вызов this.method() идёт мимо него, поэтому на внутреннем вызове аннотация просто не срабатывает. Внедрение себя это попытка получить ссылку на заместителя и позвать метод через него.
Решения по убыванию предпочтительности:
- вынести метод в отдельный бин, тогда вызов становится внешним и цикл исчезает вместе с проблемой;
ObjectProviderили@Lazy, если разнести ответственность прямо сейчас нельзя;TransactionTemplateвместо аннотации, когда речь именно о транзакции.
Полезно упомянуть, что в свежих версиях Spring Boot циклические ссылки запрещены по умолчанию и включаются отдельным свойством, то есть старый трюк перестал работать молча.
Где спотыкаются
Утверждают, что контейнер разрешает любые циклы. Он разрешает их только через раннюю ссылку, то есть для синглтонов с внедрением в поле или сеттер. Конструкторный цикл не разрешается никогда.
Считают @Lazy решением. Приложение стартует, но зависимость остаётся, а ошибка инициализации переезжает со старта на первый пользовательский запрос, где её заметят позже и дороже.
Забывают, ради чего всё затевалось, и возвращаются к прямому вызову внутри класса. Аннотация в таком вызове игнорируется без единого предупреждения: транзакция не открывается, кэш не читается, и обнаруживают это обычно по расхождению данных.