Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Request Scope в Spring: область видимости Bean
Request Scope — это одна из самых полезных, но и часто неправильно используемых сфер видимости Bean в Spring. Разберу подробно.
Что такое Scope
Scope определяет, как долго Bean живет в контексте приложения:
@Component // по умолчанию Singleton
public class MyService {
// один экземпляр на все приложение
}
@Component
@Scope("request") // новый экземпляр на каждый HTTP запрос
public class UserContext {
// разные экземпляры для разных пользователей
}
Встроенные Scope
1. Singleton (по умолчанию)
Один экземпляр на весь контекст приложения
2. Prototype
Новый экземпляр каждый раз, когда просишь у контекста
3. Request
Новый экземпляр на каждый HTTP запрос
4. Session
Один экземпляр на сессию пользователя
5. Application
Один экземпляр на сервлет контекст
Request Scope: подробное объяснение
Пользователь 1 делает GET /api/profile
→ Spring создает новый Bean с @Scope("request")
→ Bean обрабатывает запрос
→ Запрос завершен
→ Bean удаляется (garbage collect)
Пользователь 2 делает GET /api/profile
→ Spring создает НОВЫЙ Bean с @Scope("request")
→ Это полностью отдельный объект!
→ Bean обрабатывает запрос
→ Запрос завершен
→ Bean удаляется
Когда использовать Request Scope
// Сценарий 1: User Context (информация о текущем пользователе)
@Component
@Scope("request")
public class UserContext {
private User currentUser;
private String language;
private String timezone;
public User getUser() { return currentUser; }
public void setUser(User user) { this.currentUser = user; }
}
// Сценарий 2: Request-scoped Cache
@Component
@Scope("request")
public class RequestCache {
private Map<String, Object> cache = new HashMap<>();
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
}
// Сценарий 3: Audit Info (логирование действий)
@Component
@Scope("request")
public class AuditInfo {
private String requestId = UUID.randomUUID().toString();
private long startTime = System.currentTimeMillis();
private String ipAddress;
private String userAgent;
}
Практический пример: Request Scope Bean
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private final UserContext userContext; // Request-scoped!
public UserController(UserService userService, UserContext userContext) {
this.userService = userService;
this.userContext = userContext;
}
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
User user = userService.findById(id);
userContext.setUser(user); // сохраняю в контекст
// Где-то в другом компоненте (в этом же запросе):
// userContext.getUser() вернет ту же User
return UserDTO.from(user);
}
}
// В другом сервисе (автоматически инъектируется)
@Service
public class NotificationService {
private final UserContext userContext;
public NotificationService(UserContext userContext) {
this.userContext = userContext;
}
public void sendNotification(String message) {
User user = userContext.getUser(); // получаю User из контекста!
emailService.send(user.getEmail(), message);
}
}
Проблема: Singleton Bean + Request-scoped Dependency
Если Singleton Bean зависит от Request-scoped Bean, это проблема:
// НЕПРАВИЛЬНО!
@Component
public class SingletonService {
@Autowired
private UserContext userContext; // Request-scoped!
public void process() {
User user = userContext.getUser(); // какого пользователя получу?
// Если это Singleton, userContext создается один раз
// и потом используется для ВСЕх пользователей! ❌
}
}
Решение 1: ObjectProvider (рекомендуется)
@Component
public class SingletonService {
private final ObjectProvider<UserContext> userContextProvider;
public SingletonService(ObjectProvider<UserContext> userContextProvider) {
this.userContextProvider = userContextProvider;
}
public void process() {
UserContext userContext = userContextProvider.getObject(); // new каждый раз!
User user = userContext.getUser(); // правильный пользователь ✓
}
}
Решение 2: Proxy (CGLIB)
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserContext {
private User currentUser;
public User getUser() { return currentUser; }
public void setUser(User user) { this.currentUser = user; }
}
// Теперь Spring создает Proxy, который делегирует
// на правильный экземпляр UserContext для текущего запроса
@Component
public class SingletonService {
@Autowired
private UserContext userContext; // Proxy на Request-scoped Bean
public void process() {
User user = userContext.getUser(); // работает! ✓
}
}
Как Spring узнает, какой запрос
Один из самых частых вопросов: как Spring понимает, какому запросу какой Bean? Ответ: RequestContextHolder
// Когда приходит HTTP запрос, Spring сохраняет его в ThreadLocal
RequestContextHolder.setRequestAttributes(
new ServletRequestAttributes(request)
);
// Когда Spring создает Request-scoped Bean,
// он использует текущий RequestAttributes из ThreadLocal
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
String sessionId = attrs.getSessionId();
По этому работает примерно так:
Запрос 1 (thread-1) → RequestContextHolder.setRequestAttributes(req1)
↓
Spring создает UserContext для req1
↓
UserContext живет до конца req1
↓
RequestContextHolder очищает req1
↓
Bean удаляется (или реиспользуется если есть кэш)
Запрос 2 (thread-2) → RequestContextHolder.setRequestAttributes(req2)
↓
Spring создает НОВЫЙ UserContext для req2
↓
и т.д.
Важно: Thread-safety
Request-scoped Bean не нужно делать thread-safe:
@Component
@Scope("request")
public class UserContext {
// Не нужен synchronized!
// Каждый запрос имеет свой экземпляр на своем потоке
private User user; // thread-safe автоматически
}
Потому что:
- Каждый HTTP запрос = разные поток/процесс
- Каждому потоку = свой экземпляр Bean
- Разные потоки не видят друг друга
Session Scope (похоже на Request, но шире)
@Component
@Scope("session")
public class SessionData {
// Один экземпляр на сессию пользователя (вся сессия жизни)
// Если пользователь зайдет завтра — новый экземпляр
private String sessionId;
private List<String> recentViews;
}
// Различие:
// Request scope: новый Bean для каждого запроса
// Session scope: один Bean пока пользователь в сессии
Выводы
- Request Scope = новый Bean на каждый HTTP запрос
- Используй для user context, audit info, request cache
- Никогда не инъектируй Request-scoped Bean в Singleton без Proxy или ObjectProvider
- Используй
proxyMode = ScopedProxyMode.TARGET_CLASSилиObjectProvider<T> - Request-scoped Bean автоматически thread-safe
- Spring использует RequestContextHolder + ThreadLocal для связи запроса и Bean