Чем Bean scopes отличаются в web-приложениях и какие риски у request/session scope?
Web-приложения добавляют к singleton/prototype три scope: request (новый на каждый HTTP-запрос), session (один на сессию) и application (один на ServletContext). При внедрении в singleton требуется scoped proxy; session-бины должны быть Serializable, иначе возможны утечки памяти и ошибки сериализации.
Bean scopes в Spring и их особенности в web-приложениях
Spring поддерживает несколько областей видимости бинов. В стандартных приложениях доступны singleton и prototype. В web-приложениях добавляются request, session, application и websocket.
Стандартные scopes
- singleton (по умолчанию): один экземпляр бина на весь ApplicationContext. Создаётся при старте, живёт до остановки приложения. Потокобезопасность — ответственность разработчика.
- prototype: новый экземпляр при каждом запросе к контейнеру (
getBeanили внедрение). Spring не управляет уничтожением prototype-бина — @PreDestroy не вызывается.
Web-специфичные scopes
// request scope — новый экземпляр на каждый HTTP-запрос
@Component
@RequestScope // == @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String traceId = UUID.randomUUID().toString();
private String userId;
// Живёт ровно один HTTP-запрос
}
// session scope — один экземпляр на HTTP-сессию
@Component
@SessionScope
public class ShoppingCart {
private List<CartItem> items = new ArrayList<>();
public void addItem(CartItem item) {
items.add(item);
}
}
// application scope — один экземпляр на ServletContext (аналог singleton, но на уровне сервлет-контейнера)
@Component
@ApplicationScope
public class GlobalSettings {
private Map<String, String> settings = new ConcurrentHashMap<>();
}
Как scoped-бины внедряются в singleton
Если singleton-бин напрямую внедряет request-scope бин, Spring создаст его один раз при старте — и в каждом запросе будет один и тот же экземпляр. Это неправильно. Решение — scoped proxy: контейнер внедряет прокси, который при каждом обращении делегирует к актуальному экземпляру из нужного scope.
@Service
public class OrderService {
// Внедряем прокси, а не реальный request-бин
private final RequestContext requestContext; // Это прокси-объект
public OrderService(RequestContext requestContext) {
this.requestContext = requestContext;
}
public void createOrder(OrderDto dto) {
// При каждом вызове прокси достаёт актуальный RequestContext текущего запроса
String traceId = requestContext.getTraceId();
log.info("[{}] Creating order", traceId);
}
}
Scoped proxy включается через proxyMode = ScopedProxyMode.TARGET_CLASS (для классов) или ScopedProxyMode.INTERFACES (для интерфейсов). Аннотации @RequestScope, @SessionScope, @ApplicationScope включают его автоматически.
Пример конфигурации request-scope бина вручную
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public AuditContext auditContext(HttpServletRequest request) {
AuditContext ctx = new AuditContext();
ctx.setIp(request.getRemoteAddr());
ctx.setUserAgent(request.getHeader("User-Agent"));
return ctx;
}
Сравнительная таблица
- singleton: один на контекст, thread-safety нужна явно, живёт всё время работы приложения.
- prototype: новый при каждом внедрении, @PreDestroy не вызывается, Spring не отслеживает жизненный цикл.
- request: новый на каждый HTTP-запрос, уничтожается после завершения запроса, нужен scoped proxy в singleton.
- session: один на HTTP-сессию, хранится в HttpSession, уничтожается при инвалидации сессии.
- application: один на ServletContext, ближе к singleton, но привязан к web-контейнеру.
Подводные камни
- Session scope и сериализация: session-бины сохраняются в HttpSession, которая может сериализоваться (при sticky sessions, failover). Все поля должны быть
Serializable, иначе возможнаNotSerializableException. - Memory leak в session scope: если session-бин держит тяжёлые объекты (коллекции, кэши), это увеличивает память пропорционально числу активных сессий. Сессии по умолчанию живут 30 минут.
- Thread safety в request scope: один HTTP-запрос обрабатывается одним потоком, поэтому request-бины потокобезопасны «из коробки». Но при использовании
@Asyncили CompletableFuture метод выполняется в другом потоке и RequestAttributes могут быть недоступны. - Отсутствие scoped proxy: если не добавить
proxyMode, внедрение request-бина в singleton вызоветBeanCreationException— «Scope 'request' is not active for the current thread». - Prototype в singleton без прокси: singleton, внедривший prototype-бин напрямую, получит один и тот же экземпляр prototype навсегда. Для обхода —
ObjectProvider<MyPrototype>или метод с@Lookup. - @PreDestroy у prototype: Spring создаёт prototype, но не отслеживает его —
@PreDestroyникогда не будет вызван. Нужно управлять ресурсами вручную. - application scope vs singleton:
@ApplicationScopeпривязан кServletContextи существует отдельно от Spring ApplicationContext. При перезагрузке веб-приложения без перезапуска JVM singleton-бины пересоздаются, а application-бин может сохраниться.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.