JavaRush /Курси /Spring Core /Request, session і ...

Request, session і application scope

Spring Core
Рівень 11 , Лекція 2
Відкрита

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 потрібні для сервісів та інфраструктурних контекстів, а не для кожної сутності, яка випадково проходить через систему.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ