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

Как работает внедрение зависимостей?

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

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

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

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

Как работает внедрение зависимостей (Dependency Injection)

Концепция

Dependency Injection (DI) — это паттерн проектирования, который решает проблему управления зависимостями объектов. Вместо того, чтобы объект сам создавал свои зависимости, они внедряются извне.

Проблема без DI (❌ плохо)

public class UserService {
    // UserService сам создает свои зависимости
    private UserRepository repository = new DatabaseUserRepository();
    private EmailService emailService = new GmailEmailService();
    private NotificationService notificationService = new PushNotificationService();
    
    public void registerUser(UserRequest request) {
        User user = repository.save(new User(request));
        emailService.sendWelcomeEmail(user);
        notificationService.notify(user, "Добро пожаловать!");
    }
}

Проблемы:

  1. Тесты невозможны — нельзя использовать mock, будет создана реальная база и реальная почта
  2. Жесткая связанность — изменить реализацию сложно
  3. Сложно масштабировать — каждый сервис создает все свои зависимости
  4. Неправильная ответственность — UserService занимается не только бизнес-логикой, но и созданием объектов

Решение с DI (✓ хорошо)

public class UserService {
    // Зависимости внедряются, а не создаются
    private final UserRepository repository;
    private final EmailService emailService;
    private final NotificationService notificationService;
    
    // Constructor Injection
    public UserService(UserRepository repository,
                      EmailService emailService,
                      NotificationService notificationService) {
        this.repository = repository;
        this.emailService = emailService;
        this.notificationService = notificationService;
    }
    
    public void registerUser(UserRequest request) {
        User user = repository.save(new User(request));
        emailService.sendWelcomeEmail(user);
        notificationService.notify(user, "Добро пожаловать!");
    }
}

Преимущества:

  1. Легко тестировать — подставляем mock реализации
  2. Слабая связанность — работает с интерфейсами
  3. Гибко масштабировать — контейнер управляет созданием объектов
  4. Правильная ответственность — сервис только о бизнес-логике

Механизм: как работает DI в Spring

Этап 1: Регистрация beans

@Configuration
public class AppConfig {
    // Регистрируем beans — инструкция Spring как создавать объекты
    
    @Bean
    public UserRepository userRepository() {
        return new DatabaseUserRepository();
    }
    
    @Bean
    public EmailService emailService() {
        return new GmailEmailService();
    }
    
    @Bean
    public UserService userService(UserRepository repository,
                                   EmailService emailService) {
        return new UserService(repository, emailService);
    }
}

// Или с аннотациями (Stereotype Annotations)
@Repository
public class DatabaseUserRepository implements UserRepository { }

@Service
public class EmailService { }

@Service
public class UserService {
    private final UserRepository repository;
    private final EmailService emailService;
    
    // Spring автоматически увидит конструктор и внедрит зависимости
    public UserService(UserRepository repository, EmailService emailService) {
        this.repository = repository;
        this.emailService = emailService;
    }
}

Этап 2: Инициализация ApplicationContext

Жизненный цикл Spring контейнера:

1. PARSING (Разбор конфига)
   ↓
2. BEAN REGISTRATION (Регистрация beans)
   Spring сканирует классы с @Bean, @Service, @Repository и т.д.
   ↓
3. DEPENDENCY RESOLUTION (Разрешение зависимостей)
   Spring анализирует конструкторы, setters, fields
   Строит граф зависимостей
   ↓
4. INSTANTIATION (Создание объектов)
   Spring создает beans в правильном порядке
   (сначала зависимости, потом объекты которые их используют)
   ↓
5. INJECTION (Внедрение)
   Spring внедряет зависимости в каждый bean
   ↓
6. INITIALIZATION (Инициализация)
   Вызывает @PostConstruct методы
   ↓
7. READY (Готово)
   ApplicationContext ready к использованию

Три способа внедрения зависимостей

1. Constructor Injection (рекомендуется)

@Service
public class UserService {
    private final UserRepository repository;  // Final = immutable
    private final EmailService emailService;    // Обязательны при создании
    
    // Spring вызывает этот конструктор и внедряет зависимости
    public UserService(UserRepository repository, EmailService emailService) {
        this.repository = repository;
        this.emailService = emailService;
    }
}

Преимущества:

  • Очевидны все зависимости
  • Объект immutable (final поля)
  • Легко тестировать
  • Невозможно создать объект без всех зависимостей

Как это работает в Spring:

Spring видит конструктор с параметрами
  ↓
Для каждого параметра ищет bean по типу
  ↓
Находит UserRepository bean и EmailService bean
  ↓
Передает их в конструктор
  ↓
Создает новый UserService(repository, emailService)

2. Setter Injection

@Service
public class UserService {
    private UserRepository repository;     // Не final
    private EmailService emailService;       // Опциональны
    
    @Autowired
    public void setRepository(UserRepository repository) {
        this.repository = repository;
    }
    
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}

Недостатки:

  • Можно случайно создать объект без зависимостей
  • Объект mutable (можно изменить после создания)
  • Не ясны обязательные зависимости

3. Field Injection

@Service
public class UserService {
    @Autowired
    private UserRepository repository;     // Прямое внедрение в поле
    
    @Autowired
    private EmailService emailService;
}

Недостатки:

  • Сложнее тестировать (нужна рефлексия для внедрения в тестах)
  • Не ясны зависимости
  • Сложно понять, какие поля обязательны

Как Spring находит beans для внедрения

1. По типу (Type)

// Spring ищет bean типа UserRepository
@Service
public class UserService {
    public UserService(UserRepository repository) { }  // Найдет DatabaseUserRepository
}

@Repository
public class DatabaseUserRepository implements UserRepository { }

2. По имени (Name)

@Bean
public UserRepository userRepository() {
    return new DatabaseUserRepository();
}

@Service
public class UserService {
    // Если несколько реализаций, можно указать имя
    public UserService(@Qualifier("userRepository") UserRepository repo) { }
}

3. По аннотациям

@Qualifier("primary")  // Приоритет
@Repository
public class DatabaseUserRepository implements UserRepository { }

@Primary  // Альтернатива — эта реализация по умолчанию
@Repository
public class CachedUserRepository implements UserRepository { }

Пример полного процесса DI

// Шаг 1: Регистрируем beans
@Configuration
public class Config {
    @Bean
    public DataSource dataSource() {
        return new DataSource("jdbc:postgresql://localhost/mydb");
    }
    
    @Bean
    public UserRepository userRepository(DataSource dataSource) {
        return new UserRepository(dataSource);
    }
    
    @Bean
    public EmailService emailService() {
        return new EmailService();
    }
    
    @Bean
    public UserService userService(UserRepository repository,
                                   EmailService emailService) {
        return new UserService(repository, emailService);
    }
}

// Шаг 2: Приложение
public class Application {
    public static void main(String[] args) {
        // Spring создает ApplicationContext
        ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        
        // Получаем bean из контекста
        UserService userService = context.getBean(UserService.class);
        
        // Используем (все зависимости уже внедрены)
        userService.registerUser(new UserRequest("john@example.com"));
    }
}

// Шаг 3: Тестирование (подставляем mocks)
@SpringBootTest
class UserServiceTest {
    @MockBean
    private UserRepository userRepository;  // Mock вместо реального
    
    @Autowired
    private UserService userService;  // Spring внедрит сервис с mock'ом
    
    @Test
    void testRegisterUser() {
        // Настраиваем mock
        Mockito.when(userRepository.save(any()))
            .thenReturn(new User(1L, "john@example.com"));
        
        // Тестируем
        userService.registerUser(new UserRequest("john@example.com"));
        
        // Проверяем
        Mockito.verify(userRepository).save(any());
    }
}

Жизненный цикл beans

Bean жизненный цикл:

1. INSTANTIATION
   Создается экземпляр класса (через конструктор или фабрику)
   ↓
2. POPULATE PROPERTIES
   Вызываются setters для свойств
   ↓
3. SET BEAN NAME
   Если реализует BeanNameAware, вызывается setBeanName()
   ↓
4. SET FACTORY
   Если реализует BeanFactoryAware, вызывается setBeanFactory()
   ↓
5. POST PROCESS BEFORE INIT
   Вызываются BeanPostProcessor.postProcessBeforeInitialization()
   ↓
6. INIT METHOD
   Вызывается @PostConstruct или initMethod
   ↓
7. POST PROCESS AFTER INIT
   Вызываются BeanPostProcessor.postProcessAfterInitialization()
   ↓
8. USE
   Bean готов к использованию
   ↓
9. DESTROY
   При shutdown контекста вызывается @PreDestroy или destroyMethod

Пример:

@Service
public class UserService implements InitializingBean, DisposableBean {
    private UserRepository repository;
    
    public UserService(UserRepository repository) {
        System.out.println("1. Конструктор вызван");
        this.repository = repository;
    }
    
    @PostConstruct
    public void init() {
        System.out.println("2. PostConstruct вызван - инициализация");
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("3. afterPropertiesSet вызван");
    }
    
    public void doWork() {
        System.out.println("4. Основной метод вызван");
    }
    
    @PreDestroy
    public void cleanup() {
        System.out.println("5. PreDestroy вызван - очистка");
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("6. destroy вызван");
    }
}

Spring Bean Scopes

// SINGLETON (по умолчанию) — один экземпляр на весь ApplicationContext
@Bean
@Scope("singleton")
public UserService userService() { }

// PROTOTYPE — новый экземпляр каждый раз
@Bean
@Scope("prototype")
public UserService userService() { }

// REQUEST — один экземпляр на HTTP request
@Bean
@Scope("request")
public UserService userService() { }

// SESSION — один экземпляр на HTTP session
@Bean
@Scope("session")
public UserService userService() { }

Резюме

Dependency Injection — это:

  1. Паттерн — объекты получают зависимости извне
  2. Принцип — Inversion of Control (IoC) — управление жизненным циклом
  3. Инструмент в Spring — ApplicationContext создает и управляет beans
  4. Преимущество — код становится тестируемым, гибким, масштабируемым

В Spring это работает благодаря:

  • Отражению (рефлексия для анализа конструкторов)
  • Аннотациям (@Bean, @Service, @Autowired)
  • Регистрации beans в ApplicationContext
  • Разрешению зависимостей по типам