Как работает инжекция request scoped бина в singleton бин в Spring и что происходит с полями и объектами
Что проверяют
Понимаете ли вы, что singleton создаётся один раз при старте контекста, а request-бин живёт один HTTP-запрос, и как Spring сводит эти два срока жизни. Следом обычно спрашивают, что лежит в поле singleton на самом деле и что будет при обращении к нему из метода с @Async.
Как отвечать
Внедрение происходит один раз, при создании singleton. Запроса в этот момент нет, поэтому с голым @Scope("request") контекст не поднимется: будет ошибка с текстом Scope 'request' is not active for the current thread, и Spring сам подскажет определить scoped proxy.
С прокси картина такая:
- что в поле: не настоящий бин, а прокси. Он внедряется один раз и не меняется всю жизнь singleton;
- как работает вызов: каждый метод прокси достаёт целевой объект из текущего запроса через
RequestContextHolder, то есть изThreadLocal, который заполняютDispatcherServletилиRequestContextFilter. Если в запросе объекта ещё нет, он создаётся; - поля request-бина: свои у каждого запроса, в конце запроса объект уничтожается и отрабатывает
@PreDestroy; - как включить:
@RequestScopeуже содержитproxyMode = ScopedProxyMode.TARGET_CLASS, а для@Scope(WebApplicationContext.SCOPE_REQUEST)режим прокси нужно задать явно.TARGET_CLASSстроит CGLIB-подкласс,INTERFACESделает JDK-прокси по интерфейсу.
Альтернатива без прокси: внедрить ObjectProvider<MyBean> и вызывать getObject() в момент использования, либо метод с @Lookup. Оба варианта вернут бин текущего запроса, а не новый объект на каждый вызов: так ведёт себя только prototype.
Где спотыкаются
Копируют данные из request-бина в поле singleton. Singleton один на все потоки, и значение одного пользователя увидит другой, это уже утечка данных между запросами.
Обращаются к прокси из @Async, CompletableFuture или планировщика. Там нет привязанного запроса, и вызов падает с IllegalStateException о том, что в потоке нет запроса.
Делают методы request-бина final при прокси через CGLIB. Такой метод не перехватывается и выполняется на самом прокси с пустыми полями, отсюда внезапные null.