Всем привет!
Сейчас я работаю Senior Java Developer в одном из банков, и за последние годы мне довелось пройти множество собеседований, столкнуться с десятками непростых вопросов и вложить кучу времени в подготовку. И со временем я заметил одну закономерность: Spring — одна из самых объёмных и любимых тем на Java‑собеседованиях, причём спрашивают её у кандидатов любого уровня.
Поэтому в этой статье я хочу помочь вам уверенно подготовиться к вопросам по Spring, также покажу примеры задач, которые дают на собеседованиях. Поехали!
В профиле уже есть первая и вторая часть для подготовки:
❗❗Дисклеймер❗❗
Эта статья не является учебником по технологиям. Здесь я не буду углубляться в то, как всё работает под капотом или почему это устроено именно так. Это сжатая методичка по вопросам на собеседованиях — только факты, без лишней воды!
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 внутри этих конфигурационных классов, и передать их под управление контейнера.
Жизненный цикл бинов
Создание — контейнер создаёт объект бина.
Заполнение зависимостями — внедряются все зависимости (DI).
-
Инициализация — вызываются методы инициализации:
аннотация @PostConstruct
если бин через @Bean, можно указать
initMethod.
-
Уничтожение — вызываются методы разрушения:
аннотация @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("Оплата выполнена");
}
}
Почему не работает:
Механизм проксирования: Spring создает прокси вокруг бина для обработки
@TransactionalВнутренний вызов: Когда вы вызываете
processPayment()изinit()того же класса, вы обходите прокси и вызываете метод напрямую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 поднимает контекст:
Environment — собирает свойства и профили.
ApplicationContext — создаёт и конфигурирует контейнер бинов.
BeanFactory — регистрирует все бины и зависимости.
Refresh — инициализация бинов и вызов lifecycle‑методов.
Listeners — запускаются события приложения (
ApplicationReadyEventи др.).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)

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()); } } }
Elinkis
Очень интересно изложено. Спасибо! Жду следующую часть!