1. Web-scopes в non-web проектах
Сейчас может возникнуть ощущение: «Мы делаем консольный ContextFlow, при чём тут вообще request и session?». И это нормальная мысль. Но web-scopes важны как часть общей карты Spring, чтобы позже вы не ловили “магические” ошибки и не пытались лечить их аннотациями наугад. Мы посмотрим на эти scope-ы как на идею про время жизни, а не как на билет в мир @Controller и HTTP.
Главная причина — вы очень быстро встретите эти слова в реальных проектах, даже если пока не пишете web-код. Вам достаточно открыть любой код, где есть “текущий пользователь”, “текущая локаль”, “корреляционный id запроса” — и внезапно всплывают “request-scoped” и “session-scoped” объекты. Если не понимать, что это за звери, вы либо начнёте хранить всё в singleton (и потом удивляться), либо устроите себе “prototype-хаос” (и тоже удивляться, просто по другой причине).
Вторая причина — эти web-scopes очень красиво подсвечивают, что scope = не про «сколько объектов» в вакууме, а про то, какое состояние вообще допустимо и в каких границах оно живёт. Это напрямую перекликается с тем, как мы проектируем OrderProcessingSession (prototype) и как будем дальше говорить о statefulness — просто сегодня без углубления в многопоточность и без web-runtime.
2. Web-aware окружение и web-scopes
Web-scopes не появляются из воздуха и не являются “ещё тремя строками конфигурации”. Они появляются тогда, когда Spring работает в web-окружении, где есть такие понятия как HTTP-запрос, HTTP-сессия и само web-приложение (контекст сервлета). То есть контейнер получает дополнительную внешнюю “ось времени”, на которую можно привязать жизнь объектов. В нашем AnnotationConfigApplicationContext этой оси просто нет, поэтому мы рассматриваем web-scopes как обзор и mental model.
Если сильно упростить, в 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. Похожесть действительно есть: и там, и там объект “не вечный”. Но “прототип” отвечает на вопрос “что делать, когда кто-то попросил бин у контейнера”, а “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;
}
}
Теперь самый полезный conceptual нюанс, без глубокого ухода в web-детали. В обычном Spring-приложении вы можете иметь несколько контекстов. В web это встречается ещё чаще: есть root-контекст и может быть ещё один контекст, связанный с web-частью. Поскольку singleton — “один на ApplicationContext”, то два контекста означают два singleton-экземпляра. А application scope чаще воспринимается как “один на всё web-приложение”, то есть он может быть общим “поверх” этих контекстов.
Повторю: вам не нужно сейчас запоминать, как именно это устроено в Servlet API. Важно понять картинку: singleton ограничен контейнером, а application scope — окружением web-приложения. В простом случае это почти одно и то же, но mental model должна быть шире, иначе в реальном проекте можно удивляться: “почему у меня два 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 есть ещё scope-ы, которые включаются только в web-контексте”.
7. Карта scopes: время жизни и состояние
Когда терминов становится много, мозг начинает делать то, что он любит больше всего: путаться и переименовывать всё в “ну это вроде как singleton”. Чтобы этого не случилось, полезно держать простую табличку, которая отвечает на один вопрос: к какой “оси времени” привязан объект. Не к “классу”, не к “пакету”, не к “уровню архитектуры”, а именно к времени жизни.
Вот компактная карта (её не надо зубрить, лучше перечитывать, пока она не станет очевидной):
| Scope | Где имеет смысл | Сколько живёт | “Кто” задаёт границу жизни | Типичное состояние |
|---|---|---|---|---|
| singleton | любой Spring-контекст | пока живёт ApplicationContext | контейнер | обычно stateless сервисы и инфраструктура |
| prototype | любой Spring-контекст | пока вы сами держите ссылку | “запрос к контейнеру” | короткоживущие объекты на операцию (как наш OrderProcessingSession) |
| request | web | один HTTP-запрос | web-запрос | данные текущего запроса: requestId, locale, timing |
| session | web | одна HTTP-сессия | сессия пользователя | пользовательские настройки, состояние “между запросами” |
| application | web | пока живёт web-приложение | ServletContext | общие объекты web-приложения, иногда shared-cache |
И теперь “фишка” дня: 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-запросов и сессий. Контейнеру не к чему привязать такие scope-ы, поэтому он либо не знает про них, либо не может их корректно обслуживать. Правильная реакция — не “а как зарегистрировать 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-объекты, живущие в рамках операции. Контейнерные scope-ы нужны для сервисов и инфраструктурных контекстов, а не для каждой сущности, которая случайно проходит через систему.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ