Всем привет!

Сейчас я работаю Senior Java Developer в одном из банков, и за последние годы мне довелось пройти множество собеседований, столкнуться с десятками непростых вопросов и вложить кучу времени в подготовку. И со временем я заметил одну закономерность: Spring — одна из самых объёмных и любимых тем на Java‑собеседованиях, причём спрашивают её у кандидатов любого уровня.

Поэтому в этой статье я хочу помочь вам уверенно подготовиться к вопросам по Spring, также покажу примеры задач, которые дают на собеседованиях. Поехали!

В профиле уже есть первая и вторая часть для подготовки:

  1. Многопоточность без боли

  2. JVM + Память + GC без боли

❗❗Дисклеймер❗❗

Эта статья не является учебником по технологиям. Здесь я не буду углубляться в то, как всё работает под капотом или почему это устроено именно так. Это сжатая методичка по вопросам на собеседованиях — только факты, без лишней воды!

Spring Core

Inversion of Control

Inversion of Control — это принцип, при котором создание объектов и передача им зависимостей передаётся специальному контейнеру (например, Spring), чтобы сами классы не управляли этим процессом и не знали, откуда берутся их зависимости.

Виды внедрения зависимостей

  • Внедрение через поле с помощью аннотации — Autowired

@Autowired
private UserService userService;
  • Внедрение через конструктор(самый популярный вариант)

@Service
public class TestService {
    
    private final ProcessService processService;

    public TestService(ProcessService processService) {
        this.processService = processService;
    }
}
  • Внедрение через Setter

@Service
public class TestService {

    private ProcessService processService;

    @Autowired
    public void setProcessService(ProcessService processService) {
        this.processService = processService;
    }

}

Отличия @Component, @Service, @Repository, @Controller

  • @Component — базовая аннотация; помечает любой класс как бин Spring.

  • @Service — тот же @Component, но семантически для бизнес‑логики; помогает читабельности и архитектурной структуре.

  • @Repository — @Component для DAO‑слоя; дополнительно перехватывает исключения базы и преобразует их в Spring DataAccessException.

  • @Controller — используется для веб‑слоя в MVC‑приложениях; по умолчанию возвращает HTML/шаблоны, а не JSON.

  • @RestController — это @Controller + @ResponseBody, и по умолчанию возвращает JSON

@ComponentScan

ComponentScan — это аннотация, которая указывает Spring, где искать классы с аннотациями @Component, @Service, @Repository, @Controller, @RestController а также @Configuration, чтобы автоматически создать бины — включая те, что определены через методы @Bean внутри этих конфигурационных классов, и передать их под управление контейнера.

Жизненный цикл бинов

  1. Создание — контейнер создаёт объект бина.

  2. Заполнение зависимостями — внедряются все зависимости (DI).

  3. Инициализация — вызываются методы инициализации:

    • аннотация @PostConstruct

    • если бин через @Bean, можно указать initMethod.

  4. Уничтожение — вызываются методы разрушения:

    • аннотация @PreDestroy

    • или destroyMethod у @Bean.

Bean Scopes

  • Singleton (по умолчанию) — один экземпляр на весь контейнер.

  • Prototype — новый экземпляр при каждом запросе бина.

  • Request / Session / Application — для веб‑приложений, создаются на один HTTP‑запрос, сессию или приложение.

Подводный камень: если внедрить Prototype внутрь Singleton, Spring создаст только один экземпляр при создании Singleton, а не новый каждый раз.

BeanFactoryPostProcessor и BeanPostProcessor

  • BeanFactoryPostProcessor — позволяет изменить метаданные бинов до их создания контейнером. Пример: PropertySourcesPlaceholderConfigurer.

  • BeanPostProcessor — перехватывает уже созданный бин перед использованием. На этом основано проксирование, AOP и @Transactional. Методы вызываются в порядке: postProcessBeforeInitialization → инициализация → postProcessAfterInitialization.

Spring AOP

Spring AOP — это механизм аспектно‑ориентированного программирования, который позволяет вынести повторяющуюся функциональность (логирование, транзакции, безопасность) в отдельные аспекты, не смешивая её с бизнес‑логикой.

Аспект — это модуль, содержащий advice (код, который выполняется до, после или вокруг метода) и pointcut (правила, где этот код применять).

Ограничения Spring AOP:

  • Вызовы внутри одного класса — методы вызывают друг друга напрямую, прокси не срабатывает.

  • Финальные классы и методы — CGLIB‑прокси не могут переопределить final, JDK‑прокси не работают с классами.

  • Private‑методы — прокси работает только с public/protected/package‑private методами, private не зааопишь.

CGLIB — это библиотека, которую Spring использует для создания прокси‑классов через наследование. Она позволяет оборачивать бин, если тот не реализует интерфейс (в отличие от JDK Dynamic Proxy, который работает только с интерфейсами).

То есть, AOP работает через прокси, и эти ограничения — прям следствие этого механизма.

С ограничением AOP часто на собесах дают задачки, пример:

@Service
public class OrderService {

    @PostConstruct
    public void init() {
        // Вызов транзакционного метода внутри @PostConstruct
        processPayment(); // @Transactional здесь не сработает
    }

     @Transactional
    public void processPayment() {
        System.out.println("Оплата выполнена");
    }
}

Почему не работает:

  1. Механизм проксирования: Spring создает прокси вокруг бина для обработки @Transactional

  2. Внутренний вызов: Когда вы вызываете processPayment() из init() того же класса, вы обходите прокси и вызываете метод напрямую

  3. AOP не применяется: Перехватчик транзакций не срабатывает, так как вызов не проходит через прокси

Как починить:

Вынести вызов в метод, который вызывается после инициализации бина, например через ApplicationListener или @EventListener(ContextRefreshedEvent.class):

@Component
public class StartupRunner {


    @EventListener(ContextRefreshedEvent.class)
    public void onApplicationEvent() {
        processPaymen(); // @Transactional сработае
    }

    @Transactional
    public void processPayment() {
        System.out.println("Оплата выполнена");
    }
}

@SpringBootApplication

Чтобы понять, что вызывает @SpringBootApplication и как работает, достаточно посмотреть в доку или зайти в аннотацию прям из IDEA:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication

Как Spring Boot поднимает контекст:

  1. Environment — собирает свойства и профили.

  2. ApplicationContext — создаёт и конфигурирует контейнер бинов.

  3. BeanFactory — регистрирует все бины и зависимости.

  4. Refresh — инициализация бинов и вызов lifecycle‑методов.

  5. Listeners — запускаются события приложения (ApplicationReadyEvent и др.).

  6. Embedded server — если это веб‑приложение, запускается встроенный сервер (Tomcat/Jetty).

То есть Boot проходит цепочку: конфигурация → создание бинов → инициализация → события → запуск веб‑сервера.

@Primary

  • Помечает бин как основной, если есть несколько кандидатов одного типа.

  • Spring автоматически выберет его при инжекции, если не указан @Qualifier.

@Primary
@Component
public class PaypalPaymentService implements PaymentService { }

@Component
public class StripePaymentService implements PaymentService { }

@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService; // выберется PaypalPaymentService
}

@Qualifier

Позволяет явно указать, какой бин использовать, даже если есть несколько кандидатов.

@Service
public class OrderService {

    @Autowired
    @Qualifier("stripePaymentService")
    private PaymentService paymentService; // выберется StripePaymentService
}

@Transactional

Я знаю, что уже есть миллион статей на тему @Transactional, как она работает под капотом, какие проблемы и тд, я же расскажу очень коротко:

  • Proxy вокруг метода — Spring создаёт прокси (JDK или CGLIB), которое перехватывает вызов метода и управляет транзакцией.

  • TransactionInterceptor — компонент, который оборачивает метод, открывает транзакцию до выполнения и коммитит/откатывает после.

  • Propagation — правила, как метод участвует в существующей транзакции:

    • REQUIRED — использовать существующую или создать новую транзакцию (Дефолт).

    • REQUIRES_NEW — всегда создать новую, приостанавливая текущую.

    • NESTED — вложенная транзакция, можно откатить частично.

    • SUPPORTS — использовать транзакцию, если есть, иначе работать без неё.

    • NOT_SUPPORTED — работать вне транзакции, приостанавливая существующую.

    • NEVER — выбросить ошибку, если транзакция уже есть.

    • MANDATORY — обязательно использовать существующую, иначе исключение.

  • Isolation levels — уровень изоляции: READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE и READ_UNCOMMITTED.

  • Почему транзакция может не работать:

    • приватный метод (прокси не видит вызов)

    • внутренний вызов метода того же класса (this.method())

    • final класс или final метод (CGLIB не может создать прокси)

    • вызов setRollbackOnly() без корректного отката

@Profile

@Profile - аннотация для условного включения бина или конфигурации в зависимости от активного профиля приложения:

@Configuration
@Profile("dev")
public class DevConfig {
    @Bean
    public DataSource dataSource() {
        return new H2DataSource();
    }
}

Важно: если бин помечен @Profile, а вы попытаетесь его внедрить в коде при неактивном профиле, Spring не найдёт этот бин, и приложение упадёт с ошибкой NoSuchBeanDefinitionException.

@ConditionalOnProperty

@ConditionalOnProperty — аннотация Spring Boot, которая позволяет создавать бин только если задано определённое свойство в application.properties или application.yml.

@Service
@ConditionalOnProperty(name = "app.payment.enabled", havingValue = "true")
public class PaymentService {
    // бин создастся только если feature.payment.enabled=true
}

Как внедрить несколько Bean, которые реализуют один интерфейс

Вопрос, который мне задавали не один раз)
Давайте представим, что у нас есть интерфейс PaymentService, у которого есть 2 реализации, и мы хотим в сервисе прогнать сразу 2 метода оплаты:

public interface PaymentService {
    void pay();
}

@Service("creditCardPayment")
public class CreditCardPaymentService implements PaymentService {
    @Override
    public void pay() {
        System.out.println("Оплата кредитной картой");
    }
}

@Service("paypalPayment")
public class PaypalPaymentService implements PaymentService {
    @Override
    public void pay() {
        System.out.println("Оплата через PayPal");
    }
}

Внедрение через List<PaymentService>

Часто используется внедрение через List<PaymentService>, чтобы получить все сервисы реализации:

@Service
public class OrderService {

    private final List<PaymentService> paymentServices;

    @Autowired
    public OrderService(List<PaymentService> paymentServices) {
        this.paymentServices = paymentServices;
    }

    public void processAllPayments() {
        paymentServices.forEach(PaymentService::pay);
    }
}

Внедрение через Map<String, PaymentService>

Мы можем внедрять такие бины через Map<String, PaymentService> В этом случае ключами будут имена бинов ("creditCardPayment" и "paypalPayment"), а значениями — соответствующие реализации.

@Service
public class OrderService {

    private final Map<String, PaymentService> paymentServices;

    @Autowired
    public OrderService(Map<String, PaymentService> paymentServices) {
        this.paymentServices = paymentServices;
    }

    public void processSpecificPayment(String type) {
        paymentServices.get(type).pay();
    }
}

Self injection

Self injection - это когда класс внедряет сам себя через Spring-контейнер, обычно через прокси.

Зачем нужен:

  • Чтобы вызвать собственный метод, аннотированный @Transactional или @Async, и чтобы прокси Spring корректно обработал аспект.

  • Прямой вызов метода через this не проходит через прокси, поэтому аннотации не сработают.

@Service
public class OrderService {

    @Autowired
    private OrderService self;

    @Transactional
    public void processOrder() {
        // код
    }

    public void startProcess() {
        self.processOrder(); // прокси сработает
    }
}

Итог

Сегодня мы рассмотрели ключевые аспекты Spring: работу с бинами, основные аннотации и подводные камни, которые часто всплывают на собеседованиях по Java/Kotlin. Список тем составлен на основе моего опыта и опыта коллег, проходивших собеседования на позиции от Junior до Senior.

В следующей шпаргалке мы разберём индексы, транзакции и все ключевые моменты, связанные с работой с базами данных)

Всем спасибо за внимание, удачных собесов и хорошего дня!)

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


  1. Elinkis
    18.11.2025 15:25

    Очень интересно изложено. Спасибо! Жду следующую часть!


  1. EAS120
    18.11.2025 15:25

    Очень жду статью про индексы.


  1. gsaw
    18.11.2025 15:25

    Мне кажется момент с вызовом из PostConstruct метода с Transactional аннотацией у вас неверный

    @Service
    public class OrderService {
    
        @Autowired
        private PaymentService paymentService;
    
        @PostConstruct
        public void init() {
            // Вызов транзакционного метода внутри @PostConstruct
            paymentService.processPayment(); // @Transactional здесь не сработает
        }
    }
    
    @Service
    public class PaymentService {
    
        @Transactional
        public void processPayment() {
            System.out.println("Оплата выполнена");
        }
    }


    Потому как оно в таком виде должно сработать и работает (только что попробовал у себя, а то вдруг я чего не знаю :) )

    Обороачивается в прокси же PaymentService и до того, как инстанс этого бина занижектится в OrderService. То-есть при вызове из PostConstruct все саработает как и ожидается. А для сомого OrderService должно быть все равно, обернуто оно в прокси или нет.

    Вот такой мой пример выдает "Stored entities 0" c Transactional. И выдает "Stored entities 2" если аннотацию убрать, так как в этом случае на каждый вызов repo.save создается своя транзакция.

    @RequiredArgsConstructor
    @Component
    public class BeanB {
        private final MyRepository repo;
    
        @Transactional
        public void doSomethingTransactional(){
            int i = 0;
            repo.save(new MyEntity());
            i++;
            repo.save(new MyEntity());
            i++;
            if(i > 1) {
                throw new RuntimeException("Boom");
            }
        }
    
        public Long count() {
            return  repo.count();
        }
    }
    
    @Slf4j
    @RequiredArgsConstructor
    @Component
    public class BeanA {
        private final BeanB b;
    
        @PostConstruct
        public void init(){
            try {
                b.doSomethingTransactional();
            } catch (Exception e) {
                log.info("Stored entities {}", b.count());
            }
        }
    }