← Назад к вопросам
Какие плюсы и минусы Prototype Scope?
1.7 Middle🔥 181 комментариев
#Spring Framework
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Плюсы и минусы Prototype Scope в Spring
Prototype Scope — это один из самых спорных бинов в Spring. Когда я его встречаю в production коде, это обычно говорит о проблеме в дизайне. Расскажу реальные плюсы, минусы и когда его действительно стоит использовать.
Что такое Prototype Scope
Po умолчанию Spring beans имеют Singleton scope — создаётся один объект на всё приложение.
// Singleton (по умолчанию)
@Component
public class UserService {
// Одна копия на всё приложение
}
// Prototype — создаётся новый объект на каждый запрос
@Component
@Scope("prototype")
public class RequestContext {
// Новая копия каждый раз когда запросишь
}
Визуализация:
Singleton:
┌─────────────────────────────────┐
│ Spring Container │
│ UserService (одна копия) ────┐ │
└─────────────────────────────────┘
↓ inject
Controller1 получает → UserService
Controller2 получает → тот же UserService
Prototype:
┌─────────────────────────────────┐
│ Spring Container │
│ RequestContext (factory) ────┐ │
└─────────────────────────────────┘
↓ inject
Controller1 получает → RequestContext #1
Controller2 получает → RequestContext #2 (новая копия!)
ПЛЮСЫ Prototype Scope
1. Изоляция состояния между запросами
// Проблема: Singleton с состоянием
@Component
public class RequestProcessor {
private String requestId; // ❌ Shared между запросами!
private User currentUser; // ❌ Race condition
public void process(String id, User user) {
this.requestId = id;
this.currentUser = user;
// Если два потока одновременно вызовут process()
// они перезапишут друг другу значения!
}
}
// Решение: Prototype Scope
@Component
@Scope("prototype")
public class RequestProcessor {
private String requestId; // ✅ У каждого своя копия
private User currentUser; // ✅ Нет race condition
public void process(String id, User user) {
this.requestId = id;
this.currentUser = user;
// Безопасно — каждый поток имеет свой объект
}
}
2. Исключение необходимости ThreadLocal
// Без Prototype — нужен ThreadLocal (сложно)
@Component
public class SecurityContext {
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
public void setCurrentUser(User user) {
userHolder.set(user);
}
public User getCurrentUser() {
return userHolder.get();
}
// ❌ Нужно помнить очищать: userHolder.remove()
// ❌ Сложная отладка
// ❌ Утечки памяти если забыть remove()
}
// С Prototype (чище)
@Component
@Scope("prototype")
public class RequestContext {
private User currentUser; // Каждый request имеет свой
public void setCurrentUser(User user) {
this.currentUser = user;
}
public User getCurrentUser() {
return currentUser;
}
// ✅ Автоматически очищается когда request кончается
}
3. Проще с dependency injection
// ThreadLocal подход требует мануального management
private void doSomething() {
User user = new User("John");
securityContext.setCurrentUser(user);
try {
// обработка
} finally {
securityContext.removeCurrentUser(); // ❌ Нужно помнить
}
}
// Prototype подход — просто inject
@Controller
public class UserController {
private final RequestContext context; // ✅ Inject
public UserController(RequestContext context) {
this.context = context; // Каждый controller получает свой
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
context.setCurrentUser(user);
// Нет need в cleanup
}
}
4. Удобство для builder паттерна
// Prototype идеален для builder'ов
@Component
@Scope("prototype")
public class QueryBuilder {
private List<String> filters = new ArrayList<>();
private int limit = 100;
private int offset = 0;
public QueryBuilder withFilter(String filter) {
filters.add(filter);
return this;
}
public QueryBuilder limit(int limit) {
this.limit = limit;
return this;
}
public String build() {
return "SELECT * WHERE " + String.join(" AND ", filters) +
" LIMIT " + limit + " OFFSET " + offset;
}
}
// Использование
@Service
public class UserService {
@Autowired
private ObjectFactory<QueryBuilder> builderFactory; // Factory для Prototype
public String getActiveUsersQuery() {
return builderFactory.getObject() // Получить новый builder
.withFilter("active = true")
.withFilter("deleted = false")
.limit(50)
.build();
}
}
МИНУСЫ Prototype Scope
1. Высокое потребление памяти
// Если Prototype bean используется 1000 раз в день
@Component
@Scope("prototype")
public class DocumentProcessor {
private byte[] buffer = new byte[10 * 1024 * 1024]; // 10MB
private Map<String, Object> cache = new HashMap<>(); // может расти
// ...
}
// Результат:
// 1000 запросов × 10MB = 10GB памяти в день!
// GC будет работать постоянно
// Правильно: Singleton с pooling
@Component
public class DocumentProcessorPool {
private final Queue<DocumentProcessor> pool = new ConcurrentLinkedQueue<>();
public DocumentProcessor acquire() {
return pool.poll() != null ? pool.poll() : new DocumentProcessor();
}
public void release(DocumentProcessor processor) {
processor.reset();
pool.offer(processor);
}
}
2. Spring НЕ управляет lifecycle
// Singleton — Spring автоматически вызывает методы
@Component
public class SingletonService {
@PostConstruct
public void init() {
System.out.println("Singleton initialized");
// Вызывается один раз при старте приложения
}
@PreDestroy
public void cleanup() {
System.out.println("Singleton cleaning up");
// Вызывается при shutdown приложения
}
}
// Prototype — @PreDestroy НЕ вызывается!
@Component
@Scope("prototype")
public class PrototypeService {
@PostConstruct
public void init() {
System.out.println("Prototype initialized");
// Вызывается для каждого создания
}
@PreDestroy
public void cleanup() {
System.out.println("Prototype cleaning up");
// ❌ НЕ ВЫЗЫВАЕТСЯ! Spring не знает когда объект мёртв
}
}
// Утечка ресурсов!
public void leak() {
PrototypeService service = applicationContext.getBean(PrototypeService.class);
// service.cleanup() никогда не будет вызван!
// Если там открыт файл или connection — утечка
}
3. Проблема с inject Prototype в Singleton
// ❌ Неправильный паттерн (Prototype injected в Singleton)
@Component
public class SingletonService {
@Autowired
private PrototypeService prototype; // ❌ Будет создана ОДИН раз!
public void process() {
// prototype.state — SHARED между всеми вызовами
// Это не Prototype, это Singleton!
}
}
// ✅ Правильный паттерн 1: ObjectFactory
@Component
public class SingletonService {
@Autowired
private ObjectFactory<PrototypeService> prototypeFactory;
public void process() {
PrototypeService prototype = prototypeFactory.getObject(); // Новая копия
// Теперь правильно
}
}
// ✅ Правильный паттерн 2: ApplicationContext
@Component
public class SingletonService {
@Autowired
private ApplicationContext context;
public void process() {
PrototypeService prototype = context.getBean(PrototypeService.class);
// Новая копия
}
}
// ✅ Правильный паттерн 3: @Lookup (Spring AOP)
@Component
public class SingletonService {
@Lookup
protected PrototypeService getPrototype() {
// Spring переопределит этот метод через AOP
// Возвращает всегда новую копию
return null;
}
public void process() {
PrototypeService prototype = getPrototype(); // Новая копия
}
}
4. Сложность тестирования
// Singleton легко тестировать
@Test
public void testSingletonService() {
SingletonService service = new SingletonService(); // Простая конструкция
service.process();
// ...
}
// Prototype нужен контейнер
@SpringBootTest
public class PrototypeServiceTest {
@Autowired
private ApplicationContext context;
@Test
public void testPrototypeService() {
PrototypeService service1 = context.getBean(PrototypeService.class);
PrototypeService service2 = context.getBean(PrototypeService.class);
assertNotSame(service1, service2); // Проверяем что разные
}
}
5. Мониторинг и метрики усложняются
// Singleton легко мониторить
@Component
public class SingletonService {
private final MeterRegistry meterRegistry;
public void process() {
meterRegistry.counter("service.process").increment();
// Один счётчик для всех вызовов
}
}
// Prototype: нужно подумать как считать метрики
@Component
@Scope("prototype")
public class PrototypeService {
// Если счётчик в конструкторе — будет создан для каждой копии
// Много памяти
// Если глобальный счётчик — нужен синхронизм
}
6. Конфигурация усложняется
// Singleton — просто
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService(new UserRepository());
}
}
// Prototype — требует factory
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public RequestContext requestContext() {
return new RequestContext();
}
@Bean
public SingletonService singletonService(ObjectFactory<RequestContext> ctxFactory) {
return new SingletonService(ctxFactory); // Нужен factory
}
}
Когда использовать Prototype Scope
✅ ДА — Prototype нужен для:
// 1. Request-scoped объекты (но лучше использовать @RequestScope)
@Component
@Scope("prototype")
public class RequestContext {
private User currentUser;
// Каждый request имеет свой
}
// 2. Builder паттерны (редко)
@Component
@Scope("prototype")
public class QueryBuilder {
// ...
}
// 3. Когда нужна полная изоляция состояния
// (но обычно есть лучшие способы)
❌ НЕТ — Не используй Prototype для:
// 1. Тяжелых объектов (используй pooling вместо этого)
// 2. Объектов с ресурсами (файлы, connections)
// Утечки памяти
// 3. Если нужен request-scope
// Используй @RequestScope или RequestContextHolder
// 4. Просто так (эта ошибка очень распространена)
Правильные альтернативы
// Вместо Prototype — используй Spring context scopes
// 1. Request Scope (для web)
@Component
@RequestScope
public class RequestContext {
private User currentUser;
}
// 2. Session Scope (для web)
@Component
@SessionScope
public class UserSession {
private List<Item> cart;
}
// 3. RequestContextHolder для ThreadLocal management
public class SecurityUtils {
public static User getCurrentUser() {
return (User) RequestContextHolder.currentRequestAttributes()
.getAttribute("user", RequestAttributes.SCOPE_REQUEST);
}
}
Итоговая таблица
| Характеристика | Singleton | Prototype | Request |
|---|---|---|---|
| Memory | ✅ Минимум | ❌ Много | ✅ OK |
| Performance | ✅ Лучше | ❌ GC overhead | ✅ OK |
| Lifecycle | ✅ Управляется | ❌ Нет | ✅ Управляется |
| Простота | ✅ Простой | ❌ Сложный | ✅ Простой |
| State isolation | ❌ Нет | ✅ Да | ✅ Да |
| Thread safety | ❌ Нужна синхронизм | ✅ Нет | ✅ Встроена |
Золотое правило: Если вы думаете что вам нужен Prototype Scope, в 9 из 10 случаев вам нужен @RequestScope или RequestContextHolder. Настоящие use case'ы для Prototype редки.