🍃 Раздел 5 · Вопрос #2

В чём разница между constructor, setter и field injection

В экосистеме Spring существует три способа передачи зависимостей в бин: через конструктор, через сеттер и напрямую в поле:


🟢 Junior Level

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

В экосистеме Spring существует три способа передачи зависимостей в бин: через конструктор, через сеттер и напрямую в поле:

// 1. Constructor Injection (Внедрение через конструктор) — РЕКОМЕНДУЕМЫЙ СТАНДАРТ
@Service
public class OrderService {
    private final OrderRepository repository;

    // В Spring 4.3+ аннотация @Autowired над единственным конструктором не обязательна
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

// 2. Setter Injection (Внедрение через сеттер) — ДЛЯ ОПЦИОНАЛЬНЫХ ЗАВИСИМОСТЕЙ
@Service
public class OrderService {
    private NotificationService notifier;

    @Autowired(required = false)
    public void setNotifier(NotificationService notifier) {
        this.notifier = notifier;
    }
}

// 3. Field Injection (Внедрение через рефлексию в поле) — АНТИПАТТЕРН
@Service
public class OrderService {
    @Autowired
    private OrderRepository repository; // Внедряется через рефлексию
}

2. Сравнительная таблица способов

Критерий Constructor Injection Setter Injection Field Injection
Иммутабельность (final) ✅ Да (поля final) ❌ Нет ❌ Нет
Гарантия инициализации ✅ Объект всегда валиден ⚠️ Можно забыть вызвать ⚠️ Поля null до внедрения
Чистые Unit-тесты (new) ✅ Да (new Service(mock)) ⚠️ Требует ручных сеттеров ❌ Требует Mockito/Рефлексии
Явность контракта ✅ Зависимости видны в API ⚠️ Размыты по методам ❌ Зависимости скрыты
Рекомендация Spring 🟢 Основной стандарт (99%) 🟡 Только для опциональных 🔴 Запрещен в Production

🟡 Middle Level

1. Фатальные недостатки Field Injection

Несмотря на визуальную краткость, внедрение в поля через @Autowired признано сообществом и командой Spring тяжелым антипаттерном:

  1. Нарушение инкапсуляции и сокрытие зависимостей:
    Класс с Field Injection имеет пустой публичный конструктор new OrderService(). Разработчик, создавая экземпляр в тестах или коде, ожидает рабочий сервис, но при вызове любого бизнес-метода получает NullPointerException.
  2. Невозможность объявить поля как final:
    Поля остаются мутабельными. Зависимость может быть случайно или намеренно перезаписана в рантайме.
  3. Жесткая привязка к DI-контейнеру в Unit-тестах:
    Чтобы протестировать такой класс без поднятия контекста Spring, разработчику приходится использовать рефлексивные костыли (ReflectionTestUtils.setField(...)) или аннотации @InjectMocks от Mockito.
  4. Маскировка нарушения Single Responsibility Principle (SRP):
    Когда в класс добавляют 10 полей с @Autowired, код визуально кажется компактным. Но если переписать это на конструктор с 10 аргументами, разработчик сразу заметит, что класс перегружен и требует декомпозиции.

2. Хронология фаз инициализации бина

Один из частых источников багов — непонимание порядка, в котором Spring заполняет объект:

[ 1. Вызов конструктора ] ──► Constructor Injection
           │
           ▼
[ 2. Инстанцирование объекта ]
           │
           ▼
[ 3. Инъекция в поля ]    ──► Field Injection (через AutowiredAnnotationBeanPostProcessor)
           │
           ▼
[ 4. Вызов сеттеров ]     ──► Setter Injection
           │
           ▼
[ 5. Вызов @PostConstruct ]──► Бин полностью собран, все зависимости доступны

[!WARNING] Если попытаться обратиться к полю с @Autowired внутри конструктора класса, вы гарантированно получите NullPointerException, потому что на момент выполнения конструктора объект только создается, а Field Injection произойдет позже!

@Service
public class ReportService {
    @Autowired
    private DataRepository repository;

    public ReportService() {
        // ❌ ОШИБКА: repository здесь ВСЕ ЕЩЕ РАВЕН NULL!
        repository.loadInitialData(); // Выбросит NullPointerException
    }

    @PostConstruct
    public void init() {
        // ✅ ПРАВИЛЬНО: к этому моменту поля заполнены
        repository.loadInitialData();
    }
}

🔴 Senior Level

1. Архитектура под капотом: AutowiredAnnotationBeanPostProcessor

Внедрение в поля и сеттеры не является частью компилятора Java — это работа специального компонента Spring: AutowiredAnnotationBeanPostProcessor (BPP):

  1. На этапе запуска Spring сканирует метаданные BeanDefinition целевого класса.
  2. После того как фабрика создала «сырой» экземпляр класса через конструктор без параметров, вызывается метод postProcessProperties().
  3. BPP находит все поля с аннотацией @Autowired, принудительно открывает к ним доступ через рефлексию (Field.setAccessible(true) или MethodHandles.privateLookupIn()), находит подходящие бины в контексте и записывает ссылки напрямую в область памяти объекта.
  4. Это порождает накладные расходы при старте и обходит проверки безопасности JVM.

В случае Constructor Injection контейнер разрешает зависимости на этапе разрешения конструктора (ConstructorResolver) и передает их как аргументы в Constructor.newInstance(args). Рефлексивного взлома приватных полей не происходит, а JVM гарантирует безопасную публикацию (Safe Publication) final-полей по спецификации JMM (Java Memory Model).


2. Поведение при циклических зависимостях

  • Constructor Injection: если Сервис $A$ требует в конструкторе Сервис $B$, а Сервис $B$ требует в конструкторе $A$, Spring физически не может создать ни один объект. Приложение падает в момент старта с ошибкой BeanCurrentlyInCreationException. Это проявление принципа Fail-Fast: разработчик мгновенно видит архитектурную ошибку еще до деплоя.
  • Field Injection: Spring может сначала инстанцировать оба сырых объекта через дефолтные конструкторы, а затем с помощью трехслойного кэша синглтонов перекрестно заполнить поля. Проблема маскируется, но может выстрелить в рантайме при инициализации прокси или вызове методов в @PostConstruct. Начиная со Spring Boot 2.6, такие циклы запрещены по умолчанию.

3. Идиоматичный подход: Constructor Injection + Lombok

Чтобы избавиться от шаблонного бойлерплейта при объявлении конструкторов, современным промышленным стандартом является использование аннотации @RequiredArgsConstructor библиотеки Lombok:

@Service
@RequiredArgsConstructor
public class UserService {
    // Lombok автоматически создаст конструктор со всеми final-полями:
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final EmailService emailService;
}

🎯 Шпаргалка для интервью

30-секундный ответ (Elevator Pitch)

«В Spring существует три типа DI:

  1. Constructor Injection — золотой стандарт разработки. Обеспечивает иммутабельность (final), гарантирует полную инициализированность объекта, исключает NPE, обеспечивает легкое написание unit-тестов без Spring и реализует принцип Fail-Fast при циклических зависимостях.
  2. Setter Injection — используется только для опциональных зависимостей через @Autowired(required = false).
  3. Field Injection — признанный антипаттерн: скрывает реальные зависимости класса, не позволяет объявить поля final, требует костылей рефлексии в тестах и маскирует нарушение SRP».

3 каверзных вопроса на интервью с ответами

1. Почему обращение к полю, помеченному @Autowired, внутри конструктора класса приводит к NullPointerException?

Ответ: Жизненный цикл бина строго разделен на этапы. Сначала вызывается конструктор для инстанцирования объекта в памяти. Только после завершения конструктора управление переходит к AutowiredAnnotationBeanPostProcessor, который с помощью рефлексии находит поля с @Autowired и внедряет в них ссылки. Поэтому внутри самого конструктора поля еще не инициализированы и содержат null. Для пост-инициализации следует использовать аннотацию @PostConstruct.

2. Нужно ли ставить аннотацию @Autowired над конструктором в современных версиях Spring?

Ответ: Начиная со Spring Framework 4.3, если в бине объявлен ровно один конструктор, аннотация @Autowired является опциональной — Spring понимает намерение автоматически и инжектит все параметры конструктора. Если же в классе объявлено два или более конструкторов, @Autowired необходимо явно указать над тем конструктором, который контейнер должен использовать для создания бина.

3. В чём преимущество Constructor Injection с точки зрения Java Memory Model (JMM)?

Ответ: Поля при внедрении через конструктор объявляются как final. Согласно спецификации JMM (§17.5 Final Field Semantics), после завершения конструктора ссылки на значения final-полей безопасно публикуются для всех потоков без необходимости использования volatile или синхронизации. При Field Injection поля остаются не-final, что в условиях многопоточного доступа требует дополнительных мер для гарантии видимости изменений между ядрами CPU.


Типичные ошибки и Red Flags

  • ❌ Использование @Autowired на полях в новом коде: Свидетельствует о непонимании архитектурных принципов и проблем тестируемости.
  • ❌ Попытка вызвать зависимость в конструкторе при Field Injection: Приводит к неизбежному NullPointerException.
  • ❌ Использование Setter Injection для критически важных зависимостей: Оставляет возможность оставить объект в недоинициализированном состоянии.
  • ❌ Раздувание конструктора до 10–15 параметров через @RequiredArgsConstructor: Хотя Lombok скрывает код конструктора, это явный индикатор нарушения SRP — класс должен быть разбит на несколько сервисов.

Связанные темы