1. Web-scopes у non-web-проєктах
Може виникнути відчуття: «Ми робимо консольний ContextFlow, то який узагалі стосунок тут мають request і session?». І це нормальна думка. Але web-scopes важливі як частина загальної карти Spring, щоб пізніше ви не ловили «магічні» помилки й не намагалися лікувати їх анотаціями навмання. Ми подивимося на ці scopes як на ідею про час життя, а не як на квиток у світ @Controller і HTTP.
Головна причина в тому, що ви дуже швидко зустрінете ці слова в реальних проєктах, навіть якщо поки не пишете web-код. Достатньо відкрити будь-який код, де є «поточний користувач», «поточна локаль», «кореляційний ідентифікатор запиту» — і раптом спливають «request-scoped» і «session-scoped» об’єкти. Якщо не розуміти, що це за звірі, ви або почнете зберігати все в singleton — а потім дивуватиметеся — або влаштуєте собі «prototype-хаос» і теж дивуватиметеся, тільки з іншої причини.
Друга причина — ці web-scopes дуже наочно показують, що scope = не про «скільки об’єктів» у вакуумі, а про те, який стан узагалі допустимий і у яких межах він живе. Це безпосередньо перегукується з тим, як ми проєктуємо OrderProcessingSession (prototype) і як далі говоритимемо про statefulness. Просто сьогодні — без заглиблення в багатопоточність і без web-runtime.
2. Web-середовище та web-scopes
Web-scopes не з’являються з повітря й не є «ще трьома рядками конфігурації». Вони з’являються тоді, коли Spring працює у web-середовищі, де є такі поняття, як HTTP-запит, HTTP-сесія і сам web-застосунок (контекст сервлета). Тобто контейнер отримує додаткову зовнішню «вісь часу», на яку можна прив’язати життя об’єктів. У нашому AnnotationConfigApplicationContext цієї осі просто немає, тому ми розглядаємо web-scopes як огляд і ментальну модель.
Якщо сильно спростити, у web-застосунку живуть щонайменше три часові рамки:
- один конкретний HTTP-запит (дуже короткий відрізок),
- одна користувацька сесія (довша: серія запитів одного користувача),
- сам застосунок (ще довше: доки працює сервер).
Spring вбудовується в це середовище й уміє зберігати та видавати об’єкти, прив’язані до цих рамок.
Зручно уявити це як «матрьошку часу»:
flowchart TD App["Web-застосунок
(живе найдовше)"] SessionA["HTTP-сесія користувача A
(живе довше)"] SessionB["HTTP-сесія користувача B
(живе довше)"] ReqA1["Запит A-1
(живе недовго)"] ReqA2["Запит A-2
(живе недовго)"] ReqB1["Запит B-1
(живе недовго)"] App --> SessionA App --> SessionB SessionA --> ReqA1 SessionA --> ReqA2 SessionB --> ReqB1
Тут важливо не переплутати: singleton у Spring і «application scope» — не одне й те саме слово про «один об’єкт». singleton — це «один екземпляр на ApplicationContext». А «application scope» — це «один екземпляр на web-застосунок (зазвичай на ServletContext)». Іноді це збігається за відчуттям, а іноді — ні. Трохи пізніше поясню, де саме підступ.
3. request scope: бін на запит
request scope — найкороткоживучіший із web-scopes. Найзручніше розуміти його так: доки виконується один HTTP-запит, Spring може дати вам об’єкт, який гарантовано належить саме цьому запиту й зникне одразу після завершення обробки. Це як паперовий браслет на концерті: доки ви всередині, він — ваша ідентичність, вийшли — усе, браслета немає, охорона вас знову не впізнає.
Які задачі природно лягають у request scope? Усе, що має сенс лише в межах одного запиту: наприклад, requestId для кореляції логів, поточна локаль запиту, час початку обробки, прапорці експерименту для цього виклику. Такий об’єкт може спокійно мати змінний стан, тому що він не переживає наступний запит і не повинен бути «спільним для всіх».
Мініскелет request-scoped компонента (це приклад лише для web-середовища, у нашому ContextFlow він не працюватиме так, як задумано):
import java.util.UUID;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("request") // Живе рівно один HTTP-запит (у non-web-середовищі scope не працюватиме)
class CurrentRequestId {
// Генерується один раз на екземпляр біна, тобто один раз на запит
private final String value = UUID.randomUUID().toString();
String value() {
return value;
}
}
Зверніть увагу на думку, а не на код: контейнер сам вирішує, коли створити такий об’єкт і коли його «поховати». Ви як розробник просто кажете: «цей об’єкт має жити рівно стільки, скільки request».
Дуже часта плутанина новачка — спробувати порівнювати request із prototype. Схожість справді є: і там, і там об’єкт «не вічний». Але prototype відповідає на запитання «що робити, коли хтось попросив бін у контейнера», а request — на запитання «що робити, коли прийшов HTTP-запит». У request scope є ще одна важлива особливість: всередині одного запиту різні залежності, яким потрібен цей bean, мають бачити один і той самий request-scoped екземпляр. У prototype такого «природного контексту запиту» немає: ви можете випадково отримати два різні екземпляри, якщо двічі попросите контейнер.
І тут є деталь, без якої web-scopes звучать як нова магія. У web-середовищі такі об’єкти зазвичай потрапляють у більш довгоживучий граф не як «поле, яке Spring мовчки переписує на кожен виклик», а через контейнерну проміжну логіку, часто у вигляді web-aware proxy, прив’язаного до поточного request/session context. Нам зараз достатньо зафіксувати сам принцип: request і session живуть завдяки поточному web-контексту, а не за правилами prototype.
4. session scope: бін на сесію
session scope живе довше за request scope. Якщо request — це «вдих-видих», то session — це «короткий серіал»: користувач зробив запит, потім ще один, потім ще… і в межах однієї HTTP-сесії Spring може зберігати об’єкт, який буде спільним для цих запитів. Це схоже на особисту шафку в спортзалі: ви приходите кілька разів протягом дня, і шафка ваша, але не назавжди.
На практиці session scope використовують для стану, який логічно прив’язаний до користувача й має пережити кілька запитів. Класичні приклади — кошик (у старих застосунках), налаштування відображення, вибрана локаль інтерфейсу, «останній переглянутий розділ». Тобто це речі, які не хочеться щоразу обчислювати при кожному запиті.
Мініскелет session-scoped об’єкта (знову ж таки: приклад для web-середовища, не для ContextFlow):
import java.util.Locale;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("session") // Один екземпляр на HTTP-сесію користувача
class UserSessionContext {
// Змінний стан: у session scope це допустимо, але треба пам’ятати, що об’єкт живе довго
private Locale locale = Locale.forLanguageTag("ru");
Locale locale() {
return locale;
}
void locale(Locale locale) {
this.locale = locale;
}
}
Тут важливо відчути, чому session scope взагалі існує. Якщо ви покладете подібний стан у singleton, ви отримаєте «одна локаль для всіх користувачів» — це звучить як початок анекдоту, але зазвичай закінчується баг-репортом. Якщо ви зробите це prototype, ви ризикуєте загубити стан між запитами, тому що на кожен запит збирається новий об’єкт. Session scope — це рівно та середина, яка дозволяє зберігати «користувацький стан» у межах однієї сесії.
І акуратний нюанс, який просто варто знати: session-scoped об’єкт може бути змінним, але він усе одно залишається «довгожителем» порівняно з request. Тому зберігати в ньому щось велике або чутливе — погана звичка. Причини ми сьогодні детально не розбираємо, але на рівні інтуїції: якщо щось живе довго, воно встигне накопичити проблеми.
5. application scope: бін на web-застосунок
application scope зазвичай сприймають як «ну це просто singleton, тільки інакше назвали». І ось тут Spring тихо посміхається й каже: «не зовсім». У web-середовищі application scope прив’язаний до життя web-застосунку, тобто приблизно до того, що в Servlet-світі називається ServletContext. Це робить його схожим на singleton за «довжиною життя», але не ідентичним за змістом зберігання та межами.
Якщо говорити простими словами, application — це об’єкт, який має жити рівно стільки, скільки живе ваше розгортання сервісу: підняли сервер — об’єкт з’явився, зупинили — зник. Він зручний для речей рівня «загальні налаштування web-застосунку», «кеш довідників», «спільний реєстр», але знову ж таки — усе залежить від архітектури.
Мінімальний приклад у web-середовищі:
import java.time.Instant;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("application") // Один екземпляр на весь web-застосунок (ServletContext)
class WebAppStartTime {
// Фіксуємо момент старту: цей стан живе стільки ж, скільки живе web-застосунок
private final Instant startedAt = Instant.now();
Instant startedAt() {
return startedAt;
}
}
Тепер найкорисніший концептуальний нюанс, без глибокого занурення в web-деталі. У звичайному Spring-застосунку ви можете мати кілька контекстів. У web це трапляється ще частіше: є root-контекст і може бути ще один контекст, пов’язаний із web-частиною. Оскільки singleton — це «один на ApplicationContext», то два контексти означають два singleton-екземпляри. А application scope частіше сприймається як «один на весь web-застосунок», тобто він може бути спільним поверх цих контекстів.
Повторю: вам не потрібно зараз запам’ятовувати, як саме це влаштовано в Servlet API. Важливо зрозуміти картину: singleton обмежений контейнером, а application scope — оточенням web-застосунку. У простому випадку це майже одне й те саме, але ментальна модель має бути ширшою, інакше в реальному проєкті можна дивуватися: «чому у мене два singleton-и?» — і це буде не філософське запитання.
6. Web-scopes і AnnotationConfigApplicationContext
Зараз саме час чесно зафіксувати межу курсу та проєкту: ContextFlow — non-web-застосунок. Ми піднімаємо AnnotationConfigApplicationContext, у нас немає HTTP-запитів, немає сесій, немає servlet-контексту. Тому web-scopes тут не мають «носія», до якого можна прив’язати життя об’єкта. У чистому контексті ці scopes зазвичай просто не зареєстровані, і спроба ними користуватися призводить до помилок.
Щоб відчути це руками, достатньо уявити такий код. Він компілюється, тому що @Scope — це частина spring-context, але в non-web-середовищі поводиться очікувано «сумно»:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Контекст піднято в non-web режимі: request/session/application scopes тут зазвичай відсутні
ctx.getBean(CurrentRequestId.class);
// У non-web-середовищі буде помилка: scope 'request' не зареєстровано
}
Тобто важливий висновок: scope — це не магічний рядок, який можна написати завжди. Scope працює лише тоді, коли контейнер знає, що це за scope, і коли є реальне середовище, яке може задати його межі.
І ось тут з’являється методичний сенс нашої лекції. Ми не намагаємося «додати web у ContextFlow». Ми просто додаємо у вашу картину світу розуміння: «крім singleton і prototype є ще scopes, які вмикаються лише у web-контексті».
7. Карта scopes: час життя і стан
Коли термінів стає багато, мозок починає робити те, що він любить найбільше: плутатися й перейменовувати все в «ну це наче singleton». Щоб цього не сталося, корисно тримати просту табличку, яка відповідає на одне запитання: до якої «осі часу» прив’язаний об’єкт. Не до «класу», не до «пакета», не до «рівня архітектури», а саме до часу життя.
Ось компактна карта — її не треба зубрити, краще перечитувати, доки вона не стане очевидною:
| Scope | Де має сенс | Скільки живе | Хто задає межу життя | Типовий стан |
|---|---|---|---|---|
| singleton | будь-який Spring-контекст | доки живе ApplicationContext | контейнер | зазвичай stateless сервіси та інфраструктура |
| prototype | будь-який Spring-контекст | доки ви самі тримаєте посилання | «запит до контейнера» | короткоживучі об’єкти на операцію (як наш OrderProcessingSession) |
| request | web | один HTTP-запит | web-запит | дані поточного запиту: requestId, локаль, час обробки |
| session | web | одна HTTP-сесія | сесія користувача | користувацькі налаштування, стан «між запитами» |
| application | web | доки живе web-застосунок | ServletContext | загальні об’єкти web-застосунку, інколи спільний кеш |
І тепер «фішка» дня: scope сам по собі не забороняє стан. Можна зробити stateful singleton — і це буде боляче, про це окрема лекція. Можна зробити stateless session-bean — і це буде дивно, але можливо. Scope відповідає на запитання «як довго об’єкт може жити», а ви вже маєте зробити висновок, який стан допустимо тримати за такої тривалості.
Якщо пов’язувати з ContextFlow, то зараз у нас є рівно та частина карти, яка потрібна non-web-застосунку: singleton і prototype. Prototype ми використовуємо точково, коли є осмислений «об’єкт на операцію» (OrderProcessingSession). Web-scopes ми тримаємо в голові як майбутню частину карти, але не впроваджуємо в проєкт, бо це було б зміною формату застосунку, а формат зафіксовано ТЗ.
8. Типові помилки під час знайомства з web-scopes
Коли розробник уперше бачить @Scope("request"), є небезпечна спокуса: «О, клас! Зараз я цим вирішу взагалі все». Spring у цей момент знову тихо посміхається, тому що scope — штука корисна, але дуже контекстна. Помилки тут зазвичай не «синтаксичні», а концептуальні: код компілюється, а в голові модель світу трохи не збігається з реальністю.
Помилка №1: спробувати використовувати request/session у non-web-застосунку та чекати, що воно «якось саме».
У AnnotationConfigApplicationContext немає HTTP-запитів і сесій. Контейнеру нема до чого прив’язати такі scopes, тому він або не знає про них, або не може коректно їх обслуговувати. Правильна реакція — не «а як зареєструвати scope вручну?», а «чому я взагалі намагаюся впровадити web-механіку в non-web-проєкт?».
Помилка №2: вважати, що request scope — це «те саме, що prototype, тільки краще».
Prototype створюється «коли попросили у контейнера». Request створюється «коли почався запит». Усередині одного запиту request-scoped об’єкт має бути єдиним для всіх, хто його використовує. Prototype легко створює кілька екземплярів, якщо ви зверталися до контейнера кілька разів. Це різні моделі життя.
Помилка №3: використовувати session scope як зручне місце для «покласти все, що шкода перераховувати щоразу».
Session — довгоживуча штука. Якщо перетворити її на склад даних, ви швидко отримаєте важкий стан на кожного користувача. Навіть без обговорення продуктивності це просто незручно супроводжувати: в об’єкта з’являється «довга пам’ять», і ви починаєте налагоджувати поведінку «вчорашнього стану» сьогодні.
Помилка №4: плутати application scope з «абсолютно безпечним singleton» і зберігати там що завгодно.
Application справді живе довго, але «довго живе» не означає «можна зберігати будь-який змінний стан без наслідків». Ба більше, у реальних web-застосунках іноді є кілька контекстів, і відмінності між singleton і application можуть проявитися в неочікуваних місцях. Краще ставитися до application як до усвідомленого вибору «об’єкт на весь web-застосунок», а не як до «ще одного способу зробити глобальну змінну».
Помилка №5: робити request/session-scoped об’єкт «доменною сутністю» і тягнути в контейнер усе підряд.
Scope — це не виправдання «давайте зробимо bean із Order». Доменну модель зазвичай створюють як звичайні Java-об’єкти, що живуть у межах операції. Контейнерні scopes потрібні для сервісів та інфраструктурних контекстів, а не для кожної сутності, яка випадково проходить через систему.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ