🍃 Розділ 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 або Spring Cloud Bus) внутрішній кеш екземплярів скидається. При наступному зверненні до проксі цільовий об’єкт інстанціюється заново з новими властивостями з 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 і контекст.

Пов’язані теми