← Назад к вопросам

Какие плюсы и минусы 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);
    }
}

Итоговая таблица

ХарактеристикаSingletonPrototypeRequest
Memory✅ Минимум❌ Много✅ OK
Performance✅ Лучше❌ GC overhead✅ OK
Lifecycle✅ Управляется❌ Нет✅ Управляется
Простота✅ Простой❌ Сложный✅ Простой
State isolation❌ Нет✅ Да✅ Да
Thread safety❌ Нужна синхронизм✅ Нет✅ Встроена

Золотое правило: Если вы думаете что вам нужен Prototype Scope, в 9 из 10 случаев вам нужен @RequestScope или RequestContextHolder. Настоящие use case'ы для Prototype редки.