Что такое scope Bean
Когда вы объявляете класс как @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);
// Зарегистрировать коллбэк, который выполнится при уничтожении области
void registerDestructionCallback(String name, Runnable callback);
// Разрешить контекстный объект (например, HttpServletRequest)
Object resolveContextualObject(String key);
// Получить идентификатор беседы (например, Session ID)
String getConversationId();
}
Когда вызывается context.getBean(beanName), метод AbstractBeanFactory.doGetBean() анализирует scope бина:
- Если
mbd.isSingleton()→ обращается кDefaultSingletonBeanRegistry.getSingleton(). - Если
mbd.isPrototype()→ вызываетcreateBean()без сохранения ссылки. - Если другой scope → находит реализацию
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 для изоляции бинов по какому-либо контексту (например, tenant в 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 каверзных вопроса с ответами
-
Что произойдет, если внедрить
@Scope("prototype")бин в@Scope("singleton")сервис через обычный конструктор или@Autowired? Ответ: Prototype-бин создастся ровно один раз в момент инициализации синглтона. В результате он окажется «замороженным» (scoped mismatch) и при всех последующих обращениях к сервису будет переиспользоваться один и тот же экземпляр. - Как правильно внедрить короткоживущий бин (например, Request или Prototype) в Singleton?
Ответ: Есть 4 решения:
- Использовать
ObjectProvider<T>.getObject(). - Настроить Scoped Proxy:
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS). - Использовать методную инъекцию
@Lookup. - Использовать
Provider<T>изjakarta.inject.
- Использовать
-
Как работает
ScopedProxyMode.TARGET_CLASSпод капотом? Ответ: Spring генерирует CGLIB-прокси для целевого класса. Этот прокси внедряется в синглтон вместо реального объекта. При каждом вызове любого бизнес-метода прокси обращается кRequestContextHolder(ThreadLocal) или соответствующей реализацииScope, извлекает актуальный для текущего потока/запроса экземпляр бина и делегирует ему вызов. - Где физически хранятся 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.