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

Какие scope существуют в Spring

В Spring Framework области видимости (scopes) разделяются на базовые (доступные в любом типе приложения) и веб-ориентированные (доступные только при наличии веб-контекста, напри...


🟢 Junior Level

В Spring Framework области видимости (scopes) разделяются на базовые (доступные в любом типе приложения) и веб-ориентированные (доступные только при наличии веб-контекста, например WebApplicationContext в Spring MVC / Boot Web).

Сводная таблица всех стандартных областей видимости

Scope Контекст Количество экземпляров и время жизни
singleton (по умолчанию) Любой Один экземпляр на весь ApplicationContext. Живет до закрытия контекста.
prototype Любой Новый экземпляр при каждом внедрении или вызове getBean().
request Web Один экземпляр на один HTTP-запрос. Уничтожается по завершении запроса.
session Web Один экземпляр на одну HTTP-сессию. Живет, пока активна сессия пользователя.
application Web Один экземпляр на весь ServletContext. Живет на уровне контейнера сервлетов.
websocket Web Один экземпляр на жизненный цикл WebSocket-сессии.

Синтаксис объявления в Spring Boot

В современном Spring Boot для веб-скоупов введены удобные композитные аннотации:

package com.example.scopes;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.ApplicationScope;
import org.springframework.web.context.annotation.RequestScope;
import org.springframework.web.context.annotation.SessionScope;

// 1. Singleton (по умолчанию)
@Component
public class AppCatalogService { }

// 2. Prototype
@Component
@Scope("prototype")
public class HeavyReportTask { }

// 3. Request Scope (равносильно @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS))
@Component
@RequestScope
public class RequestAuditLog { }

// 4. Session Scope (равносильно @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS))
@Component
@SessionScope
public class UserShoppingCart { }

// 5. Application Scope (равносильно @Scope(value = "application", proxyMode = ScopedProxyMode.TARGET_CLASS))
@Component
@ApplicationScope
public class ServletContextMetadata { }

🟡 Middle Level

Особенности веб-скоупов и где они хранятся

Для работы веб-скоупов Spring использует интерфейс RequestAttributes и статический диспетчер потоков RequestContextHolder, хранящий контекст текущего запроса в ThreadLocal.

  1. request scope:
    • Экземпляр бина сохраняется как атрибут в объекте сервлета: HttpServletRequest.setAttribute(name, bean).
    • При завершении обработки HTTP-запроса поток сервлета очищает атрибуты, и бин уничтожается.
  2. session scope:
    • Экземпляр сохраняется в HttpSession: HttpSession.setAttribute(name, bean).
    • Позволяет изолировать пользовательское состояние (корзина покупок, параметры локали) между разными HTTP-запросами одного клиента.
  3. application scope vs singleton:
    • В чем разница? Singleton существует в единственном экземпляре в рамках одного ApplicationContext.
    • Application scope создается в единственном экземпляре в рамках всего ServletContext.
    • Если в одном сервлет-контейнере (например, Tomcat) развернуто несколько диспетчеров сервлетов (DispatcherServlet), у каждого из них будет свой дочерний ApplicationContext со своими синглтонами, но бин с областью @ApplicationScope будет общим для всех них!

Режимы проксирования: ScopedProxyMode

При объявлении бинов с узким скоупом (request, session) критически важно правильно настроить proxyMode:

ScopedProxyMode:
  1. DEFAULT / NO:
     Прокси НЕ создается. При инъекции в Singleton возникнет ошибка или "заморозка" первого объекта.
     
  2. TARGET_CLASS (по умолчанию для @RequestScope / @SessionScope):
     Создается CGLIB-прокси (наследник класса). Работает для классов без интерфейсов.
     
  3. INTERFACES:
     Создается стандартный JDK Dynamic Proxy. Требует, чтобы бин реализовывал хотя бы один интерфейс.
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserProfileSession userProfileSession() {
    return new UserProfileSession();
}

🔴 Senior Level

Специальные и распределенные области видимости

1. @RefreshScope в Spring Cloud

В микросервисной архитектуре критически важно обновлять конфигурацию (токены, флаги фичей) без перезапуска JVM:

package com.example.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

@Component
@RefreshScope
@ConfigurationProperties(prefix = "payment.gateway")
public class PaymentGatewayProperties {
    private String apiUrl;
    private int timeout;
    // геттеры, сеттеры
}

Как устроен @RefreshScope под капотом:

  1. Spring Cloud регистрирует реализацию org.springframework.cloud.context.scope.refresh.RefreshScope.
  2. Бин оборачивается в CGLIB-прокси с интерцептором RefreshScope.
  3. Все экземпляры кэшируются во внутренней структуре BeanLifecycleDecoratorCache.
  4. При поступлении события RefreshEvent (или вызове эндпоинта /actuator/refresh) кэш экземпляров очищается.
  5. При следующем вызове любого метода через CGLIB-прокси интерцептор обнаруживает отсутствие экземпляра в кэше и вызывает createBeanInstance(), перечитывая обновленные Environment-свойства!

2. Проблема Session Scope в распределенной среде (Horizontal Scaling)

Если сервис масштабирован на несколько экземпляров (Kubernetes Pods / микросервисы за Load Balancer):

  • Хранение данных в стандартном session scope привязано к оперативной памяти конкретной ноды Tomcat.
  • Если последующий запрос пользователя попадает на другую ноду через балансировщик, сессия теряется (или требует настройки Sticky Sessions на балансировщике).
  • Архитектурное решение: использовать Spring Session + Redis. Сессия сериализуется и сохраняется в централизованном кэше Redis, что делает session scope полностью независимым от конкретной ноды.

Встроенный SimpleThreadScope в Spring Core

Spring «из коробки» предоставляет SimpleThreadScope, но намеренно не регистрирует его по умолчанию:

@Configuration
public class ThreadScopeConfiguration {

    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        // Регистрация встроенного SimpleThreadScope
        configurer.addScope("thread", new SimpleThreadScope());
        return configurer;
    }
}

Опасность SimpleThreadScope в пулах потоков: Потоки в сервлет-контейнерах и ThreadPoolTaskExecutor переиспользуются (ThreadLocal не очищается автоматически). Если поток завершил задачу и вернулся в пул, бин из SimpleThreadScope останется привязанным к потоку. Следующая задача, выполняемая этим же потоком, получит чужой грязный контекст. Кроме того, SimpleThreadScope не поддерживает коллбэки уничтожения (destruction callbacks)!


🎯 Шпаргалка для интервью

30-секундный ответ

В Spring существует 6 стандартных областей видимости:

  • Базовые: singleton (один на ApplicationContext, по умолчанию) и prototype (новый при каждом запросе).
  • Веб-ориентированные: request (на HTTP-запрос), session (на сессию пользователя), application (на ServletContext) и websocket (на вебсокет-сессию).
  • В экосистеме Spring Cloud добавляется @RefreshScope для горячей перезагрузки конфигурации. Веб-скоупы внедряются в синглтоны через Scoped Proxy (ScopedProxyMode.TARGET_CLASS), обращаясь к RequestContextHolder (ThreadLocal).

4 каверзных вопроса с ответами

  1. В чём разница между singleton и application scope? Ответ: Singleton создается в единственном экземпляре в рамках конкретного ApplicationContext. Application scope создается в единственном экземпляре в рамках ServletContext контейнера сервлетов. Если в одном контейнере запущено несколько ApplicationContext (например, разные дочерние контексты сервлетов), у каждого будет свой singleton, но один общий application scoped бин.

  2. Как работает аннотация @RequestScope в Spring Boot под капотом? Ответ: @RequestScope — это мета-аннотация, объединяющая @Scope("request") и proxyMode = ScopedProxyMode.TARGET_CLASS. Она генерирует CGLIB-прокси, который перехватывает вызовы методов и извлекает целевой бин из атрибутов текущего HttpServletRequest через статический диспетчер RequestContextHolder.

  3. Что делает @RefreshScope в Spring Cloud? Ответ: Он создает прокси над бином конфигурации. При получении события обновления настроек (через /actuator/refresh) внутренний кэш экземпляров сбрасывается. При следующем обращении к прокси целевой объект инстанцируется заново с новыми свойствами из Environment.

  4. Почему опасно использовать SimpleThreadScope в веб-приложениях? Ответ: Веб-серверы используют пулы потоков (Worker Pools). SimpleThreadScope хранит объекты в ThreadLocal. При возвращении потока в пул его ThreadLocal не очищается, что ведет к утечкам памяти и перекрестному загрязнению данных между разными пользователями. Также SimpleThreadScope не выполняет методы очистки (destruction callbacks).

Красные флаги (чего категорически нельзя говорить)

  • ❌ «Веб-скоупы работают в обычных консольных Java-приложениях» — без WebApplicationContext и RequestContextListener выбросится IllegalStateException: No thread-bound request found.
  • ❌ «@RequestScope создает обычный объект без прокси» — @RequestScope по умолчанию включает ScopedProxyMode.TARGET_CLASS.
  • ❌ «В распределенной системе можно спокойно хранить состояние в session scope без Redis» — без распределенного хранилища запросы на соседние ноды потеряют сессию.
  • ❌ «@RefreshScope перезапускает весь Spring Boot контекст» — он инвалидирует только помеченные бины, не перезагружая JVM и контекст.

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