Що таке Bean scope
Коли ви оголошуєте клас як @Component або метод як @Bean, Spring повинен знати: створювати єдиний екземпляр на весь застосунок чи породжувати новий об'єкт під час кожного зверне...
🟢 Junior Level
Scope (область видимості біна) — це декларативна стратегія Spring IoC-контейнера, що визначає життєвий цикл, правила створення, кількість екземплярів та контекст зберігання конкретного біна.
Коли ви оголошуєте клас як @Component або метод як @Bean, Spring повинен знати: створювати єдиний екземпляр на весь застосунок чи породжувати новий об’єкт під час кожного звернення.
Базові області видимості
За замовчуванням у Spring усі біни мають область видимості Singleton.
| Scope | Опис | Скільки екземплярів у пам’яті |
|---|---|---|
singleton (за замовчуванням) |
Один екземпляр на весь ApplicationContext |
1 екземпляр на контейнер |
prototype |
Новий екземпляр створюється при кожному запиті з контейнера | Безліч екземплярів (N разів) |
Базовий приклад
package com.example.demo;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
// 1. Singleton (за замовчуванням, анотацію @Scope можна опустити)
@Service
public class OrderService {
// Єдиний екземпляр для всіх користувачів та потоків застосунку
}
// 2. Prototype (новий об'єкт при кожному впровадженні або виклику getBean)
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class InvoiceReportTask {
// Зберігає стан конкретного звіту
}
// Перевірка в коді:
OrderService s1 = context.getBean(OrderService.class);
OrderService s2 = context.getBean(OrderService.class);
System.out.println(s1 == s2); // true: посилання вказують на один і той самий об'єкт у Heap
InvoiceReportTask t1 = context.getBean(InvoiceReportTask.class);
InvoiceReportTask t2 = context.getBean(InvoiceReportTask.class);
System.out.println(t1 == t2); // false: створено два незалежні об'єкти
🟡 Middle Level
Архітектурний інтерфейс org.springframework.beans.factory.config.Scope
В основі механізму областей видимості лежить інтерфейс Scope. Саме через нього Spring абстрагує зберігання та отримання об’єктів:
package org.springframework.beans.factory.config;
public interface Scope {
// Отримати наявний об'єкт або створити новий через objectFactory
Object get(String name, ObjectFactory<?> objectFactory);
// Видалити об'єкт з області видимості (наприклад, при завершенні HTTP-сесії)
Object remove(String name);
// Зареєструвати зворотний виклик (callback), що виконається при знищенні області
void registerDestructionCallback(String name, Runnable callback);
// Дозволити контекстний об'єкт (наприклад, HttpServletRequest)
Object resolveContextualObject(String key);
// Отримати ідентифікатор сесії/розмови (наприклад, Session ID)
String getConversationId();
}
Коли викликається context.getBean(beanName), метод AbstractBeanFactory.doGetBean() аналізує scope біна:
- Якщо
mbd.isSingleton()$\to$ звертається доDefaultSingletonBeanRegistry.getSingleton(). - Якщо
mbd.isPrototype()$\to$ викликаєcreateBean()без збереження посилання в кеші. - Якщо інший scope $\to$ знаходить відповідну реалізацію
Scopeв мапіthis.scopesі викликаєscope.get(beanName, () -> createBean(...)).
Проблема невідповідності життєвих циклів (Scoped Mismatch)
Найпідступніша помилка при роботі зі scope: впровадження біна з коротким життєвим циклом (prototype, request) у бін із довгим життєвим циклом (singleton).
@Service // Singleton (живе весь час роботи застосунку)
public class OrderProcessor {
@Autowired // ПОМИЛКА: Prototype створиться лише ОДИН раз під час старту застосунку!
private PaymentSession paymentSession;
public void processOrder() {
// paymentSession буде одним і тим самим об'єктом для всіх замовлень та потоків!
paymentSession.pay();
}
}
Чому це відбувається:
Singleton-бін інстанціюється один раз під час старту контексту. У цей самий момент контейнер одноразово впроваджує всі його залежності. У результаті prototype-бін «заморожується» всередині синглтона і перестає оновлюватися під час наступних викликів.
🔴 Senior Level
4 способи розв’язання проблеми Scoped Mismatch
Спосіб 1: ObjectProvider<T> (Ідіоматичний та типобезпечний)
Механізм відкладеного отримання біна без прямого звернення до ApplicationContext:
@Service
public class OrderProcessor {
private final ObjectProvider<PaymentSession> paymentSessionProvider;
public OrderProcessor(ObjectProvider<PaymentSession> paymentSessionProvider) {
this.paymentSessionProvider = paymentSessionProvider;
}
public void processOrder() {
// При кожному виклику запитується новий актуальний екземпляр Prototype/Request
PaymentSession session = paymentSessionProvider.getObject();
session.pay();
}
}
Спосіб 2: Scoped Proxy (ScopedProxyMode.TARGET_CLASS)
Spring впроваджує в singleton-бін не сам цільовий об’єкт, а CGLIB-проксі. Проксі перехоплює виклики методів і на льоту делегує їх актуальному екземпляру:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserRequestContext {
private String userId;
// гетери, сетери
}
@Service
public class BillingService {
// Впроваджується CGLIB-проксі UserRequestContext
private final UserRequestContext context;
public BillingService(UserRequestContext context) {
this.context = context;
}
public void bill() {
// Виклик context.getUserId() через проксі звертається до RequestContextHolder (ThreadLocal)
// поточного HTTP-потоку і знаходить бін саме поточного запиту!
System.out.println("Processing user: " + context.getUserId());
}
}
Спосіб 3: Метод ін’єкції (@Lookup)
Spring за допомогою CGLIB перевизначає метод класу в рантаймі, змушуючи його звертатися до BeanFactory:
@Service
public abstract class ReportService {
public void generateReport() {
ReportTask task = getReportTask(); // CGLIB згенерує виклик context.getBean(ReportTask.class)
task.execute();
}
@Lookup
protected abstract ReportTask getReportTask();
}
Спосіб 4: Фабричний інтерфейс (Provider<T> із JSR-330 / Jakarta Inject)
Стандартний інтерфейс jakarta.inject.Provider<T>, аналогічний ObjectProvider:
@Service
public class OrderProcessor {
@Autowired
private Provider<PaymentSession> paymentSessionProvider;
}
Реалізація кастомного Scope: ThreadScope
У Spring можна створити власний scope для ізоляції бінів за будь-яким контекстом (наприклад, орендар у multitenancy, транзакція чи окремий потік):
package com.example.scope;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import java.util.HashMap;
import java.util.Map;
public class CustomThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> threadLocal =
ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = threadLocal.get();
return scope.computeIfAbsent(name, k -> objectFactory.getObject());
}
@Override
public Object remove(String name) {
return threadLocal.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// Реєстрація зворотних викликів очищення за потреби
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
public void clear() {
threadLocal.remove(); // Запобігання витокам пам'яті в пулах потоків!
}
}
Реєстрація у конфігурації:
@Configuration
public class ScopeConfig {
@Bean
public static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new CustomThreadScope());
return configurer;
}
}
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
Scope біна — це декларативна стратегія IoC-контейнера, що керує життєвим циклом та кількістю екземплярів об’єкта. Базовими є
singleton(один екземпляр наApplicationContext, кешується вsingletonObjects) таprototype(новий об’єкт при кожному запиті, контейнер не викликає@PreDestroy). У Web-застосунках додаютьсяrequest,session,applicationтаwebsocket. Архітектурно scope реалізується через інтерфейсorg.springframework.beans.factory.config.Scope.
4 каверзних питання з відповідями
1. Що станеться, якщо впровадити @Scope("prototype") бін у @Scope("singleton") сервіс через звичайний конструктор або @Autowired?
Відповідь: Prototype-бін створиться рівно один раз у момент ініціалізації синглтона. У результаті він виявиться «замороженим» (scoped mismatch), і при всіх наступних зверненнях до сервісу перевикористовуватиметься один і той самий екземпляр.
2. Як правильно впровадити короткоживучий бін (наприклад, Request або Prototype) у Singleton?
Відповідь: Існує 4 основні рішення:
- Використовувати
ObjectProvider<T>.getObject(). - Налаштувати Scoped Proxy:
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS). - Використовувати метод ін’єкції
@Lookup. - Використовувати
Provider<T>ізjakarta.inject.
3. Як працює ScopedProxyMode.TARGET_CLASS під капотом?
Відповідь: Spring генерує CGLIB-проксі для цільового класу. Цей проксі впроваджується в синглтон замість реального об’єкта. При кожному виклику будь-якого бізнес-методу проксі звертається до RequestContextHolder (ThreadLocal) або відповідної реалізації Scope, витягує актуальний для поточного потоку/запиту екземпляр біна та делегує йому виклик.
4. Де фізично зберігаються Singleton-біни в пам’яті JVM?
Відповідь: У карті singletonObjects (тип ConcurrentHashMap<String, Object>), розташованій у низькорівневому реєстрі DefaultSingletonBeanRegistry всередині DefaultListableBeanFactory.
Червоні прапорці (чого категорично не можна говорити)
- ❌ «Singleton у Spring — це те саме, що патерн Singleton GoF» — GoF Singleton гарантує один об’єкт на ClassLoader за рахунок приватного конструктора і static-поля; Spring Singleton гарантує один екземпляр на конкретний
ApplicationContext. - ❌ «Prototype-біни автоматично знищуються Spring під час збирання сміття» — контейнер взагалі не відстежує їхню деструкцію (
@PreDestroyне викликається). - ❌ «Впровадити Request-бін у Singleton можна безпосередньо без проксі» — застосунок або впаде з помилкою під час старту, або зафіксує перший контекст у всіх наступних запитах.
- ❌ «Для отримання Prototype у Singleton потрібно заінжектити
ApplicationContextі викликатиgetBean()» — антипатерн Service Locator, що порушує інверсію управління. Слід використовуватиObjectProvider.