🍃 Раздел 5 · Вопрос #10

Что такое 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 каверзных вопроса с ответами

  1. Что произойдет, если внедрить @Scope("prototype") бин в @Scope("singleton") сервис через обычный конструктор или @Autowired? Ответ: Prototype-бин создастся ровно один раз в момент инициализации синглтона. В результате он окажется «замороженным» (scoped mismatch) и при всех последующих обращениях к сервису будет переиспользоваться один и тот же экземпляр.

  2. Как правильно внедрить короткоживущий бин (например, Request или Prototype) в Singleton? Ответ: Есть 4 решения:
    1. Использовать ObjectProvider<T>.getObject().
    2. Настроить Scoped Proxy: @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS).
    3. Использовать методную инъекцию @Lookup.
    4. Использовать 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.

Связанные темы