Питання 10 · Розділ 5

Що таке Bean scope?

НЕ зберігайте змінюваний стан у Singleton бінах (наприклад, кеш користувача в полі сервісу). Це призведе до race condition і витоків пам'яті. Замість цього:

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Scope — визначає, скільки екземплярів біна створює Spring.

Основні scope:

Scope Скільки екземплярів
Singleton Один на весь додаток (за замовчуванням)
Prototype Новий при кожному запиті
@Service  // Singleton за замовчуванням
public class UserService { }

@Scope("prototype")
@Service
public class ReportGenerator { }

Приклад:

// Singleton: завжди один і той самий
UserService s1 = context.getBean(UserService.class);
UserService s2 = context.getBean(UserService.class);
s1 == s2  // → true

// Prototype: щоразу новий
ReportGenerator r1 = context.getBean(ReportGenerator.class);
ReportGenerator r2 = context.getBean(ReportGenerator.class);
r1 == r2  // → false

🟡 Middle Level

Singleton

// За замовчуванням!
@Service
public class UserService { }

// Один екземпляр на ApplicationContext
// Усі потоки використовують один об'єкт
 Повинен бути Stateless (без стану)!

Prototype

@Scope("prototype")
@Service
public class ReportGenerator { }

// Новий екземпляр при КОЖНОМУ getBean()
// Spring створює, але НЕ управляє знищенням!
 @PreDestroy НЕ викликається

Web Scopes

@Scope("request")     // Один екземпляр на HTTP запит
@Scope("session")     // Один екземпляр на HTTP сесію
@Scope("application") // Один на ServletContext

Проблема Prototype в Singleton

// ❌ Prototype створиться ОДИН раз!
@Service
public class SingletonService {
    @Autowired
    private PrototypeBean prototype;  // Застряв один екземпляр!
}

// ✅ ObjectProvider
@Service
public class SingletonService {
    @Autowired
    private ObjectProvider<PrototypeBean> provider;

    public void process() {
        PrototypeBean bean = provider.getObject();  // Новий щоразу!
    }
}

🔴 Senior Level

Коли НЕ використовувати Singleton зі змінюваним станом

НЕ зберігайте змінюваний стан у Singleton бінах (наприклад, кеш користувача в полі сервісу). Це призведе до race condition і витоків пам’яті. Замість цього:

  • Винесіть стан у зовнішнє сховище (Redis, БД)
  • Використовуйте Request scope для даних запиту
  • Використовуйте Prototype якщо потрібен новий екземпляр

Scoped Proxy

// Вирішення проблеми Prototype/Request в Singleton
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserPreferences preferences() {
    return new UserPreferences();
}

// Spring впровадить проксі в Singleton
// Проксі при кожному виклику звертається до RequestContextHolder (ThreadLocal), який зберігає атрибути поточного HTTP-запиту. Таким чином, навіть singleton-бін, що інжектує request-scoped бін, щоразу отримує актуальний об'єкт для поточного запиту.

Under the hood: де зберігаються біни

Singleton → DefaultSingletonBeanRegistry (ConcurrentHashMap)
Request → RequestContextHolder (ThreadLocal) — ScopedProxyMode.TARGET_CLASS створює CGLIB-проксі, який при кожному виклику звертається до актуальної області (request/session). RequestContextHolder зберігає дані поточного HTTP-запиту в ThreadLocal.
Session → SessionAttributes
Prototype → Створюється, не зберігається

Custom Scope

// Свій scope!
public class ThreadScope implements Scope {
    private final ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();

    public Object get(String name, ObjectFactory<?> factory) {
        return threadLocal.get().computeIfAbsent(name, k -> factory.getObject());
    }
}

// Реєстрація
context.getBeanFactory().registerScope("thread", new ThreadScope());

Production Experience

Реальний сценарій: Request бін в Singleton

// ❌ Singleton отримав Request бін під час старту
// → Усі запити використовують дані першого користувача!

// ✅ ScopedProxyMode.TARGET_CLASS
// → Проксі → RequestContextHolder → актуальний бін

Best Practices

  1. Singleton — за замовчуванням (Stateless!)
  2. Prototype — рідко. Якщо вам просто потрібен новий об’єкт без залежностей Spring — використовуйте new. Але якщо об’єкт вимагає впровадження залежностей — Prototype виправданий.
  3. Request/Session — для web даних
  4. Scoped Proxy — для впровадження вузьких scope в широкі
  5. ObjectProvider — альтернатива Scoped Proxy. ObjectProvider vs ScopedProxy: ObjectProvider — явний виклик getObject() в коді (більш читабельно). ScopedProxy — проксі прозоро підставляється (менш помітно, але зручніше). ObjectProvider переважніший для явного контролю.
  6. Уникайте стану в Singleton

Резюме для Senior

  • Singleton = один на додаток, Stateless
  • Prototype = новий щоразу, @PreDestroy НЕ працює
  • Request/Session = web scope
  • Scoped Proxy = проксі → RequestContextHolder
  • ObjectProvider = явний запит біна
  • Custom Scope = свій lifecycle

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Singleton (за замовчуванням) — один екземпляр на ApplicationContext, повинен бути Stateless
  • Prototype — новий екземпляр при кожному запиті, @PreDestroy НЕ викликається, Spring не управляє знищенням
  • Web scopes: request (на HTTP-запит), session (на HTTP-сесію), application (на ServletContext)
  • Проблема Prototype в Singleton: prototype створюється ОДИН раз при впровадженні — використовуйте ObjectProvider.getObject()
  • Scoped Proxy (ScopedProxyMode.TARGET_CLASS) — CGLIB-проксі звертається до RequestContextHolder (ThreadLocal) при кожному виклику
  • Уникайте змінюваного стану в Singleton — race condition і витоки пам’яті
  • Custom Scope — можна створити свій (наприклад, ThreadScope через ThreadLocal)
  • Singleton зберігає біни в DefaultSingletonBeanRegistry (ConcurrentHashMap), Request — в ThreadLocal

Часті уточнюючі запитання:

  • Як впровадити Prototype в Singleton? ObjectProvider<PrototypeBean>.getObject() — новий щоразу, або Scoped Proxy.
  • Що таке Scoped Proxy? Проксі, яке при кожному виклику звертається до актуальної області (request/session) через RequestContextHolder.
  • Чому Singleton повинен бути Stateless? Один об’єкт використовується всіма потоками — змінюваний стан = race condition.
  • Коли використовувати Prototype замість new? Коли об’єкт вимагає впровадження залежностей від Spring.

Червоні прапорці (НЕ говорити):

  • «Singleton можна зберігати змінюваний стан» (race condition і витоки пам’яті)
  • «Prototype біни знищуються автоматично» (@PreDestroy НЕ викликається, чистіть вручну)
  • «Request/Session scope працюють без Scoped Proxy в Singleton» (буде один екземпляр на всі запити!)
  • «ObjectProvider і Scoped Proxy — одне й те саме» (ObjectProvider — явний getObject(), ScopedProxy — прозора підстановка)

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

  • [[6. Що таке Bean Lifecycle]]
  • [[7. Які етапи є у Bean lifecycle]]
  • [[9. Що роблять методи з анотаціями @PostConstruct та @PreDestroy]]
  • [[4. Що таке Bean в Spring]]
  • [[1. Що таке Dependency Injection]]