Spring FrameworkMiddleTechnical

Чем 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.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics