Всем привет!
Сейчас я работаю 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 не применяется: Перехватчик транзакций не срабатывает, так как вызов не проходит через прокси
Как обойти:
Из доки спринга:
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 поднимает контекст:
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 @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.
В следующей шпаргалке мы разберём индексы, транзакции и все ключевые моменты, связанные с работой с базами данных)
Всем спасибо за внимание, удачных собесов и хорошего дня!)
