← Назад к вопросам

Что делать при закольцованности от взаимной инъекции классов

2.0 Middle🔥 201 комментариев
#SOLID и паттерны проектирования#Spring Framework

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

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблема циклической зависимости при инъекции зависимостей

Циклическая зависимость (circular dependency) возникает, когда класс A требует класс B, класс B требует класс C, а класс C требует класс A. В контексте инъекции зависимостей (DI) это может привести к невозможности создать экземпляры бинов, так как контейнер не может разрешить цепочку зависимостей.

Как это выглядит

@Component
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private final ServiceA serviceA;
    
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

В этом примере Spring не сможет создать ни ServiceA, ни ServiceB, так как каждый класс зависит от другого.

Основные решения

1. Рефакторинг архитектуры (рекомендуется)

Исключить циклическую зависимость, перепроектировав иерархию классов. Создать промежуточный сервис или использовать принцип разделения ответственности:

@Component
public class SharedService {
    // Общая логика
}

@Component
public class ServiceA {
    private final SharedService sharedService;
    
    public ServiceA(SharedService sharedService) {
        this.sharedService = sharedService;
    }
}

@Component
public class ServiceB {
    private final SharedService sharedService;
    
    public ServiceB(SharedService sharedService) {
        this.sharedService = sharedService;
    }
}

2. Использование @Lazy для отложенной инициализации

Помечить одну из зависимостей как lazy-loaded, чтобы она создавалась не при инициализации контейнера, а при первом обращении:

@Component
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private final ServiceA serviceA;
    
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

3. Инъекция через сеттеры вместо конструктора

Использовать setter-based injection для одной из зависимостей:

@Component
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private ServiceA serviceA;
    
    @Autowired
    public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

4. ObjectProvider для получения бина во время использования

Вместо прямой инъекции использовать ObjectProvider, который предоставляет доступ к бину при необходимости:

@Component
public class ServiceA {
    private final ObjectProvider<ServiceB> serviceBProvider;
    
    public ServiceA(ObjectProvider<ServiceB> serviceBProvider) {
        this.serviceBProvider = serviceBProvider;
    }
    
    public void doSomething() {
        ServiceB serviceB = serviceBProvider.getIfAvailable();
        if (serviceB != null) {
            // использовать serviceB
        }
    }
}

@Component
public class ServiceB {
    private final ServiceA serviceA;
    
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

Лучший подход

Первый вариант — рефакторинг архитектуры — является наиболее правильным решением. Циклическая зависимость часто указывает на нарушение принципов SOLID, особенно принципа единственной ответственности. Вместо того чтобы применять обходные решения, лучше пересмотреть дизайн и извлечь общую функциональность в отдельный компонент.

Если рефакторинг невозможен, следует использовать @Lazy — это наименее инвазивное решение, не меняющее логику кода.

Что делать при закольцованности от взаимной инъекции классов | PrepBro