RU | EN | DE

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_COMMITTEDREPEATABLE_READSERIALIZABLE и 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\>, чтобы получить все сервисы реализации:

@Servicepublic 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.