Всем привет!

Сейчас я работаю 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 не применяется: Перехватчик транзакций не срабатывает, так как вызов не проходит через прокси

Как обойти:

Из доки спринга:

BeanPostProcessors are applied before any initialization methods (such as @PostConstruct)

В Spring Framework Reference явно указано, что BeanPostProcessors (которые создают AOP прокси) применяются до вызова @PostConstruct

@Component
public class OrderService {
    
    @Autowired
    private ApplicationContext context;
    
    @PostConstruct
    public void init() {
        context.getBean(MyService.class).processPayment();
    }

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

Такое решение с ApplicationContext должно сработать. Но сразу хочется сказать, скажите что нужно вынести в таком случае метод в отдельный бин например, если уж скажут, что нельзя и хотим именно в этом, тогда можно сказать про ApplicationContext

@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
    @Lazy
    private OrderService self;

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

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

@Cacheable

Есть такая прекрасная аннотация как @Cacheable , которая включает кэш на методе, чтобы при вызове метода с такими же параметрами, мы не выполняли его снова, а получали значение из кэша:

  @Cacheable(cacheNames = "test", key = "#test")
  public String test(int test) {
      return LocalDateTime.now().toString();
  }

И бывает, что спрашивают, похожий вопрос, как у транзакций, но с кэшем. Возьмем в качестве примера:

@Service
@EnableCaching
public class CacheClass {
    
    @SneakyThrows
    @PostConstruct
    public void init() {
        System.out.println(test(1));
        Thread.sleep(1000);
        System.out.println(test(2));
        Thread.sleep(1000);
        System.out.println(test(1));
    }

    @Cacheable(cacheNames = "test", key = "#integer")
    public String test(int integer) {
        return LocalDateTime.now().toString();
    }
}

Тут мы видим, что нет ни self injection, ни ApplicationContext, значит, что это не сработает, и если применить self injection или ApplicationContext, то все сработает, но для @Cacheable ситуация ещё хуже: кеш-аспект инициализируется позднее, поэтому вызов @Cacheable из @PostConstruct обычно не срабатываетоб этом есть явное issue в Spring

Тут вы можете применить ApplicationRunner или CommandLineRunner, которые смогу помочь, просто заимплементить его:

@Service
@EnableCaching
public class CacheClass implements ApplicationRunner {

    @Autowired
    @Lazy
    private CacheClass self;
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(self.test(1));
        Thread.sleep(1000);
        System.out.println(self.test(2));
        Thread.sleep(1000);
        System.out.println(self.test(1));
    }

    @Cacheable(cacheNames = "test", key = "#integer")
    public String test(int integer) {
        return LocalDateTime.now().toString();
    }
}

Код, чтобы поиграться с @Transaction

Чтобы проверить разные способы работы @Transaction я пользовался этим кодом:

@Service
public class TestClass {

    @Autowired
    @Lazy
    private TestClass self;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void init() {
        System.out.println("Тестируем @Transactional in @PostConstruct");

        // Тест 1: Прямой вызов (не должен работать)
        System.out.println("1. Прямой вызов:");
        try {
            directTransactionalMethod();
        } catch (Exception e) {
            System.out.println("Ошибка: " + e.getMessage());
        }

        // Тест 2: Через self + @Lazy
        System.out.println("2. self + @Lazy:");
        self.selfTransactionalMethod();

        // Тест 3: Через ApplicationContext
        System.out.println("3. ApplicationContext:");
        TestClass proxy = applicationContext.getBean(TestClass.class);
        proxy.contextTransactionalMethod();

        System.out.println("@PostConstruct выполнился");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void directTransactionalMethod() {
        checkTransaction("directTransactionalMethod");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void selfTransactionalMethod() {
        checkTransaction("selfTransactionalMethod");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void contextTransactionalMethod() {
        checkTransaction("contextTransactionalMethod");
    }

    private void checkTransaction(String methodName) {
        boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
        String transactionName = TransactionSynchronizationManager.getCurrentTransactionName();

        System.out.println("   " + methodName + ":");
        System.out.println("   - Активна ли транзакция: " + isActive);
        System.out.println("   - Имя транзакции: " + transactionName);
    }
}

Результат:

Тестируем @Transactional in @PostConstruct
1. Прямой вызов:
   directTransactionalMethod:
   - Активна ли транзакция: false
   - Имя транзакции: null
2. self + @Lazy:
   selfTransactionalMethod:
   - Активна ли транзакция: true
   - Имя транзакции: TestClass.selfTransactionalMethod
3. ApplicationContext:
   contextTransactionalMethod:
   - Активна ли транзакция: true
   - Имя транзакции: TestClass.contextTransactionalMethod

Он достаточно прост, с его помощью вы сможете попробовать разные способы или проверить свой)

Итог

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

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

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

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


  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());
            }
        }
    }